mirror of
https://github.com/Crocmagnon/checkout.git
synced 2024-11-21 07:38:03 +01:00
Initial commit
This commit is contained in:
commit
1288a30308
32 changed files with 2249 additions and 0 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
@ -0,0 +1,9 @@
|
|||
db/
|
||||
media/
|
||||
src/media/
|
||||
staticfiles/
|
||||
src/staticfiles/
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
.pytest_cache/
|
||||
.idea/
|
51
.eslintrc
Normal file
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"
|
||||
}
|
||||
}
|
278
.gitignore
vendored
Normal file
278
.gitignore
vendored
Normal file
|
@ -0,0 +1,278 @@
|
|||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/osx,pycharm,python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=osx,pycharm,python
|
||||
|
||||
### OSX ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### PyCharm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### PyCharm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytestdebug.log
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db/
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
doc/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.envrc
|
||||
*.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/osx,pycharm,python
|
||||
|
||||
.idea
|
||||
staticfiles/
|
||||
media/
|
||||
db.sqlite3
|
71
.pre-commit-config.yaml
Normal file
71
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,71 @@
|
|||
exclude: (\.min\.(js|css)(\.map)?$|/vendor/)
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-xml
|
||||
- id: check-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
|
||||
- --no-sort-keys
|
||||
- id: trailing-whitespace
|
||||
args:
|
||||
- --markdown-linebreak-ext=md
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.32.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py310-plus
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.5.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
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.6.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, css]
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.14.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
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
57
Dockerfile
Normal file
57
Dockerfile
Normal file
|
@ -0,0 +1,57 @@
|
|||
## Build venv
|
||||
FROM python:3.10.4-bullseye AS venv
|
||||
|
||||
# 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
|
||||
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
|
||||
RUN python -m venv --copies /app/venv \
|
||||
&& . /app/venv/bin/activate \
|
||||
&& poetry install $POETRY_OPTIONS
|
||||
|
||||
ENV PATH /app/venv/bin:$PATH
|
||||
COPY src ./src/
|
||||
RUN python ./src/manage.py collectstatic --no-input
|
||||
|
||||
## Get git versions
|
||||
FROM alpine/git AS git
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
RUN git rev-parse HEAD | tee /version
|
||||
|
||||
|
||||
## 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"]
|
24
LICENSE
Normal file
24
LICENSE
Normal file
|
@ -0,0 +1,24 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
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>
|
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Blog
|
||||
|
||||
Simple blog management system.
|
||||
|
||||
The authoritative source for this repo is at https://git.augendre.info/gaugendre/blog
|
||||
|
||||
Hosted at https://gabnotes.org
|
||||
|
||||
## Development
|
||||
```shell
|
||||
inv test-cov
|
||||
inv beam
|
||||
```
|
||||
|
||||
# Reuse
|
||||
If you do reuse my work, please consider linking back to this repository 🙂
|
12
docker-compose-build.yaml
Normal 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/cheese-factory:latest
|
||||
platform: linux/amd64
|
||||
|
||||
volumes:
|
||||
staticfiles: {}
|
||||
media: {}
|
23
docker-compose.yaml
Normal file
23
docker-compose.yaml
Normal file
|
@ -0,0 +1,23 @@
|
|||
version: '2.4'
|
||||
services:
|
||||
django:
|
||||
image: crocmagnon/cheese-factory:dev
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
POETRY_OPTIONS: "--no-dev"
|
||||
env_file:
|
||||
- envs/docker-local.env
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
- staticfiles:/app/staticfiles
|
||||
- media:/app/media
|
||||
restart: on-failure
|
||||
init: true
|
||||
tty: true
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
volumes:
|
||||
staticfiles: {}
|
||||
media: {}
|
4
docker/run.sh
Executable file
4
docker/run.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
set -eux
|
||||
python manage.py migrate --noinput
|
||||
gunicorn checkout.wsgi -b 0.0.0.0:8000 --log-file -
|
6
envs/local.env.dist
Normal file
6
envs/local.env.dist
Normal file
|
@ -0,0 +1,6 @@
|
|||
PYTHONUNBUFFERED=1
|
||||
DJANGO_SETTINGS_MODULE=checkout.settings
|
||||
ADMINS="Gabriel Augendre|gabriel@augendre.info"
|
||||
MAILGUN_API_KEY=API_KEY_HERE
|
||||
MAILGUN_SENDER_DOMAIN=mg.gabnotes.org
|
||||
HOSTS=192.168.0.1,192.168.0.2
|
1065
poetry.lock
generated
Normal file
1065
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
67
pyproject.toml
Normal file
67
pyproject.toml
Normal file
|
@ -0,0 +1,67 @@
|
|||
[tool.poetry]
|
||||
name = "checkout"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Gabriel Augendre <gabriel@augendre.info>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
django = "^4.0"
|
||||
django-anymail = {version = "^8.4", extras = ["mailgun"]}
|
||||
django-cleanup = "^5.0"
|
||||
whitenoise = {extras = ["brotli"], version = "^6.0"}
|
||||
django-csp = "^3.7"
|
||||
django-environ = "^0.8.1"
|
||||
requests = "^2.27.1"
|
||||
django-extensions = "^3.1.5"
|
||||
bpython = "^0.22.1"
|
||||
gunicorn = "^20.1.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pre-commit = "^2.7"
|
||||
pytest = "^6.0"
|
||||
pytest-django = "^4.5"
|
||||
model-bakery = "^1.1"
|
||||
pytest-cov = "^3.0"
|
||||
poetry-deps-scanner = "^1.0.1"
|
||||
invoke = "^1.7.0"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py310']
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--color=yes"
|
||||
minversion = "6.0"
|
||||
DJANGO_SETTINGS_MODULE = "checkout.settings"
|
||||
testpaths = [
|
||||
"src",
|
||||
]
|
||||
|
||||
[tool.flakeheaven]
|
||||
max_complexity = 10
|
||||
format = "grouped"
|
||||
|
||||
[tool.flakeheaven.plugins]
|
||||
"flake8-*" = [
|
||||
"+*",
|
||||
# long lines
|
||||
"-E501",
|
||||
# conflict with black on PEP8 interpretation
|
||||
"-E203",
|
||||
# deprecated rule: https://www.flake8rules.com/rules/W503.html
|
||||
"-W503",
|
||||
]
|
||||
flake8-quotes = ["+*", "-Q000"] # found double quotes, conflict with black
|
||||
flake8-commas = ["+*", "-C812"] # missing trailing comma, conflict with black
|
||||
flake8-docstrings = ["+*", "-D1??"] # missing docstring
|
||||
flake8-rst-docstrings = ["-*"]
|
||||
|
||||
[tool.flakeheaven.exceptions."**/tests/*"]
|
||||
flake8-bandit = ["+*", "-S101"] # Use of assert detected.
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
0
src/checkout/__init__.py
Normal file
0
src/checkout/__init__.py
Normal file
16
src/checkout/asgi.py
Normal file
16
src/checkout/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for checkout project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "checkout.settings")
|
||||
|
||||
application = get_asgi_application()
|
205
src/checkout/settings.py
Normal file
205
src/checkout/settings.py
Normal file
|
@ -0,0 +1,205 @@
|
|||
"""
|
||||
Django settings for checkout project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.1.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import environ
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||||
|
||||
env = environ.Env(
|
||||
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, ""),
|
||||
HOSTS=(list, []),
|
||||
DB_BASE_DIR=(Path, BASE_DIR),
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
admins = env("ADMINS")
|
||||
if admins:
|
||||
ADMINS = list(map(lambda x: tuple(x.split("|")), admins))
|
||||
|
||||
DEFAULT_FROM_EMAIL = "Gab's Notes <checkout@mg.gabnotes.org>"
|
||||
SERVER_EMAIL = "Gab's Notes <checkout@mg.gabnotes.org>"
|
||||
EMAIL_SUBJECT_PREFIX = "[Cheese checkout] "
|
||||
EMAIL_TIMEOUT = 30
|
||||
|
||||
ANYMAIL = {
|
||||
"MAILGUN_API_KEY": env("MAILGUN_API_KEY"),
|
||||
"MAILGUN_SENDER_DOMAIN": env("MAILGUN_SENDER_DOMAIN"),
|
||||
"MAILGUN_API_URL": "https://api.eu.mailgun.net/v3",
|
||||
}
|
||||
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
|
||||
|
||||
|
||||
# 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",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"anymail",
|
||||
"django_cleanup.apps.CleanupConfig",
|
||||
"common",
|
||||
]
|
||||
|
||||
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",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"csp.middleware.CSPMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "checkout.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": ["checkout/templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "checkout.wsgi.application"
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||
"LOCATION": "cache",
|
||||
}
|
||||
}
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||
|
||||
DB_BASE_DIR = env("DB_BASE_DIR")
|
||||
if not DB_BASE_DIR:
|
||||
# Protect against empty strings
|
||||
DB_BASE_DIR = BASE_DIR
|
||||
else:
|
||||
DB_BASE_DIR = DB_BASE_DIR.resolve(strict=True)
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": DB_BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
INTERNAL_IPS = [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
]
|
||||
|
||||
# Password validation
|
||||
# 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"},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "Europe/Paris"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = BASE_DIR.parent / "media"
|
||||
|
||||
AUTH_USER_MODEL = "common.User"
|
||||
|
||||
LOGIN_URL = "admin:login"
|
||||
|
||||
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_HSTS_SECONDS = 63072000
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# CSP
|
||||
CSP_DEFAULT_SRC = ("'none'",)
|
||||
CSP_IMG_SRC = ("'self'",)
|
||||
CSP_SCRIPT_SRC = ("'self'",)
|
||||
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"
|
0
src/checkout/tests/__init__.py
Normal file
0
src/checkout/tests/__init__.py
Normal file
9
src/checkout/tests/test_robots.py
Normal file
9
src/checkout/tests/test_robots.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.test.client import Client
|
||||
|
||||
|
||||
def test_robots_txt(client: Client) -> None:
|
||||
res = client.get("/robots.txt")
|
||||
assert res.status_code == 200
|
||||
assert res["Content-Type"] == "text/plain"
|
||||
content = res.content.decode("utf-8")
|
||||
assert "User-Agent" in content
|
34
src/checkout/urls.py
Normal file
34
src/checkout/urls.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
"""checkout URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('checkout/', include('checkout.urls'))
|
||||
"""
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from checkout import settings
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"robots.txt",
|
||||
TemplateView.as_view(
|
||||
template_name="common/robots.txt", content_type="text/plain"
|
||||
),
|
||||
),
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
16
src/checkout/wsgi.py
Normal file
16
src/checkout/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for checkout project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "checkout.settings")
|
||||
|
||||
application = get_wsgi_application()
|
0
src/common/__init__.py
Normal file
0
src/common/__init__.py
Normal file
3
src/common/admin.py
Normal file
3
src/common/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
src/common/apps.py
Normal file
6
src/common/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "common"
|
132
src/common/migrations/0001_initial.py
Normal file
132
src/common/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
# Generated by Django 4.0.4 on 2022-04-24 13:35
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
0
src/common/migrations/__init__.py
Normal file
0
src/common/migrations/__init__.py
Normal file
5
src/common/models.py
Normal file
5
src/common/models.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
pass
|
2
src/common/templates/common/robots.txt
Normal file
2
src/common/templates/common/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-Agent: *
|
||||
Disallow: /admin/
|
3
src/common/tests.py
Normal file
3
src/common/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
3
src/common/views.py
Normal file
3
src/common/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
22
src/manage.py
Executable file
22
src/manage.py
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run administrative tasks.""" # noqa: DAR401
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "checkout.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
105
tasks.py
Normal file
105
tasks.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
"""
|
||||
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
|
||||
def mypy(ctx: Context) -> None:
|
||||
with ctx.cd(BASE_DIR):
|
||||
ctx.run("pre-commit run --all-files mypy", 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://gabnotes.org")
|
||||
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
|
||||
|
||||
|
||||
@task
|
||||
def download_db(ctx: Context) -> None:
|
||||
with ctx.cd(BASE_DIR):
|
||||
ctx.run("scp ubuntu:/home/gaugendre/checkout/db/db.sqlite3 ./db/db.sqlite3")
|
||||
ctx.run("rm -rf src/media/")
|
||||
ctx.run("scp -r ubuntu:/home/gaugendre/checkout/media/ ./src/media")
|
||||
with ctx.cd(SRC_DIR):
|
||||
ctx.run("./manage.py changepassword gaugendre", pty=True)
|
Loading…
Reference in a new issue