Initial commit

This commit is contained in:
Gabriel Augendre 2022-04-24 15:51:25 +02:00
commit 1288a30308
32 changed files with 2249 additions and 0 deletions

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
db/
media/
src/media/
staticfiles/
src/staticfiles/
__pycache__/
**/__pycache__/
.pytest_cache/
.idea/

51
.eslintrc Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
{
"tabWidth": 4,
"printWidth": 120,
"endOfLine": "auto"
}

57
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

67
pyproject.toml Normal file
View 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
View file

16
src/checkout/asgi.py Normal file
View 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
View 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"

View file

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

3
src/common/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
src/common/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CommonConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "common"

View 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()),
],
),
]

View file

5
src/common/models.py Normal file
View file

@ -0,0 +1,5 @@
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass

View file

@ -0,0 +1,2 @@
User-Agent: *
Disallow: /admin/

3
src/common/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
src/common/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

22
src/manage.py Executable file
View 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
View 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)