Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
Gabriel Augendre | e54f681e47 | ||
Gabriel Augendre | 692a83b277 | ||
Gabriel Augendre | 9e67c19790 | ||
Gabriel Augendre | 6b7fb68f50 | ||
Gabriel Augendre | b5e7c3037f | ||
Gabriel Augendre | 323a5d86ea | ||
Gabriel Augendre | 02d5f86e48 | ||
Gabriel Augendre | 4bd48ffedd | ||
Gabriel Augendre | bf76cd227a |
51
.eslintrc
Normal file
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jquery": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"ignorePatterns": ["dist/", "node_modules/"],
|
||||
"rules": {
|
||||
"block-scoped-var": "error",
|
||||
"consistent-return": "error",
|
||||
"curly": "error",
|
||||
"default-case": "error",
|
||||
"default-param-last": ["error"],
|
||||
"dot-notation": "error",
|
||||
"eqeqeq": "error",
|
||||
"guard-for-in": "error",
|
||||
"max-classes-per-file": "error",
|
||||
"no-alert": "error",
|
||||
"no-caller": "error",
|
||||
"no-else-return": "error",
|
||||
"no-empty-function": "error",
|
||||
"no-floating-decimal": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-multi-str": "error",
|
||||
"no-param-reassign": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-return-await": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-useless-concat": "error",
|
||||
"radix": ["error", "as-needed"],
|
||||
"require-await": "error",
|
||||
"yoda": "error",
|
||||
"no-shadow": "off",
|
||||
"prefer-destructuring": ["error", { "array": false, "object": true }],
|
||||
"padding-line-between-statements": [
|
||||
"error",
|
||||
{ "blankLine": "always", "prev": "import", "next": "export" },
|
||||
{ "blankLine": "always", "prev": "export", "next": "export" },
|
||||
{ "blankLine": "always", "prev": "*", "next": "return" }
|
||||
]
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
envs/local.env
|
||||
envs/docker-local.env
|
||||
.envrc
|
||||
pytest_result
|
||||
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
exclude: \.min\.(js|css)$|/generated/
|
||||
exclude: (\.min\.(js|css)(\.map)?$|/vendor/)
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.4.0
|
||||
rev: v4.1.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
types: [python]
|
||||
- id: check-json
|
||||
types: [json]
|
||||
- id: check-toml
|
||||
types: [toml]
|
||||
- id: check-xml
|
||||
types: [xml]
|
||||
- id: check-yaml
|
||||
types: [yaml]
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
- id: detect-private-key
|
||||
- id: pretty-format-json
|
||||
args:
|
||||
- --autofix
|
||||
|
@ -23,12 +21,57 @@ repos:
|
|||
args:
|
||||
- --markdown-linebreak-ext=md
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.8.0
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
types: [python]
|
||||
args:
|
||||
- --profile=black
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
rev: 22.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
types: [python]
|
||||
- id: black
|
||||
args:
|
||||
- --target-version=py310
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py310-plus
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.4.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [--target-version, "4.0"]
|
||||
- repo: https://github.com/rtts/djhtml
|
||||
rev: v1.5.0
|
||||
hooks:
|
||||
- id: djhtml
|
||||
- repo: https://github.com/flakeheaven/flakeheaven
|
||||
rev: 0.11.0
|
||||
hooks:
|
||||
- id: flakeheaven
|
||||
additional_dependencies:
|
||||
- flake8-annotations-complexity
|
||||
- flake8-builtins
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
- flake8-eradicate
|
||||
- flake8-noqa
|
||||
- flake8-pytest-style
|
||||
- flake8-pyi
|
||||
- wemake-python-styleguide
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.5.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, css]
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.9.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
args: [--fix]
|
||||
types_or: [javascript, css]
|
||||
additional_dependencies:
|
||||
- eslint@^7.29.0
|
||||
- eslint-config-prettier@^8.3.0
|
||||
|
|
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
79
Dockerfile
|
@ -1,34 +1,63 @@
|
|||
FROM python:3.6-alpine
|
||||
## Build venv
|
||||
FROM python:3.10.4-bullseye AS venv
|
||||
|
||||
RUN apk add --update postgresql-libs && \
|
||||
apk add --udpate --virtual .build-deps gcc musl-dev postgresql-dev tzdata && \
|
||||
cp /usr/share/zoneinfo/Europe/Paris /etc/localtime && \
|
||||
echo "Europe/Paris" > /etc/timezone
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gettext
|
||||
|
||||
# https://python-poetry.org/docs/#installation
|
||||
ENV POETRY_VERSION=1.1.13
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
ENV PATH /root/.local/bin:$PATH
|
||||
ARG POETRY_OPTIONS
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 8000
|
||||
VOLUME /app/staticfiles
|
||||
|
||||
RUN pip3 install pipenv
|
||||
COPY Pipfile Pipfile.lock ./
|
||||
RUN pipenv install --deploy
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
|
||||
RUN apk del .build-deps
|
||||
RUN python -m venv --copies /app/venv \
|
||||
&& . /app/venv/bin/activate \
|
||||
&& poetry install $POETRY_OPTIONS
|
||||
|
||||
COPY . ./
|
||||
ENV PATH /app/venv/bin:$PATH
|
||||
COPY src ./src/
|
||||
ARG SECRET_KEY="somevalue"
|
||||
ARG DATABASE_URL="somevalue"
|
||||
RUN python ./src/manage.py collectstatic --no-input
|
||||
RUN python ./src/manage.py compilemessages -l fr -l en
|
||||
|
||||
CMD ["sh", "bash/run-prod.sh"]
|
||||
## Get git versions
|
||||
FROM alpine/git AS git
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
RUN git rev-parse HEAD | tee /version
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=10s CMD ["pipenv", "run", "python", "healthcheck.py"]
|
||||
|
||||
ENV DATABASE_URL postgres://postgresql:postgresql@db:5432/manuels
|
||||
ENV SECRET_KEY ''
|
||||
ENV MAILGUN_ACCESS_KEY ''
|
||||
ENV MAILGUN_SERVER_NAME ''
|
||||
ENV DJANGO_ENV ''
|
||||
ENV ADMIN_EMAIL ''
|
||||
ENV SERVER_EMAIL ''
|
||||
ENV HOST ''
|
||||
ENV REPLY_TO ''
|
||||
ENV AUTHORIZED_EMAILS ''
|
||||
ENV LIBRARIAN_EMAILS ''
|
||||
## Beginning of runtime image
|
||||
FROM python:3.10.4-slim-bullseye as prod
|
||||
ENV TZ "Europe/Paris"
|
||||
RUN mkdir -p /app/db
|
||||
|
||||
COPY --from=venv /app/venv /app/venv/
|
||||
ENV PATH /app/venv/bin:$PATH
|
||||
|
||||
WORKDIR /app
|
||||
COPY LICENSE pyproject.toml ./
|
||||
COPY docker ./docker/
|
||||
COPY src ./src/
|
||||
COPY --from=git /version /app/.version
|
||||
COPY --from=venv /app/staticfiles /app/staticfiles/
|
||||
|
||||
ENV SECRET_KEY "changeme"
|
||||
ENV DEBUG "false"
|
||||
ENV DB_BASE_DIR "/app/db"
|
||||
#ENV HOSTS="host1;host2"
|
||||
#ENV ADMINS='Full Name,email@example.com'
|
||||
#ENV MAILGUN_API_KEY='key-yourapikey'
|
||||
#ENV MAILGUN_SENDER_DOMAIN='mailgun.example.com'
|
||||
#ENV BLOG_BASE_URL='https://url-of-your-blog.example.com'
|
||||
|
||||
HEALTHCHECK --start-period=30s CMD python -c "import requests; requests.get('http://localhost:8000', timeout=2)"
|
||||
|
||||
WORKDIR /app/src
|
||||
CMD ["/app/docker/run.sh"]
|
||||
|
|
1
LICENSE
|
@ -22,4 +22,3 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
|
||||
|
|
24
README.md
|
@ -4,21 +4,15 @@ Help librarian manage textbooks requests from colleagues
|
|||
## Local development
|
||||
|
||||
```bash
|
||||
pipenv install
|
||||
pipenv run python manage.py migrate
|
||||
DJANGO_ENV=dev pipenv run python manage.py runserver
|
||||
pyenv virtualenv 3.10.4 manuels
|
||||
pyenv local manuels
|
||||
poetry install
|
||||
cp envs/local.env.dist envs/local.env # edit env file, it won't work without mailgun keys
|
||||
echo 'export ENV_FILE=$(realpath "./envs/local.env")' > .envrc
|
||||
direnv allow
|
||||
inv test
|
||||
python src/manage.py runserver
|
||||
```
|
||||
|
||||
## Deploy on Heroku
|
||||
|
||||
```bash
|
||||
heroku login
|
||||
heroku git:remote --app manuels-scolaires
|
||||
git push heroku master
|
||||
```
|
||||
|
||||
You may need to upgrade Python since Heroku tends to deprecate old patch versions rather quickly.
|
||||
In this case, edit `runtime.txt`.
|
||||
|
||||
# Reuse
|
||||
If you do reuse my work, please consider linking back to this repository 🙂
|
||||
If you do reuse my work, please consider linking back to this repository 🙂
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/sh
|
||||
yes yes | pipenv run python manage.py migrate && \
|
||||
yes yes | pipenv run python manage.py createcachetable && \
|
||||
pipenv run python manage.py collectstatic --noinput && \
|
||||
pipenv run gunicorn manuels_collection.wsgi -b 0.0.0.0:8000 --log-file -
|
12
docker-compose-build.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
version: '2.4'
|
||||
services:
|
||||
django:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: django
|
||||
image: crocmagnon/manuels-scolaires:latest
|
||||
platform: linux/amd64
|
||||
|
||||
volumes:
|
||||
staticfiles: {}
|
||||
media: {}
|
34
docker-compose.yaml
Normal file
|
@ -0,0 +1,34 @@
|
|||
version: '2.4'
|
||||
services:
|
||||
django:
|
||||
image: crocmagnon/manuels-scolaires:dev
|
||||
platform: linux/amd64
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
POETRY_OPTIONS: "--no-dev"
|
||||
env_file:
|
||||
- envs/docker-local.env
|
||||
volumes:
|
||||
- staticfiles:/app/staticfiles
|
||||
- media:/app/media
|
||||
restart: on-failure
|
||||
init: true
|
||||
tty: true
|
||||
ports:
|
||||
- "8000:8000"
|
||||
postgres:
|
||||
image: postgres:14
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "manuels"
|
||||
POSTGRES_USER: "manuels"
|
||||
POSTGRES_DB: "manuels"
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
staticfiles: {}
|
||||
media: {}
|
||||
pg_data: {}
|
6
docker/run.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
set -eux
|
||||
python manage.py migrate --noinput
|
||||
python manage.py createcachetable
|
||||
python manage.py clearcache
|
||||
gunicorn manuels_collection.wsgi -b 0.0.0.0:8000 --log-file -
|
|
@ -1,104 +0,0 @@
|
|||
document.addEventListener("DOMContentLoaded", function (event) {
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
|
||||
var isbnButton = document.querySelector('#id_isbn_button');
|
||||
var isbn = document.querySelector('#id_isbn');
|
||||
var title = document.querySelector('#id_title');
|
||||
var authors = document.querySelector('#id_authors');
|
||||
var year = document.querySelector('#id_publication_year');
|
||||
var price = document.querySelector('#id_price');
|
||||
var editor = document.querySelector('#id_editor');
|
||||
var otherEditor = document.querySelector('#id_other_editor');
|
||||
var spinner = document.querySelector('#id_isbn_spinner');
|
||||
var feedback = document.querySelector('#id_isbn_invalid_feedback');
|
||||
|
||||
function enableFields() {
|
||||
isbn.removeAttribute('disabled');
|
||||
isbnButton.removeAttribute('disabled');
|
||||
document.querySelector('#id_title').removeAttribute('disabled');
|
||||
authors.removeAttribute('disabled');
|
||||
year.removeAttribute('disabled');
|
||||
price.removeAttribute('disabled');
|
||||
editor.removeAttribute('disabled');
|
||||
otherEditor.removeAttribute('disabled');
|
||||
spinner.setAttribute('hidden', 'hidden');
|
||||
}
|
||||
|
||||
function disableFields() {
|
||||
isbn.setAttribute('disabled', 'disabled');
|
||||
isbnButton.setAttribute('disabled', 'disabled');
|
||||
title.setAttribute('disabled', 'disabled');
|
||||
authors.setAttribute('disabled', 'disabled');
|
||||
year.setAttribute('disabled', 'disabled');
|
||||
price.setAttribute('disabled', 'disabled');
|
||||
editor.setAttribute('disabled', 'disabled');
|
||||
otherEditor.setAttribute('disabled', 'disabled');
|
||||
spinner.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
isbnButton.addEventListener('click', function (event) {
|
||||
if (!isbn.value) {
|
||||
return;
|
||||
}
|
||||
disableFields();
|
||||
|
||||
fetch("/isbn_api/" + isbn.value).then(function (data) {
|
||||
if (!data.ok) {
|
||||
throw Error("Erreur dans la récupération des données");
|
||||
}
|
||||
return data.json();
|
||||
}).then(function (data) {
|
||||
isbn.classList.remove('is-invalid');
|
||||
isbn.classList.add('is-valid');
|
||||
feedback.style.display = 'none';
|
||||
feedback.textContent = '';
|
||||
|
||||
title.value = data.title;
|
||||
title.classList.add('is-valid');
|
||||
authors.value = data.authors;
|
||||
authors.classList.add('is-valid');
|
||||
year.value = data.year;
|
||||
year.classList.add('is-valid');
|
||||
price.value = data.price;
|
||||
price.classList.add('is-valid');
|
||||
|
||||
var editorValue = "";
|
||||
var editorIsOther = false;
|
||||
if (data.editor) {
|
||||
for (var option of document.querySelector('#id_editor').children) {
|
||||
if (editorValue === "" && option.firstChild.data.toLowerCase().indexOf('autre') !== -1) {
|
||||
editorValue = option.value;
|
||||
editorIsOther = true;
|
||||
}
|
||||
if (option.firstChild.data.toLowerCase() === data.editor.toLowerCase()) {
|
||||
editorValue = option.value;
|
||||
editorIsOther = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.value = editorValue;
|
||||
editor.classList.add('is-valid');
|
||||
|
||||
if (editorIsOther) {
|
||||
otherEditor.value = data.editor;
|
||||
otherEditor.classList.add('is-valid');
|
||||
}
|
||||
enableFields();
|
||||
|
||||
// The event propagation must be done after the fields have been re-enabled
|
||||
// because a change event can't be propagated to a field that's disabled.
|
||||
var event = document.createEvent("HTMLEvents");
|
||||
event.initEvent("change", true, true);
|
||||
event.eventName = "change";
|
||||
document.querySelector('#id_editor').dispatchEvent(event);
|
||||
}).catch(function(error) {
|
||||
isbn.classList.add('is-invalid');
|
||||
isbn.classList.remove('is-valid');
|
||||
feedback.style.display = 'block';
|
||||
feedback.textContent = error;
|
||||
enableFields();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
document.addEventListener("DOMContentLoaded", function (event) {
|
||||
var selectors = [
|
||||
{
|
||||
id: "#id_no_book",
|
||||
value: "PAS DE LIVRE POUR CETTE CLASSE"
|
||||
},
|
||||
{
|
||||
id: "#id_see_later",
|
||||
value: "VOIR À LA RENTRÉE"
|
||||
},
|
||||
];
|
||||
selectors.forEach(function (selector, index, array) {
|
||||
var _selector = document.querySelector(selector.id);
|
||||
if (_selector === null) return;
|
||||
var data = {
|
||||
title: document.querySelector('#id_title').value,
|
||||
authors: document.querySelector('#id_authors').value,
|
||||
publicationYear: document.querySelector('#id_publication_year').value,
|
||||
isbn: document.querySelector('#id_isbn').value,
|
||||
price: document.querySelector('#id_price').value,
|
||||
editor: document.querySelector('#id_editor').value,
|
||||
previouslyAcquired: document.querySelector('#id_previously_acquired').value,
|
||||
};
|
||||
_selector.addEventListener('change', function (event) {
|
||||
if (_selector.checked) {
|
||||
data = {
|
||||
title: document.querySelector('#id_title').value,
|
||||
authors: document.querySelector('#id_authors').value,
|
||||
publicationYear: document.querySelector('#id_publication_year').value,
|
||||
isbn: document.querySelector('#id_isbn').value,
|
||||
price: document.querySelector('#id_price').value,
|
||||
editor: document.querySelector('#id_editor').value,
|
||||
previouslyAcquired: document.querySelector('#id_previously_acquired').value,
|
||||
};
|
||||
document.querySelector('#id_title').value = selector.value;
|
||||
document.querySelector('#id_authors').value = "N/A";
|
||||
document.querySelector('#id_publication_year').value = 1900;
|
||||
document.querySelector('#id_isbn').value = "0000000000";
|
||||
document.querySelector('#id_price').value = 0;
|
||||
document.querySelector('#id_previously_acquired').value = "False";
|
||||
var editorValue = null;
|
||||
for (var option of document.querySelector('#id_editor').children) {
|
||||
if (editorValue === null && option.value !== "") {
|
||||
editorValue = option.value;
|
||||
}
|
||||
if (option.firstChild.data.toLowerCase().indexOf('autre') !== -1) {
|
||||
editorValue = option.value;
|
||||
}
|
||||
}
|
||||
document.querySelector('#id_editor').value = editorValue;
|
||||
} else {
|
||||
document.querySelector('#id_title').value = data.title;
|
||||
document.querySelector('#id_authors').value = data.authors;
|
||||
document.querySelector('#id_editor').value = data.editor;
|
||||
document.querySelector('#id_publication_year').value = data.publicationYear;
|
||||
document.querySelector('#id_isbn').value = data.isbn;
|
||||
document.querySelector('#id_price').value = data.price;
|
||||
document.querySelector('#id_previously_acquired').value = data.previouslyAcquired;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function toggleOtherEditorDisplay() {
|
||||
var editor = document.querySelector('#id_editor');
|
||||
var otherEditor = document.querySelector('#id_other_editor').parentElement;
|
||||
|
||||
if (editor.options[editor.selectedIndex].text.toLowerCase().indexOf('autre') !== -1) {
|
||||
otherEditor.style.display = 'block';
|
||||
}
|
||||
else {
|
||||
otherEditor.style.display = 'none';
|
||||
}
|
||||
}
|
||||
toggleOtherEditorDisplay();
|
||||
|
||||
document.querySelector('#id_editor').addEventListener('change', toggleOtherEditorDisplay);
|
||||
|
||||
});
|
|
@ -1,88 +0,0 @@
|
|||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Manuels - {% block title %}{% endblock %}</title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180"
|
||||
href="{% static 'icons/apple-touch-icon.png' %}">
|
||||
<link rel="icon" type="image/png" sizes="32x32"
|
||||
href="{% static 'icons/favicon-32x32.png' %}">
|
||||
<link rel="icon" type="image/png" sizes="16x16"
|
||||
href="{% static 'icons/favicon-16x16.png' %}">
|
||||
<link rel="manifest" href="{% static 'icons/site.webmanifest' %}">
|
||||
<link rel="mask-icon" href="{% static 'icons/safari-pinned-tab.svg' %}"
|
||||
color="#000000">
|
||||
<link rel="shortcut icon" href="{% static 'icons/favicon.ico' %}">
|
||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
||||
<meta name="msapplication-config"
|
||||
content="{% static 'icons/browserconfig.xml' %}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l"
|
||||
crossorigin="anonymous">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="{% url 'home_page' %}">Manuels scolaires</a>
|
||||
{% if user.is_authenticated or teacher and teacher.email in authorized_mails %}
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse"
|
||||
data-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}">
|
||||
<i class="fas fa-door-open"></i>
|
||||
Administration
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<main class="flex-fill">
|
||||
<div class="container-fluid">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} fade show" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-light py-3">
|
||||
<div class="container-fluid">
|
||||
<span class="text-muted">Ce service est un logiciel libre sous licence MIT réalisé par
|
||||
Gabriel Augendre d'après des besoins exprimés par Sandrine Augendre. Le code source est disponible
|
||||
<a href="https://git.augendre.info/gaugendre/manuels-scolaires">à cette adresse</a>.</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script defer src="https://kit.fontawesome.com/350c07ee78.js"
|
||||
integrity="sha384-IwFbZvLB3nqmwJikzn6JZAqNDTwjFfauT4djixzjaxmR030Fd2gx05kWWwBLwRYZ"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
|
||||
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
{% block end_js %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +0,0 @@
|
|||
<html>
|
||||
<p>Bonjour,</p>
|
||||
<p>
|
||||
{{ teacher.full_name }} a confirmé ses listes sur <a href="{{ link }}">{{ link }}</a>
|
||||
</p>
|
||||
</html>
|
|
@ -1,10 +0,0 @@
|
|||
<html>
|
||||
<p>Bonjour {{ teacher.first_name }},</p>
|
||||
<p>
|
||||
Voici votre lien pour la gestion des manuels scolaires :
|
||||
</p>
|
||||
|
||||
<p style="text-align: center;">
|
||||
<a href="{{ link }}">{{ link }}</a>
|
||||
</p>
|
||||
</html>
|
|
@ -1,149 +0,0 @@
|
|||
{% extends 'manuels/base.html' %}
|
||||
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block title %}Livres et fournitures demandés{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Bienvenue {{ teacher.full_name }}
|
||||
<a href="{% url 'clear_teacher' %}" class="btn btn-warning">
|
||||
<i class="fas fa-sign-out-alt"></i> Se déconnecter</a>
|
||||
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<a href="{% url 'confirm_teacher' pk=teacher.pk %}" class="btn btn-danger">
|
||||
<i class="fas fa-check-circle"></i> Confirmer ma liste</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> Attention</h4>
|
||||
<p class="mb-0">
|
||||
Le lien permettant d'accéder à cette page vous est personnel et vous a été envoyé par email.
|
||||
N'oubliez pas de vérifier vos courriers indésirables.<br>
|
||||
Pensez à <strong>sauvegarder cette page dans vos favoris</strong>.<br>
|
||||
Si vous perdez ce lien, vous ne risquez de ne plus être en mesure d'accéder à votre espace personnel
|
||||
et vous ne pourrez plus ajouter de livre à votre liste.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>
|
||||
Liste des livres demandés
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<a href="{% url 'add_book' pk=teacher.pk %}"
|
||||
class="btn btn-primary"
|
||||
id="add-book">
|
||||
<i class="fas fa-plus-circle"></i> Ajouter un livre
|
||||
</a>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="col">Modifier</th>
|
||||
{% endif %}
|
||||
<th scope="col">Classe</th>
|
||||
<th scope="col">Discipline</th>
|
||||
<th scope="col">Titre</th>
|
||||
<th scope="col">Auteurs</th>
|
||||
<th scope="col">Éditeur</th>
|
||||
<th scope="col">Année de publication</th>
|
||||
<th scope="col">ISBN</th>
|
||||
<th scope="col">Prix</th>
|
||||
<th scope="col">Déjà acheté par l'élève</th>
|
||||
<th scope="col">Consommable</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for book in teacher.book_set.all %}
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="row">
|
||||
<div class="btn-group">
|
||||
<a title="Modifier"
|
||||
href="{% url 'edit_book' teacher_pk=book.teacher.pk pk=book.pk %}"
|
||||
class="btn btn-sm btn-secondary"><i class="fas fa-edit"></i></a>
|
||||
<a title="Supprimer"
|
||||
href="{% url 'delete_book' teacher_pk=book.teacher.pk pk=book.pk %}"
|
||||
class="btn btn-sm btn-danger"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</th>
|
||||
{% endif %}
|
||||
<td>{{ book.level }}</td>
|
||||
<td>{{ book.field }}</td>
|
||||
<td>{{ book.title }}</td>
|
||||
<td>{{ book.authors }}</td>
|
||||
<td>{{ book.editor }}{% if book.other_editor %}
|
||||
({{ book.other_editor }})
|
||||
{% endif %}</td>
|
||||
<td>{{ book.publication_year }}</td>
|
||||
<td>{{ book.isbn }}</td>
|
||||
<td>{{ book.price }}€</td>
|
||||
<td>
|
||||
<i class="fas fa-{% if book.previously_acquired %}check-circle{% else %}ban{% endif %}"></i>
|
||||
{{ book.previously_acquired_text }}
|
||||
</td>
|
||||
<td>
|
||||
<i class="fas fa-{% if book.consumable %}check-circle{% else %}ban{% endif %}"></i>
|
||||
{{ book.consumable_text }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>
|
||||
Liste des fournitures demandées
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<a href="{% url 'add_supplies' pk=teacher.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle"></i> Ajouter des fournitures
|
||||
</a>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="col">Modifier</th>
|
||||
{% endif %}
|
||||
<th scope="col">Classe</th>
|
||||
<th scope="col">Discipline</th>
|
||||
<th scope="col">Liste de fournitures</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for supply in teacher.suppliesrequirement_set.all %}
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="row">
|
||||
<div class="btn-group">
|
||||
<a title="Modifier"
|
||||
href="{% url 'edit_supplies' teacher_pk=supply.teacher.pk pk=supply.pk %}"
|
||||
class="btn btn-sm btn-secondary"><i class="fas fa-edit"></i></a>
|
||||
<a title="Supprimer"
|
||||
href="{% url 'delete_supplies' teacher_pk=supply.teacher.pk pk=supply.pk %}"
|
||||
class="btn btn-sm btn-danger"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</th>
|
||||
{% endif %}
|
||||
<td>{{ supply.level }}</td>
|
||||
<td>{{ supply.field }}</td>
|
||||
<td>{{ supply.supplies|linebreaksbr }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
962
poetry.lock
generated
|
@ -6,38 +6,34 @@ authors = ["Gabriel Augendre <gabriel@augendre.info>"]
|
|||
license = "MIT"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
python = "^3.10"
|
||||
Django = "^3.2.4"
|
||||
django-bootstrap4 = "^3.0.1"
|
||||
django-bootstrap4 = "^22.1"
|
||||
gunicorn = "^20.1.0"
|
||||
psycopg2-binary = "^2.9.1"
|
||||
django-anymail = "^8.4"
|
||||
whitenoise = "^5.2.0"
|
||||
whitenoise = "^6.2.0"
|
||||
django-import-export = "^2.5.0"
|
||||
beautifulsoup4 = "^4.9.3"
|
||||
requests = "^2.25.1"
|
||||
django-environ-2 = "^2.1.0"
|
||||
django-environ = "^0.9.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
django-debug-toolbar = "^3.2.1"
|
||||
pre-commit = "^2.13.0"
|
||||
pytest = "^6.2.4"
|
||||
pytest = "^7.1"
|
||||
pytest-django = "^4.4.0"
|
||||
pytest-html = "^3.1.1"
|
||||
model-bakery = "^1.3.2"
|
||||
selenium = "^3.141.0"
|
||||
vcrpy = "^4.1.1"
|
||||
invoke = "^1.7.1"
|
||||
poetry-deps-scanner = "^1.0.1"
|
||||
pytest-cov = "^3.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py38']
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--html=pytest_result/pytest.html --color=yes"
|
||||
minversion = "6.0"
|
||||
|
|
|
@ -93,7 +93,7 @@ class LevelAdmin(ExportMixin, admin.ModelAdmin):
|
|||
|
||||
def get_queryset(self, request):
|
||||
return (
|
||||
super(LevelAdmin, self)
|
||||
super()
|
||||
.get_queryset(request)
|
||||
.prefetch_related(Prefetch("book_set", to_attr="prefetched_books"))
|
||||
)
|
||||
|
@ -230,9 +230,7 @@ class BookAdmin(ExportMixin, admin.ModelAdmin):
|
|||
|
||||
def get_queryset(self, request):
|
||||
return (
|
||||
super(BookAdmin, self)
|
||||
.get_queryset(request)
|
||||
.select_related("editor", "level", "teacher")
|
||||
super().get_queryset(request).select_related("editor", "level", "teacher")
|
||||
)
|
||||
|
||||
def update_with_decitre(self, request, queryset):
|
||||
|
@ -298,8 +296,4 @@ class SuppliesRequirementAdmin(ExportMixin, admin.ModelAdmin):
|
|||
list_filter = ["done", "teacher", "level"]
|
||||
|
||||
def get_queryset(self, request):
|
||||
return (
|
||||
super(SuppliesRequirementAdmin, self)
|
||||
.get_queryset(request)
|
||||
.select_related("level", "teacher")
|
||||
)
|
||||
return super().get_queryset(request).select_related("level", "teacher")
|
|
@ -11,8 +11,6 @@ class EditBookForm(forms.ModelForm):
|
|||
"teacher",
|
||||
"level",
|
||||
"field",
|
||||
"no_book",
|
||||
"see_later",
|
||||
"title",
|
||||
"authors",
|
||||
"editor",
|
||||
|
@ -25,16 +23,6 @@ class EditBookForm(forms.ModelForm):
|
|||
"consumable",
|
||||
]
|
||||
|
||||
no_book = forms.BooleanField(
|
||||
label="Pas de livre pour cette classe/matière", required=False, initial=False
|
||||
)
|
||||
see_later = forms.BooleanField(
|
||||
label="Voir à la rentrée",
|
||||
help_text="Notamment en cas de désaccord sur l'adoption ou non d'un manuel",
|
||||
required=False,
|
||||
initial=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["title"].widget = forms.TextInput()
|
||||
|
@ -82,8 +70,6 @@ class AddBookForm(EditBookForm):
|
|||
"teacher",
|
||||
"levels",
|
||||
"field",
|
||||
"no_book",
|
||||
"see_later",
|
||||
"title",
|
||||
"authors",
|
||||
"editor",
|
34
src/manuels/migrations/0039_auto_20220615_2016.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 3.2.13 on 2022-06-15 18:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import manuels.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("manuels", "0038_auto_20210510_0854"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="book",
|
||||
name="isbn",
|
||||
field=models.CharField(
|
||||
help_text="Format attendu : 10 ou 13 chiffres, éventuellement séparés par des tirets et éventuellement suivis de la lettre <code>X</code>. La recherche sur Decitre ne fonctionnera qu'avec un code ISBN à 13 chiffres (ou EAN)",
|
||||
max_length=20,
|
||||
validators=[manuels.models.isbn_validator],
|
||||
verbose_name="ISBN/EAN du livre ou consommable demandé (hors specimen)",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="book",
|
||||
name="previously_acquired",
|
||||
field=models.BooleanField(
|
||||
choices=[(None, "------------"), (False, "Non"), (True, "Oui")],
|
||||
default=None,
|
||||
verbose_name="Livre ou consommable déjà acquis par l'élève les années précédentes",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -87,7 +87,7 @@ class Teacher(BaseModel):
|
|||
from_email=settings.SERVER_EMAIL,
|
||||
to=[dest],
|
||||
)
|
||||
reply_to = [os.getenv("REPLY_TO")]
|
||||
reply_to = settings.EMAIL_REPLY_TO
|
||||
if reply_to:
|
||||
msg.reply_to = reply_to
|
||||
msg.attach_alternative(
|
||||
|
@ -107,7 +107,7 @@ class Teacher(BaseModel):
|
|||
from_email=settings.SERVER_EMAIL,
|
||||
to=dest,
|
||||
)
|
||||
reply_to = [os.getenv("REPLY_TO")]
|
||||
reply_to = settings.EMAIL_REPLY_TO
|
||||
if reply_to:
|
||||
msg.reply_to = reply_to
|
||||
msg.attach_alternative(
|
||||
|
@ -240,7 +240,7 @@ class Book(BaseModel):
|
|||
other_editor = models.CharField(verbose_name="préciser", max_length=100, blank=True)
|
||||
publication_year = models.PositiveIntegerField("année de publication")
|
||||
isbn = models.CharField(
|
||||
"ISBN/EAN du manuel élève (hors specimen)",
|
||||
"ISBN/EAN du livre ou consommable demandé (hors specimen)",
|
||||
max_length=20,
|
||||
help_text="Format attendu : 10 ou 13 chiffres, éventuellement séparés par des tirets et éventuellement "
|
||||
"suivis de la lettre <code>X</code>. La recherche sur Decitre ne fonctionnera qu'avec un code ISBN à "
|
||||
|
@ -254,7 +254,7 @@ class Book(BaseModel):
|
|||
(True, "Oui"),
|
||||
)
|
||||
previously_acquired = models.BooleanField(
|
||||
"manuel acquis précédemment par l'élève",
|
||||
"Livre ou consommable déjà acquis par l'élève les années précédentes",
|
||||
choices=YES_NO_CHOICE,
|
||||
blank=False,
|
||||
default=None,
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 813 B After Width: | Height: | Size: 813 B |
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 409 B |
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 454 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
108
src/manuels/static/manuels/fetch-isbn.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
document.addEventListener("DOMContentLoaded", function () {
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
|
||||
var isbnButton = document.querySelector("#id_isbn_button");
|
||||
var isbn = document.querySelector("#id_isbn");
|
||||
var title = document.querySelector("#id_title");
|
||||
var authors = document.querySelector("#id_authors");
|
||||
var year = document.querySelector("#id_publication_year");
|
||||
var price = document.querySelector("#id_price");
|
||||
var editor = document.querySelector("#id_editor");
|
||||
var otherEditor = document.querySelector("#id_other_editor");
|
||||
var spinner = document.querySelector("#id_isbn_spinner");
|
||||
var feedback = document.querySelector("#id_isbn_invalid_feedback");
|
||||
|
||||
function enableFields() {
|
||||
isbn.removeAttribute("disabled");
|
||||
isbnButton.removeAttribute("disabled");
|
||||
document.querySelector("#id_title").removeAttribute("disabled");
|
||||
authors.removeAttribute("disabled");
|
||||
year.removeAttribute("disabled");
|
||||
price.removeAttribute("disabled");
|
||||
editor.removeAttribute("disabled");
|
||||
otherEditor.removeAttribute("disabled");
|
||||
spinner.setAttribute("hidden", "hidden");
|
||||
}
|
||||
|
||||
function disableFields() {
|
||||
isbn.setAttribute("disabled", "disabled");
|
||||
isbnButton.setAttribute("disabled", "disabled");
|
||||
title.setAttribute("disabled", "disabled");
|
||||
authors.setAttribute("disabled", "disabled");
|
||||
year.setAttribute("disabled", "disabled");
|
||||
price.setAttribute("disabled", "disabled");
|
||||
editor.setAttribute("disabled", "disabled");
|
||||
otherEditor.setAttribute("disabled", "disabled");
|
||||
spinner.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
isbnButton.addEventListener("click", function () {
|
||||
if (!isbn.value) {
|
||||
return;
|
||||
}
|
||||
disableFields();
|
||||
|
||||
fetch("/isbn_api/" + isbn.value)
|
||||
.then(function (data) {
|
||||
if (!data.ok) {
|
||||
throw Error("Erreur dans la récupération des données");
|
||||
}
|
||||
|
||||
return data.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
isbn.classList.remove("is-invalid");
|
||||
isbn.classList.add("is-valid");
|
||||
feedback.style.display = "none";
|
||||
feedback.textContent = "";
|
||||
|
||||
title.value = data.title;
|
||||
title.classList.add("is-valid");
|
||||
authors.value = data.authors;
|
||||
authors.classList.add("is-valid");
|
||||
year.value = data.year;
|
||||
year.classList.add("is-valid");
|
||||
price.value = data.price;
|
||||
price.classList.add("is-valid");
|
||||
|
||||
var editorValue = "";
|
||||
var editorIsOther = false;
|
||||
if (data.editor) {
|
||||
for (var option of document.querySelector("#id_editor").children) {
|
||||
if (editorValue === "" && option.firstChild.data.toLowerCase().indexOf("autre") !== -1) {
|
||||
editorValue = option.value;
|
||||
editorIsOther = true;
|
||||
}
|
||||
if (option.firstChild.data.toLowerCase() === data.editor.toLowerCase()) {
|
||||
editorValue = option.value;
|
||||
editorIsOther = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.value = editorValue;
|
||||
editor.classList.add("is-valid");
|
||||
|
||||
if (editorIsOther) {
|
||||
otherEditor.value = data.editor;
|
||||
otherEditor.classList.add("is-valid");
|
||||
}
|
||||
enableFields();
|
||||
|
||||
// The event propagation must be done after the fields have been re-enabled
|
||||
// because a change event can't be propagated to a field that's disabled.
|
||||
var event = document.createEvent("HTMLEvents");
|
||||
event.initEvent("change", true, true);
|
||||
event.eventName = "change";
|
||||
document.querySelector("#id_editor").dispatchEvent(event);
|
||||
})
|
||||
.catch(function (error) {
|
||||
isbn.classList.add("is-invalid");
|
||||
isbn.classList.remove("is-valid");
|
||||
feedback.style.display = "block";
|
||||
feedback.textContent = error;
|
||||
enableFields();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -19,7 +19,7 @@
|
|||
Vous avez peut-être déjà rencontré ce type d'erreur (peut-être même sur ce site) sans
|
||||
forcément comprendre ce qu'il signifie.
|
||||
Voici donc un extrait de <a href="https://fr.wikipedia.org/wiki/Erreur_HTTP_404">la page Wikipedia
|
||||
traitant spécifiquement de l'erreur 404</a> <i class="fas fa-book-reader"></i>
|
||||
traitant spécifiquement de l'erreur 404</a> <i class="fas fa-book-reader"></i>
|
||||
</p>
|
||||
|
||||
<blockquote class="blockquote text-right">
|
|
@ -17,37 +17,16 @@
|
|||
{% bootstrap_field form.field %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-lg-6">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="{{ form.no_book.auto_id }}">
|
||||
<label class="custom-control-label" for="{{ form.no_book.auto_id }}">{{ form.no_book.label }}</label>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.no_book.help_text|safe }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="{{ form.see_later.auto_id }}">
|
||||
<label class="custom-control-label"
|
||||
for="{{ form.see_later.auto_id }}">{{ form.see_later.label }}</label>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.see_later.help_text|safe }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
{% bootstrap_label content=form.isbn.label label_for=form.isbn.auto_id %}
|
||||
<div class="input-group">
|
||||
<input name="{{ form.isbn.name }}" maxlength="20"
|
||||
class="form-control"
|
||||
placeholder="{{ form.isbn.label }}"
|
||||
{% if form.isbn.value != None %} value="{{ form.isbn.value|stringformat:'s' }}"{% endif %}
|
||||
required="" id="{{ form.isbn.auto_id }}" type="text">
|
||||
class="form-control"
|
||||
placeholder="{{ form.isbn.label }}"
|
||||
{% if form.isbn.value != None %} value="{{ form.isbn.value|stringformat:'s' }}"{% endif %}
|
||||
required="" id="{{ form.isbn.auto_id }}" type="text">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="button" id="id_isbn_button" data-toggle="tooltip" data-placement="top" title="Chercher avec Decitre">
|
||||
<i class="fas fa-search"></i> <i class="fas fa-spinner fa-spin" id="id_isbn_spinner" hidden></i>
|
||||
|
@ -95,11 +74,11 @@
|
|||
<div class="col-12">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input"
|
||||
{% if form.add_another.initial %}checked="checked"{% endif %}
|
||||
name="{{ form.add_another.name }}"
|
||||
id="{{ form.add_another.auto_id }}">
|
||||
{% if form.add_another.initial %}checked="checked"{% endif %}
|
||||
name="{{ form.add_another.name }}"
|
||||
id="{{ form.add_another.auto_id }}">
|
||||
<label class="custom-control-label"
|
||||
for="{{ form.add_another.auto_id }}">{{ form.add_another.label }}</label>
|
||||
for="{{ form.add_another.auto_id }}">{{ form.add_another.label }}</label>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.add_another.help_text|safe }}
|
||||
</small>
|
||||
|
@ -110,6 +89,5 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block end_js %}
|
||||
<script defer src="{% static 'manuels/no_book.js' %}"></script>
|
||||
<script defer src="{% static 'manuels/fetch-isbn.js' %}"></script>
|
||||
{% endblock %}
|
|
@ -34,11 +34,11 @@
|
|||
<div class="col-12">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input"
|
||||
{% if form.add_another.initial %}checked="checked"{% endif %}
|
||||
name="{{ form.add_another.name }}"
|
||||
id="{{ form.add_another.auto_id }}">
|
||||
{% if form.add_another.initial %}checked="checked"{% endif %}
|
||||
name="{{ form.add_another.name }}"
|
||||
id="{{ form.add_another.auto_id }}">
|
||||
<label class="custom-control-label"
|
||||
for="{{ form.add_another.auto_id }}">{{ form.add_another.label }}</label>
|
||||
for="{{ form.add_another.auto_id }}">{{ form.add_another.label }}</label>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.add_another.help_text|safe }}
|
||||
</small>
|
88
src/manuels/templates/manuels/base.html
Normal file
|
@ -0,0 +1,88 @@
|
|||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Manuels & fournitures - {% block title %}{% endblock %}</title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180"
|
||||
href="{% static 'icons/apple-touch-icon.png' %}">
|
||||
<link rel="icon" type="image/png" sizes="32x32"
|
||||
href="{% static 'icons/favicon-32x32.png' %}">
|
||||
<link rel="icon" type="image/png" sizes="16x16"
|
||||
href="{% static 'icons/favicon-16x16.png' %}">
|
||||
<link rel="manifest" href="{% static 'icons/site.webmanifest' %}">
|
||||
<link rel="mask-icon" href="{% static 'icons/safari-pinned-tab.svg' %}"
|
||||
color="#000000">
|
||||
<link rel="shortcut icon" href="{% static 'icons/favicon.ico' %}">
|
||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
||||
<meta name="msapplication-config"
|
||||
content="{% static 'icons/browserconfig.xml' %}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l"
|
||||
crossorigin="anonymous">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="{% url 'home_page' %}">Fournitures, consommables ou livres supplémentaires</a>
|
||||
{% if user.is_authenticated or teacher and teacher.email in authorized_mails %}
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse"
|
||||
data-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}">
|
||||
<i class="fas fa-door-open"></i>
|
||||
Administration
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<main class="flex-fill">
|
||||
<div class="container-fluid">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} fade show" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-light py-3">
|
||||
<div class="container-fluid">
|
||||
<span class="text-muted">Ce service est un logiciel libre placé dans le domaine public réalisé par
|
||||
Gabriel Augendre d'après des besoins exprimés par Sandrine Augendre. Le code source est disponible
|
||||
<a href="https://git.augendre.info/gaugendre/manuels-scolaires">à cette adresse</a>.</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script defer src="https://kit.fontawesome.com/350c07ee78.js"
|
||||
integrity="sha384-IwFbZvLB3nqmwJikzn6JZAqNDTwjFfauT4djixzjaxmR030Fd2gx05kWWwBLwRYZ"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
|
||||
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
{% block end_js %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
7
src/manuels/templates/manuels/books_message.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div class="alert alert-info">
|
||||
<h4 class="alert-heading"><i class="fas fa-info-circle"></i> Information</h4>
|
||||
<p class="mb-0">
|
||||
Indiquer uniquement les livres autres que les manuels des matières générales
|
||||
(Français / Maths / Histoire / Géo / EMC / Physique-Chimie / Langues)
|
||||
</p>
|
||||
</div>
|
|
@ -18,7 +18,7 @@
|
|||
<h4 class="alert-heading"><i class="fas fa-exclamation-circle"></i> Danger</h4>
|
||||
<p class="mb-0">
|
||||
Êtes-vous <strong>certain·e</strong> de vouloir confirmer vos listes de manuels et de fournitures ?
|
||||
Cette action est <u><strong>définitive</strong></u> : vous ne ne serez plus en mesure de les modifier ensuite.
|
||||
Cette action est <u><strong>définitive</strong></u> : vous ne ne serez plus en mesure de les modifier ensuite.
|
||||
</p>
|
||||
</div>
|
||||
<form action="" method="post" class="form">
|
6
src/manuels/templates/manuels/emails_confirmation.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<html>
|
||||
<p>Bonjour,</p>
|
||||
<p>
|
||||
{{ teacher.full_name }} a confirmé ses listes sur <a href="{{ link }}">{{ link }}</a>
|
||||
</p>
|
||||
</html>
|
10
src/manuels/templates/manuels/emails_link.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<p>Bonjour {{ teacher.first_name }},</p>
|
||||
<p>
|
||||
Voici votre lien pour la gestion des manuels scolaires :
|
||||
</p>
|
||||
|
||||
<p style="text-align: center;">
|
||||
<a href="{{ link }}">{{ link }}</a>
|
||||
</p>
|
||||
</html>
|
|
@ -10,7 +10,7 @@
|
|||
<h1>Bienvenue !</h1>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">Bienvenue</h4>
|
||||
<p>Si c'est la première fois que vous visitez ce site, remplissez le formulaire suivant
|
||||
<p>Si c'est la première fois que vous visitez ce site cette année, remplissez le formulaire suivant
|
||||
afin de créer votre espace.</p>
|
||||
<hr>
|
||||
<p class="mb-0">
|
152
src/manuels/templates/manuels/list_books_supplies.html
Normal file
|
@ -0,0 +1,152 @@
|
|||
{% extends 'manuels/base.html' %}
|
||||
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block title %}Livres et fournitures demandés{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Bienvenue {{ teacher.full_name }}
|
||||
<a href="{% url 'clear_teacher' %}" class="btn btn-warning">
|
||||
<i class="fas fa-sign-out-alt"></i> Se déconnecter</a>
|
||||
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<a href="{% url 'confirm_teacher' pk=teacher.pk %}" class="btn btn-danger">
|
||||
<i class="fas fa-check-circle"></i> Confirmer ma liste</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> Attention</h4>
|
||||
<p class="mb-0">
|
||||
Le lien permettant d'accéder à cette page vous est personnel et vous a été envoyé par email.
|
||||
N'oubliez pas de vérifier vos courriers indésirables.<br>
|
||||
Pensez à <strong>sauvegarder cette page dans vos favoris</strong>.<br>
|
||||
Si vous perdez ce lien, vous ne risquez de ne plus être en mesure d'accéder à votre espace personnel
|
||||
et vous ne pourrez plus ajouter de livre à votre liste.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>
|
||||
Liste des fournitures demandées
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<a href="{% url 'add_supplies' pk=teacher.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle"></i> Ajouter des fournitures
|
||||
</a>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="col">Modifier</th>
|
||||
{% endif %}
|
||||
<th scope="col">Classe</th>
|
||||
<th scope="col">Discipline</th>
|
||||
<th scope="col">Liste de fournitures</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for supply in teacher.suppliesrequirement_set.all %}
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="row">
|
||||
<div class="btn-group">
|
||||
<a title="Modifier"
|
||||
href="{% url 'edit_supplies' teacher_pk=supply.teacher.pk pk=supply.pk %}"
|
||||
class="btn btn-sm btn-secondary"><i class="fas fa-edit"></i></a>
|
||||
<a title="Supprimer"
|
||||
href="{% url 'delete_supplies' teacher_pk=supply.teacher.pk pk=supply.pk %}"
|
||||
class="btn btn-sm btn-danger"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</th>
|
||||
{% endif %}
|
||||
<td>{{ supply.level }}</td>
|
||||
<td>{{ supply.field }}</td>
|
||||
<td>{{ supply.supplies|linebreaksbr }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>
|
||||
Liste des livres et consommables demandés
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<a href="{% url 'add_book' pk=teacher.pk %}"
|
||||
class="btn btn-primary"
|
||||
id="add-book">
|
||||
<i class="fas fa-plus-circle"></i> Ajouter un livre
|
||||
</a>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="alert alert-info" role="alert">
|
||||
Autres que les manuels des matières générales (Français / Maths / Histoire / Géo / EMC / Physique-Chimie / Langues)
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="col">Modifier</th>
|
||||
{% endif %}
|
||||
<th scope="col">Classe</th>
|
||||
<th scope="col">Discipline</th>
|
||||
<th scope="col">Titre</th>
|
||||
<th scope="col">Auteurs</th>
|
||||
<th scope="col">Éditeur</th>
|
||||
<th scope="col">Année de publication</th>
|
||||
<th scope="col">ISBN</th>
|
||||
<th scope="col">Prix</th>
|
||||
<th scope="col">Déjà acheté par l'élève</th>
|
||||
<th scope="col">Consommable</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for book in teacher.book_set.all %}
|
||||
<tr>
|
||||
{% if not teacher.has_confirmed_list %}
|
||||
<th scope="row">
|
||||
<div class="btn-group">
|
||||
<a title="Modifier"
|
||||
href="{% url 'edit_book' teacher_pk=book.teacher.pk pk=book.pk %}"
|
||||
class="btn btn-sm btn-secondary"><i class="fas fa-edit"></i></a>
|
||||
<a title="Supprimer"
|
||||
href="{% url 'delete_book' teacher_pk=book.teacher.pk pk=book.pk %}"
|
||||
class="btn btn-sm btn-danger"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</th>
|
||||
{% endif %}
|
||||
<td>{{ book.level }}</td>
|
||||
<td>{{ book.field }}</td>
|
||||
<td>{{ book.title }}</td>
|
||||
<td>{{ book.authors }}</td>
|
||||
<td>{{ book.editor }}{% if book.other_editor %}
|
||||
({{ book.other_editor }})
|
||||
{% endif %}</td>
|
||||
<td>{{ book.publication_year }}</td>
|
||||
<td>{{ book.isbn }}</td>
|
||||
<td>{{ book.price }}€</td>
|
||||
<td>
|
||||
<i class="fas fa-{% if book.previously_acquired %}check-circle{% else %}ban{% endif %}"></i>
|
||||
{{ book.previously_acquired_text }}
|
||||
</td>
|
||||
<td>
|
||||
<i class="fas fa-{% if book.consumable %}check-circle{% else %}ban{% endif %}"></i>
|
||||
{{ book.consumable_text }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
7
src/manuels/tests/conftest.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import pytest
|
||||
from django.core.management import call_command
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def _collect_static() -> None:
|
||||
call_command("collectstatic", "--no-input", "--clear")
|