Rework project structure

This commit is contained in:
Gabriel Augendre 2022-06-15 19:45:35 +02:00
parent bf76cd227a
commit 4bd48ffedd
100 changed files with 423 additions and 188 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
envs/local.env
envs/docker-local.env
.envrc
pytest_result

View file

@ -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"]

View file

@ -4,21 +4,14 @@ 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
echo 'export ENV_FILE=$(realpath "./envs/local.env")' > .envrc
direnv allow
inv test
```
## 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 🙂

View file

@ -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
View 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
View 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
View 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 -

171
poetry.lock generated
View file

@ -141,31 +141,19 @@ postal = ["cryptography"]
[[package]]
name = "django-bootstrap4"
version = "3.0.1"
version = "22.1"
description = "Bootstrap 4 for Django"
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
beautifulsoup4 = ">=4.8.0"
Django = ">=2.2"
[[package]]
name = "django-debug-toolbar"
version = "3.4.0"
description = "A configurable set of panels that display various debug information about the current request/response."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
Django = ">=3.2"
sqlparse = ">=0.2.0"
[[package]]
name = "django-environ"
version = "0.8.1"
version = "0.9.0"
description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
category = "main"
optional = false
@ -176,22 +164,6 @@ develop = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "furo (>=2021.8.17b4
docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"]
[[package]]
name = "django-environ-2"
version = "2.3.0"
description = "Configure Django made easy."
category = "main"
optional = false
python-versions = ">=3.6, <4"
[package.dependencies]
django-environ = "*"
[package.extras]
develop = ["coverage[toml] (>=5.4)", "pytest (>=6.2.4)", "pylint (>=2.6.0,!=2.6.1)", "flake8 (>=3.8.4)", "flake8-import-order (>=0.18.1)", "flake8-blind-except (>=0.2.0)", "check-manifest (>=0.45)", "furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
testing = ["coverage[toml] (>=5.4)", "pytest (>=6.2.4)", "pylint (>=2.6.0,!=2.6.1)", "flake8 (>=3.8.4)", "flake8-import-order (>=0.18.1)", "flake8-blind-except (>=0.2.0)", "check-manifest (>=0.45)"]
[[package]]
name = "django-import-export"
version = "2.8.0"
@ -266,6 +238,14 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "invoke"
version = "1.7.1"
description = "Pythonic task execution"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "markuppy"
version = "1.14"
@ -358,6 +338,20 @@ python-versions = ">=3.6"
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "poetry-deps-scanner"
version = "1.0.1"
description = "Analyse poetry dependencies and comment on gitlab"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
python-gitlab = ">=2.6.0"
requests = ">=2.25.1"
semver = ">=3.0.0-dev.2"
toml = ">=0.10.2"
[[package]]
name = "pre-commit"
version = "2.19.0"
@ -403,11 +397,11 @@ diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pytest"
version = "6.2.5"
version = "7.1.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
@ -417,10 +411,10 @@ iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
toml = "*"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-django"
@ -451,14 +445,30 @@ pytest-metadata = "*"
[[package]]
name = "pytest-metadata"
version = "1.11.0"
version = "2.0.0"
description = "pytest plugin for test session metadata"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
python-versions = ">=3.7,<4.0"
[package.dependencies]
pytest = ">=2.9.0"
pytest = ">=7.1.1,<8.0.0"
[[package]]
name = "python-gitlab"
version = "3.5.0"
description = "Interact with GitLab API"
category = "dev"
optional = false
python-versions = ">=3.7.0"
[package.dependencies]
requests = ">=2.25.0"
requests-toolbelt = ">=0.9.1"
[package.extras]
autocompletion = ["argcomplete (>=1.10.0,<3)"]
yaml = ["PyYaml (>=5.2)"]
[[package]]
name = "pytz"
@ -494,6 +504,17 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "requests-toolbelt"
version = "0.9.1"
description = "A utility belt for advanced users of python-requests"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
name = "selenium"
version = "3.141.0"
@ -505,6 +526,14 @@ python-versions = "*"
[package.dependencies]
urllib3 = "*"
[[package]]
name = "semver"
version = "3.0.0.dev3"
description = "Python helper for Semantic Versioning (http://semver.org)"
category = "dev"
optional = false
python-versions = ">=3.6.*"
[[package]]
name = "six"
version = "1.16.0"
@ -563,6 +592,14 @@ category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "urllib3"
version = "1.26.9"
@ -610,11 +647,11 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)",
[[package]]
name = "whitenoise"
version = "5.3.0"
version = "6.2.0"
description = "Radically simplified static file serving for WSGI applications"
category = "main"
optional = false
python-versions = ">=3.5, <4"
python-versions = ">=3.7"
[package.extras]
brotli = ["brotli"]
@ -663,7 +700,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "888ada102147bc0242781801902680c0d9c0c4c9e6d6bc52bf69c257c62d7f79"
content-hash = "b4789baaa83d1e40c5f39ff581b03fb901cadf0207bb2b577439c41fffa2ae02"
[metadata.files]
asgiref = [
@ -719,20 +756,12 @@ django-anymail = [
{file = "django_anymail-8.6-py3-none-any.whl", hash = "sha256:49d83d7c16316ca86a624097496881d59b7d71b16bf1c5211cffa5b19ef98d0c"},
]
django-bootstrap4 = [
{file = "django-bootstrap4-3.0.1.tar.gz", hash = "sha256:c5c97fb473bb56e3a91b4f4be52b74a3fc384ec3baae50dd0807fa922a55ec2b"},
{file = "django_bootstrap4-3.0.1-py3-none-any.whl", hash = "sha256:aa8a9cb5ab27cfae52a27d377a0401af268d0e4b91a5f8e660546464582cc010"},
]
django-debug-toolbar = [
{file = "django-debug-toolbar-3.4.0.tar.gz", hash = "sha256:ae6bec2c1ce0e6900b0ab0443e1427eb233d8e6f57a84a0b2705eeecb8874e22"},
{file = "django_debug_toolbar-3.4.0-py3-none-any.whl", hash = "sha256:42c1c2e9dc05bb57b53d641e3a6d131fc031b92377b34ae32e604a1fe439bb83"},
{file = "django-bootstrap4-22.1.tar.gz", hash = "sha256:fc9984f7238fbcd330ec5111bf0435083caa7192b022eedd53bfa4128bee318f"},
{file = "django_bootstrap4-22.1-py3-none-any.whl", hash = "sha256:b6da4cb54682012ff8baa1a1e672ba30cbfae82fb3d74f4b341109074e8e239f"},
]
django-environ = [
{file = "django-environ-0.8.1.tar.gz", hash = "sha256:6f0bc902b43891656b20486938cba0861dc62892784a44919170719572a534cb"},
{file = "django_environ-0.8.1-py2.py3-none-any.whl", hash = "sha256:42593bee519a527602a467c7b682aee1a051c2597f98c45f4f4f44169ecdb6e5"},
]
django-environ-2 = [
{file = "django-environ-2-2.3.0.tar.gz", hash = "sha256:6795fe26eb01291fad80866d4bfb572bb2966db13a8fab13dee77cc353ea4011"},
{file = "django_environ_2-2.3.0-py2.py3-none-any.whl", hash = "sha256:9503cc8d6e8d9d8a87ceb178b66fb717f0d1323e2064595038eae6021ba359a1"},
{file = "django-environ-0.9.0.tar.gz", hash = "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21"},
{file = "django_environ-0.9.0-py2.py3-none-any.whl", hash = "sha256:f21a5ef8cc603da1870bbf9a09b7e5577ab5f6da451b843dbcc721a7bca6b3d9"},
]
django-import-export = [
{file = "django-import-export-2.8.0.tar.gz", hash = "sha256:33c37b2921ef84e2cd9aa0eb76d04a7c2b538c9d04cb1ed97ac32600876cab30"},
@ -762,6 +791,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
invoke = [
{file = "invoke-1.7.1-py3-none-any.whl", hash = "sha256:2dc975b4f92be0c0a174ad2d063010c8a1fdb5e9389d69871001118b4fcac4fb"},
{file = "invoke-1.7.1.tar.gz", hash = "sha256:7b6deaf585eee0a848205d0b8c0014b9bf6f287a8eb798818a642dff1df14b19"},
]
markuppy = [
{file = "MarkupPy-1.14.tar.gz", hash = "sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f"},
]
@ -854,6 +887,10 @@ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
poetry-deps-scanner = [
{file = "poetry-deps-scanner-1.0.1.tar.gz", hash = "sha256:a1662c4ddc27a4606f4133830994e27212ee146709efc39ef3c91e1ee3dacbc5"},
{file = "poetry_deps_scanner-1.0.1-py3-none-any.whl", hash = "sha256:932ee6ef53def05030e47abf8e1a877e4c02252e1f1e6d17214aeae1a6b1c1a9"},
]
pre-commit = [
{file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"},
{file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"},
@ -925,8 +962,8 @@ pyparsing = [
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
]
pytest-django = [
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
@ -937,8 +974,12 @@ pytest-html = [
{file = "pytest_html-3.1.1-py3-none-any.whl", hash = "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"},
]
pytest-metadata = [
{file = "pytest-metadata-1.11.0.tar.gz", hash = "sha256:71b506d49d34e539cc3cfdb7ce2c5f072bea5c953320002c95968e0238f8ecf1"},
{file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"},
{file = "pytest-metadata-2.0.0.tar.gz", hash = "sha256:08dcc2779f4393309dd6d341ea1ddc15265239b6c4d51671737e784406ec07dc"},
{file = "pytest_metadata-2.0.0-py3-none-any.whl", hash = "sha256:e25f1a77ed02baf1d83911604247a70d60d7dcb970aa12be38e1ed58d4d38e65"},
]
python-gitlab = [
{file = "python-gitlab-3.5.0.tar.gz", hash = "sha256:29ae7fb9b8c9aeb2e6e19bd2fd04867e93ecd7af719978ce68fac0cf116ab30d"},
{file = "python_gitlab-3.5.0-py3-none-any.whl", hash = "sha256:73b5aa6502efa557ee1a51227cceb0243fac5529627da34f08c5f265bf50417c"},
]
pytz = [
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
@ -983,10 +1024,18 @@ requests = [
{file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"},
{file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"},
]
requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
selenium = [
{file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"},
{file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"},
]
semver = [
{file = "semver-3.0.0.dev3-py3-none-any.whl", hash = "sha256:10651d0dacffdce7a2f0658e58894b138f52c2a080f7d69aa999810ebfc1f97d"},
{file = "semver-3.0.0.dev3.tar.gz", hash = "sha256:7175229bdcf96a6702b077e30226f041ce112057146ae81e84b51df08a8a75cf"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@ -1007,6 +1056,10 @@ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
urllib3 = [
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
@ -1020,8 +1073,8 @@ virtualenv = [
{file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
]
whitenoise = [
{file = "whitenoise-5.3.0-py2.py3-none-any.whl", hash = "sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c"},
{file = "whitenoise-5.3.0.tar.gz", hash = "sha256:d234b871b52271ae7ed6d9da47ffe857c76568f11dd30e28e18c5869dbd11e12"},
{file = "whitenoise-6.2.0-py3-none-any.whl", hash = "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2"},
{file = "whitenoise-6.2.0.tar.gz", hash = "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567"},
]
wrapt = [
{file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},

View file

@ -8,25 +8,26 @@ license = "MIT"
[tool.poetry.dependencies]
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"
[build-system]
requires = ["poetry-core>=1.0.0"]

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 813 B

View file

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 409 B

View file

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 454 B

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -65,7 +65,7 @@
<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
<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>

View 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")

View file

@ -48,10 +48,3 @@ urlpatterns = [
path("clear", clear_teacher_view, name="clear_teacher"),
path("isbn_api/<str:isbn>", isbn_api, name="isbn_api"),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns

View file

@ -1,63 +1,84 @@
"""
Django settings for manuels_collection project.
Django settings for checkout project.
Generated by 'django-admin startproject' using Django 2.0.1.
Generated by 'django-admin startproject' using Django 3.1.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
from pathlib import Path
import environ
from django.contrib.messages import constants as messages
# Build paths inside the project like this: BASE_DIR / 'subdir'.
from django.contrib import messages
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
env = environ.Env(
SECRET_KEY=str,
DJANGO_ENV=(str, "prod"),
CURRENT_IP=(str, "192.168.0.200"),
HOST=(list, None),
ADMIN_EMAIL=str,
SERVER_EMAIL=str,
DEBUG=(bool, False),
SECRET_KEY=(str, "s#!83!8e$3s89m)r$1ghsgxbndf8=#^qt(_*o%xbq0j2t8#db5"),
ADMINS=(list, []),
MAILGUN_API_KEY=(str, ""),
MAILGUN_SENDER_DOMAIN=(str, ""),
SERVER_EMAIL=(str, ""),
AUTHORIZED_EMAILS=(list, []),
LIBRARIAN_EMAILS=(list, []),
MAILGUN_ACCESS_KEY=(str, ""),
MAILGUN_SERVER_NAME=(str, ""),
HOSTS=(list, []),
TIME_ZONE=(str, "Europe/Paris"),
LANGUAGE_CODE=(str, "fr-fr"),
)
env_file = os.getenv("ENV_FILE", None)
if env_file:
environ.Env.read_env(env_file)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DJANGO_ENV") == "dev"
admins = env("ADMINS")
if admins:
ADMINS = list(map(lambda x: tuple(x.split("|")), admins))
ALLOWED_HOSTS = ["web", "127.0.0.1"]
if DEBUG:
ALLOWED_HOSTS.extend(["localhost", env("CURRENT_IP")])
ALLOWED_HOSTS.extend(env("HOST"))
ADMINS = [
("Gabriel", env("ADMIN_EMAIL")),
]
DEFAULT_FROM_EMAIL = env("SERVER_EMAIL")
SERVER_EMAIL = env("SERVER_EMAIL")
EMAIL_SUBJECT_PREFIX = "[Manuels] "
EMAIL_TIMEOUT = 30
ANYMAIL = {
"MAILGUN_API_KEY": env("MAILGUN_API_KEY"),
"MAILGUN_SENDER_DOMAIN": env("MAILGUN_SENDER_DOMAIN"),
"MAILGUN_API_URL": "https://api.mailgun.net/v3",
}
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
AUTHORIZED_EMAILS = env("AUTHORIZED_EMAILS")
LIBRARIAN_EMAILS = env("LIBRARIAN_EMAILS")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG")
ALLOWED_HOSTS = ["localhost"] # Required for healthcheck
if DEBUG:
ALLOWED_HOSTS.append("127.0.0.1")
ALLOWED_HOSTS.extend(env("HOSTS"))
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
# Application definition
INSTALLED_APPS = [
"whitenoise.runserver_nostatic",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@ -70,14 +91,10 @@ INSTALLED_APPS = [
"import_export",
]
if DEBUG:
INSTALLED_APPS += [
"debug_toolbar",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.middleware.gzip.GZipMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@ -86,11 +103,6 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
if DEBUG:
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
ROOT_URLCONF = "manuels_collection.urls"
TEMPLATES = [
@ -104,7 +116,6 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"manuels.context_processors.authorized_mails",
],
},
},
@ -112,46 +123,48 @@ TEMPLATES = [
WSGI_APPLICATION = "manuels_collection.wsgi.application"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "manuels_cache",
}
}
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
default_db_path = BASE_DIR / "db.sqlite3"
DATABASES = {
"default": env.db(default=f"sqlite:///{default_db_path}"),
}
INTERNAL_IPS = [
"127.0.0.1",
"localhost",
]
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
INTERNAL_IPS = [
"127.0.0.1",
]
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = "fr-fr"
LANGUAGE_CODE = env("LANGUAGE_CODE")
TIME_ZONE = "Europe/Paris"
TIME_ZONE = env("TIME_ZONE")
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Logging
@ -173,29 +186,38 @@ LOGGING = {
}
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR.parent / "media"
LOGIN_REDIRECT_URL = "rooms-list"
ANYMAIL = {
"MAILGUN_API_KEY": env("MAILGUN_ACCESS_KEY"),
"MAILGUN_SENDER_DOMAIN": env("MAILGUN_SERVER_NAME"),
}
LOGIN_URL = "admin:login"
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_SECONDS = 63072000
MESSAGE_TAGS = {
messages.ERROR: "danger",
}
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "manuels_cache",
}
}
# CSP
CSP_DEFAULT_SRC = ("'none'",)
CSP_IMG_SRC = ("'self'", "data:")
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
CSP_CONNECT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_MANIFEST_SRC = ("'self'",)
CSP_FONT_SRC = ("'self'",)
CSP_BASE_URI = ("'none'",)
CSP_FORM_ACTION = ("'self'",)
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
MESSAGE_TAGS = {messages.ERROR: "danger"}

89
tasks.py Normal file
View file

@ -0,0 +1,89 @@
"""
Invoke management tasks for the project.
The current implementation with type annotations is not compatible
with invoke 1.6.0 and requires manual patching.
See https://github.com/pyinvoke/invoke/pull/458/files
"""
import time
from pathlib import Path
import requests
from invoke import Context, task
BASE_DIR = Path(__file__).parent.resolve(strict=True)
SRC_DIR = BASE_DIR / "src"
COMPOSE_BUILD_FILE = BASE_DIR / "docker-compose-build.yaml"
COMPOSE_BUILD_ENV = {"COMPOSE_FILE": COMPOSE_BUILD_FILE}
@task
def test(ctx: Context) -> None:
with ctx.cd(SRC_DIR):
ctx.run("pytest", pty=True, echo=True)
@task
def test_cov(ctx: Context) -> None:
with ctx.cd(SRC_DIR):
ctx.run(
"pytest --cov=. --cov-branch --cov-report term-missing:skip-covered",
pty=True,
echo=True,
env={"COVERAGE_FILE": BASE_DIR / ".coverage"},
)
@task
def pre_commit(ctx: Context) -> None:
with ctx.cd(BASE_DIR):
ctx.run("pre-commit run --all-files", pty=True)
@task(pre=[pre_commit, test_cov])
def check(ctx: Context) -> None:
pass
@task
def build(ctx: Context) -> None:
with ctx.cd(BASE_DIR):
ctx.run(
"docker-compose build django", pty=True, echo=True, env=COMPOSE_BUILD_ENV
)
@task
def publish(ctx: Context) -> None:
with ctx.cd(BASE_DIR):
ctx.run(
"docker-compose push django", pty=True, echo=True, env=COMPOSE_BUILD_ENV
)
@task
def deploy(ctx: Context) -> None:
ctx.run("ssh ubuntu /home/gaugendre/checkout/update", pty=True, echo=True)
@task
def check_alive(ctx: Context) -> None:
exception = None
for _ in range(5):
try:
res = requests.get("https://manuels.augendre.info")
res.raise_for_status()
print("Server is up & running")
return
except requests.exceptions.HTTPError as e:
time.sleep(2)
exception = e
raise RuntimeError("Failed to reach the server") from exception
@task(pre=[check, build, publish, deploy], post=[check_alive])
def beam(ctx: Context) -> None:
pass