Rework project structure
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
envs/local.env
|
||||
envs/docker-local.env
|
||||
.envrc
|
||||
pytest_result
|
||||
|
||||
|
|
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"]
|
||||
|
|
21
README.md
|
@ -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 🙂
|
||||
|
|
|
@ -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 -
|
171
poetry.lock
generated
|
@ -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"},
|
||||
|
|
|
@ -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"]
|
||||
|
|
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 |
|
@ -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>
|
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")
|
|
@ -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
|
|
@ -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
|
@ -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
|
||||