Initial commit

This commit is contained in:
Gabriel Augendre 2022-10-28 22:16:23 +02:00
commit 5e7d22ac20
45 changed files with 1243 additions and 0 deletions

2
.envrc Normal file
View file

@ -0,0 +1,2 @@
export ENV_FILE="$(realpath ./envs/local-envs.env)"
export DATABASE_URL="sqlite:///$(realpath ./db/db.sqlite3)"

56
.eslintrc Normal file
View file

@ -0,0 +1,56 @@
{
"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-implicit-globals": "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": "script"
},
"globals": {
"bootstrap": false,
"moment": false
}
}

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
media
public/*
### Celery ###
celerybeat.pid
celerybeat-schedule
### documentation ###
documentation/*
### Coverage ###
coverage.*
htmlcov/
.coverage
.python-version
dashboard_templates/rendered/
dashboard_templates/downloaded/
pytest_result
.DS_Store
# files open in Excel
~$*.xlsx
src/public/static/
import_files/
test_reports/
dashboard_templates/backup_*.zip

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "grafana/grafonnet-lib"]
path = grafana/grafonnet-lib
url = https://github.com/grafana/grafonnet-lib.git

8
.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

37
.idea/charasheet.iml Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/src" />
<option name="settingsModule" value="charasheet/settings.py" />
<option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map&gt;&#10; &lt;entry&gt;&#10; &lt;string&gt;ENV_FILE&lt;/string&gt;&#10; &lt;string&gt;$MODULE_DIR$/envs/local-envs.env&lt;/string&gt;&#10; &lt;/entry&gt;&#10; &lt;entry&gt;&#10; &lt;string&gt;DATABASE_URL&lt;/string&gt;&#10; &lt;string&gt;sqlite:///$MODULE_DIR$/db/db.sqlite3&lt;/string&gt;&#10; &lt;/entry&gt;&#10;&lt;/map&gt;" />
<option name="doNotUseTestRunner" value="true" />
<option name="trackFilePattern" value="" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/src/common/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

View file

@ -0,0 +1,19 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="fastapi" />
<item index="1" class="java.lang.String" itemvalue="requests" />
<item index="2" class="java.lang.String" itemvalue="hypothesis" />
<item index="3" class="java.lang.String" itemvalue="pre-commit" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/charasheet.iml" filepath="$PROJECT_DIR$/.idea/charasheet.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

45
.idea/watcherTasks.xml Normal file
View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="run --file $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="NEVER" />
<option name="fileExtension" value="*" />
<option name="immediateSync" value="false" />
<option name="name" value="pre-commit" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$USER_HOME$/.local/pipx/venvs/pre-commit/bin/pre-commit" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="true">
<option name="arguments" value="run flakeheaven --file $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="py" />
<option name="immediateSync" value="false" />
<option name="name" value="flake8" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$USER_HOME$/.local/pipx/venvs/pre-commit/bin/pre-commit" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="" />
<envs />
</TaskOptions>
</component>
</project>

71
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,71 @@
exclude: \.min\.(js|css)(\.map)?$|^\.idea/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-ast
- id: check-json
- id: check-toml
- id: check-xml
- id: check-yaml
- id: end-of-file-fixer
- id: check-merge-conflict
- id: pretty-format-json
args:
- --autofix
- --no-sort-keys
- id: trailing-whitespace
args:
- --markdown-linebreak-ext=md
- repo: https://github.com/asottile/pyupgrade
rev: v3.1.0
hooks:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.11.0
hooks:
- id: django-upgrade
args: [--target-version, "4.0"]
- repo: https://github.com/timothycrosley/isort
rev: 5.10.1
hooks:
- id: isort
args: [--profile, black]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
args: [--target-version, py310]
- repo: https://github.com/rtts/djhtml
rev: v1.5.2
hooks:
- id: djhtml
- repo: https://github.com/flakeheaven/flakeheaven
rev: 3.2.0
hooks:
- id: flakeheaven
additional_dependencies:
- flake8-annotations-complexity
- flake8-bandit
- flake8-builtins
- flake8-bugbear
- flake8-comprehensions
- flake8-docstrings
- flake8-eradicate
- flake8-noqa
- pep8-naming
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0-alpha.4
hooks:
- id: prettier
types_or: [javascript, css]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.26.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"
}

136
Dockerfile Normal file
View file

@ -0,0 +1,136 @@
##############################################
# Build virtualenv
##############################################
FROM python:3.10.7-bullseye AS venv
# Prepare poetry
##############################################
# https://python-poetry.org/docs/#installation
ENV POETRY_VERSION=1.1.15
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH /root/.local/bin:$PATH
RUN python -m pip install --user poetry-lock-check==0.1.0 \
cleo==0.8.1 # poetry-lock-check depends on cleo
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN python -m poetry_lock_check check-lock
# Install python dependencies
##############################################
RUN python -m venv --copies /app/venv
# Will install dev deps as well, so that we can run tests in this image
RUN . /app/venv/bin/activate \
&& poetry install --no-interaction
ENV PATH /app/venv/bin:$PATH
# Collect static files & build assets
##############################################
COPY ./src /app/src
COPY ./envs/local-envs.env /app/.env
WORKDIR /app/src
# Required for manage.py to startup
ARG ENV_FILE=/app/.env
ARG DEBUG=true
ENV STATIC_ROOT=/app/static
RUN mkdir -p $STATIC_ROOT
# Build assets so that we don't need the build tools later
RUN python manage.py collectstatic --noinput --clear
##############################################
# write git info
##############################################
FROM alpine/git:v2.26.2 AS git
WORKDIR /app
COPY .git /app/.git/
RUN git describe --tags --always > /git-describe
RUN git rev-parse HEAD > /git-commit
RUN date +'%Y-%m-%d %H:%M %Z' > /build-date
##############################################
# Main image
##############################################
FROM python:3.10.7-slim-bullseye AS final
ARG DEBIAN_FRONTEND=noninteractive
# Setup user & group
##############################################
RUN groupadd -g 1000 django
RUN useradd -M -d /app -u 1000 -g 1000 -s /bin/bash django
# Setup system
##############################################
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends \
libxml2 \
media-types \
postgresql-client
# Fetch project requirements
##############################################
COPY --chown=django:django --from=venv /app/venv /app/venv/
COPY --chown=django:django --from=git /git-describe /git-commit /build-date /app/git/
ENV PATH /app/venv/bin:$PATH
# Fetch built assets & static files
##############################################
ENV STATIC_ROOT=/app/static
COPY --chown=django:django --from=venv $STATIC_ROOT $STATIC_ROOT
# uWSGI env vars
##############################################
ENV UWSGI_HTTP=:8000
ENV UWSGI_CHDIR="/app/src"
ENV UWSGI_WSGI_FILE="/app/src/charasheet/wsgi.py"
ENV UWSGI_MASTER=1
ENV UWSGI_HTTP_AUTO_CHUNKED=1
ENV UWSGI_HTTP_KEEPALIVE=1
ENV UWSGI_UID=1000
ENV UWSGI_GID=1000
ENV UWSGI_WSGI_ENV_BEHAVIOR=holy
ENV UWSGI_DIE_ON_TERM=true
ENV UWSGI_STRICT=true
ENV UWSGI_NEED_APP=true
# Tweak for perf
ENV UWSGI_SINGLE_INTERPRETER=true
ENV UWSGI_AUTO_PROCNAME=true
ENV UWSGI_MAX_REQUESTS=5000
ENV UWSGI_MAX_WORKER_LIFETIME=3600
ENV UWSGI_RELOAD_ON_RSS=500
ENV UWSGI_WORKER_RELOAD_MERCY=10
ENV UWSGI_WORKERS=2 UWSGI_THREADS=4
# Create directory structure
##############################################
WORKDIR /app
COPY pyproject.toml poetry.lock ./
ADD --chown=django:django ./src ./src
COPY --chown=django:django tasks.py ./tasks.py
COPY --chown=django:django docker/uwsgi.ini ./uwsgi.ini
RUN mkdir -p /app/data
RUN chown django:django /app /app/data
EXPOSE 8000
WORKDIR /app/src
USER django
CMD ["uwsgi", "--show-config", "--ini", "/app/uwsgi.ini"]

8
README.md Normal file
View file

@ -0,0 +1,8 @@
# charasheet
## Quick start
```shell
pre-commit install --install-hooks
poetry install
inv test
```

0
contrib/.gitkeep Normal file
View file

0
db/.gitkeep Normal file
View file

View file

@ -0,0 +1,8 @@
version: "2.4"
services:
django:
extends:
file: docker-compose.yaml
service: django
platform: linux/amd64

14
docker-compose.yaml Normal file
View file

@ -0,0 +1,14 @@
version: "2.4"
services:
django:
build: .
image: crocmagnon/charasheet
command: /app/src/manage.py runserver 0.0.0.0:8000
env_file:
- envs/docker-local-envs.env
volumes:
- src:/app/src
- db:/app/db
ports:
- "8000:8000"

15
docker/uwsgi.ini Normal file
View file

@ -0,0 +1,15 @@
[uwsgi]
plugin = /app/escape_json_plugin.so
static-map = /media=/app/data/media/
logger-req = stdio
; json_uri and json_host are json-escaped fields defined in `escape_json_plugin.so`
log-format = "address":"%(addr)", "host":"%(json_host)", "method":"%(method)", "uri":"%(json_uri)", "protocol":"%(proto)", "resp_size":%(size), "req_body_size":%(cl), "resp_status":%(status), "resp_time":%(msecs), "referer":"%(referer)", "user_agent":"%(uagent)"
log-req-encoder = format {"source":"uwsgi-req", "time":"${strftime:%%FT%%T%%z}", ${msg}}
log-req-encoder = nl
; Ignore write errors
; https://github.com/getsentry/raven-python/issues/732#issuecomment-176854438
ignore-sigpipe = true
ignore-write-errors = true
disable-write-exception = true

View file

@ -0,0 +1,17 @@
###############################################################################
# DJANGO
###############################################################################
DJANGO_SETTINGS_MODULE=charasheet.settings
SECRET_KEY="UkZF3iM%Fqdj6HWugPWS26q!tmquRm#8G^X#&AiXiT$r2t%N4F"
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
###############################################################################
# LOGGING
###############################################################################
LOG_LEVEL=DEBUG
###############################################################################
# SQLITE DB
###############################################################################
DATABASE_URL=sqlite:////app/db/db.sqlite3

12
envs/local-envs.env Normal file
View file

@ -0,0 +1,12 @@
###############################################################################
# DJANGO
###############################################################################
DJANGO_SETTINGS_MODULE=charasheet.settings
SECRET_KEY="UkZF3iM%Fqdj6HWugPWS26q!tmquRm#8G^X#&AiXiT$r2t%N4F"
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
###############################################################################
# LOGGING
###############################################################################
LOG_LEVEL=DEBUG

12
envs/test-envs.env Normal file
View file

@ -0,0 +1,12 @@
###############################################################################
# DJANGO
###############################################################################
DJANGO_SETTINGS_MODULE=charasheet.settings
SECRET_KEY="UkZF3iM%Fqdj6HWugPWS26q!tmquRm#8G^X#&AiXiT$r2t%N4F"
DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1
###############################################################################
# LOGGING
###############################################################################
LOG_LEVEL=DEBUG

105
pyproject.toml Normal file
View file

@ -0,0 +1,105 @@
###############################################################################
# poetry
###############################################################################
[tool.poetry]
name = "charasheet"
version = "0.1.0"
description = ""
authors = ["Gabriel Augendre <gabriel@augendre.info>"]
[tool.poetry.dependencies]
python = ">=3.10.0, <4"
django = "^4.0"
django-cleanup = ">=6.0"
django-environ = ">=0.9.0"
django-htmx = ">=1.12.2"
django-linear-migrations = ">=2.2.0"
django-extensions = ">=3.1.5"
psycopg2-binary = ">=2.8"
whitenoise = ">=6.2"
uWSGI = ">=2.0.21"
[tool.poetry.dev-dependencies]
django-debug-toolbar = ">=3.2"
pytest = ">=6.0"
pytest-cov = ">=3.0.0"
pytest-django = ">=4.1.0"
pytest-html = ">=3.1.1"
pre-commit = ">=2.1"
model-bakery = ">=1.3.1"
freezegun = ">=1.1.0"
bpython = ">=0.22.1"
poetry-deps-scanner = ">=2.0.0"
invoke = ">=1.7.3"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
###############################################################################
# pytest
###############################################################################
[tool.pytest.ini_options]
addopts = """
--html=test_reports/pytest_result/pytest.html --color=yes --durations 20
--no-cov-on-fail --strict-markers
-W error
"""
markers = []
minversion = "6.0"
DJANGO_SETTINGS_MODULE = "charasheet.settings"
junit_family = "xunit1"
norecursedirs = [
".*",
"docker",
"documentation",
"static",
"public",
]
testpaths = [
"src",
]
python_files = [
"test_*.py",
"tests.py",
]
###############################################################################
# flake8 / flakeheaven
###############################################################################
[tool.flakeheaven]
max_complexity = 10
format = "grouped"
# Base rules
#############################
[tool.flakeheaven.plugins]
"*" = [
"+*",
"-E501", # long lines
"-E203", # conflict with black on PEP8 interpretation
"-W503", # deprecated rule: https://www.flake8rules.com/rules/W503.html
]
flake8-builtins = [
"+*",
"-A003", # class attribute is shadowing a python builtin
]
flake8-docstrings = [
"+*",
"-D1??", # missing docstring
]
flake8-bandit = [
"+*",
"-S308", # Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed.
"-S703", # Potential XSS on mark_safe function.
]
# Exceptions
#############################
[tool.flakeheaven.exceptions."**/tests/*"]
flake8-bandit = [
"+*",
"-S101", # Use of assert detected.
"-S106", # Possible hardcoded password.
"-S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes.
]

View file

View file

@ -0,0 +1,11 @@
from django.conf import settings
def debug_toolbar_bypass_internal_ips(request) -> bool:
"""
Display debug toolbar according to the DEBUG_TOOLBAR setting only.
By default, DjDT is displayed according to an `INTERNAL_IPS` settings.
This is impossible to predict in a docker/k8s environment so we bypass this check.
"""
return settings.DEBUG_TOOLBAR

193
src/charasheet/settings.py Normal file
View file

@ -0,0 +1,193 @@
import os
from pathlib import Path
import environ
INTERNAL_IPS = [
"127.0.0.1",
]
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
BASE_DIR = PROJECT_ROOT / "src"
CONTRIB_DIR = PROJECT_ROOT / "contrib"
env = environ.Env(
DEBUG=(bool, False),
SECRET_KEY=str,
ALLOWED_HOSTS=(list, []),
DEBUG_TOOLBAR=(bool, True),
STATIC_ROOT=(Path, BASE_DIR / "public" / "static"),
LOG_LEVEL=(str, "DEBUG"),
LOG_FORMAT=(str, "default"),
APP_DATA=(Path, PROJECT_ROOT / "data"),
DATABASE_URL=str,
)
env_file = os.getenv("ENV_FILE", None)
if env_file:
environ.Env.read_env(env_file)
SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
DEBUG_TOOLBAR = env("DEBUG") and env("DEBUG_TOOLBAR")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")
# Application definition
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
EXTERNAL_APPS = [
"django_linear_migrations",
"django_extensions",
"django_htmx",
"django_cleanup.apps.CleanupConfig", # should be last: https://pypi.org/project/django-cleanup/
]
if DEBUG_TOOLBAR:
EXTERNAL_APPS.append("debug_toolbar")
CUSTOM_APPS = [
"whitenoise.runserver_nostatic", # should be first
"common",
]
INSTALLED_APPS = CUSTOM_APPS + DJANGO_APPS + EXTERNAL_APPS
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"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",
"django_htmx.middleware.HtmxMiddleware",
]
if DEBUG_TOOLBAR:
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
ROOT_URLCONF = "charasheet.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"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 = "charasheet.wsgi.application"
DATABASES = {"default": env.db()}
############################################################
# Cache configuration
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
}
}
SOLO_CACHE = "default"
SOLO_CACHE_TIMEOUT = 60 * 10 # 10 mins
# Password validation
# https://docs.djangoproject.com/en/2.2/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/2.2/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/2.2/howto/static-files/
APP_DATA = env("APP_DATA")
STATIC_URL = "/static/"
STATIC_ROOT = env("STATIC_ROOT")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Medias
MEDIA_URL = "/media/"
MEDIA_ROOT = APP_DATA / "media"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "[%(asctime)s - %(levelname)s - %(processName)s/%(module)s.%(funcName)s:%(lineno)d] %(message)s",
},
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "default",
},
},
"loggers": {
"django.db.backends": {
"handlers": ["console"],
"level": "INFO", # set to DEBUG for SQL log
"propagate": False,
},
},
}
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": "charasheet.middleware.debug_toolbar_bypass_internal_ips",
"RESULTS_CACHE_SIZE": 100,
}
# Authentication configuration.
AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",)
LOGOUT_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/admin/login"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "common.User"

33
src/charasheet/urls.py Normal file
View file

@ -0,0 +1,33 @@
"""charasheet URL Configuration.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/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('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import logout
from django.urls import include, path
from common.views import hello_world
urlpatterns = [
path("logout/", logout, {"next_page": settings.LOGOUT_REDIRECT_URL}, name="logout"),
path("admin/", admin.site.urls),
path("", hello_world, name="hello_world"),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG_TOOLBAR:
urlpatterns.insert(0, path("__debug__/", include("debug_toolbar.urls")))

16
src/charasheet/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for charasheet 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/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "charasheet.settings")
application = get_wsgi_application()

0
src/common/__init__.py Normal file
View file

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

@ -0,0 +1,6 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
admin.site.register(User, UserAdmin)

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,130 @@
# Generated by Django 3.2.12 on 2022-03-24 16:14
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

View file

@ -0,0 +1 @@
0001_initial

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

@ -0,0 +1,7 @@
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
"""Default custom user model for My Awesome Project."""
pass

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,28 @@
{% load static django_htmx %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Character Sheet</title>
</head>
<body>
{% include "common/hello-random.html" %}
<script src="{% static 'vendor/htmx-1.8.2.min.js' %}" defer></script>
{% django_htmx_script %}
{% if debug %}
<script type="javascript">
if (typeof window.htmx !== "undefined") {
htmx.on("htmx:afterSettle", function(detail) {
if (
typeof window.djdt !== "undefined"
&& detail.target instanceof HTMLBodyElement
) {
djdt.show_toolbar();
}
});
}
</script>
{% endif %}
</body>
</html>

View file

@ -0,0 +1 @@
<p hx-get="{% url "hello_world" %}">Hello, world! Click me - {{ value }}</p>

View file

View file

@ -0,0 +1,39 @@
import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse
pytestmark = pytest.mark.django_db
class TestUserAdmin:
def test_changelist(self, admin_client):
url = reverse("admin:common_user_changelist")
response = admin_client.get(url)
assert response.status_code == 200
def test_search(self, admin_client):
url = reverse("admin:common_user_changelist")
response = admin_client.get(url, data={"q": "test"})
assert response.status_code == 200
def test_add(self, admin_client):
url = reverse("admin:common_user_add")
response = admin_client.get(url)
assert response.status_code == 200
response = admin_client.post(
url,
data={
"username": "test",
"password1": "My_R@ndom-P@ssw0rd",
"password2": "My_R@ndom-P@ssw0rd",
},
)
assert response.status_code == 302
assert get_user_model().objects.filter(username="test").exists()
def test_view_user(self, admin_client):
user = get_user_model().objects.get(username="admin")
url = reverse("admin:common_user_change", kwargs={"object_id": user.pk})
response = admin_client.get(url)
assert response.status_code == 200

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

@ -0,0 +1,12 @@
import random
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render
def hello_world(request: WSGIRequest) -> HttpResponse:
context = {"value": random.randint(1, 1000)} # noqa: S311
if request.htmx:
return render(request, "common/hello-random.html", context)
return render(request, "common/base.html", context)

7
src/conftest.py Normal file
View file

@ -0,0 +1,7 @@
import pytest
from django.core.management import call_command
@pytest.fixture(scope="session", autouse=True)
def collectstatic():
call_command("collectstatic", "--clear", "--noinput", "--verbosity=0")

21
src/manage.py Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "charasheet.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()

102
tasks.py Normal file
View file

@ -0,0 +1,102 @@
import time
from pathlib import Path
import requests
from invoke import 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}
TEST_ENV = {"ENV_FILE": BASE_DIR / "envs" / "test-envs.env"}
@task
def makemessages(ctx):
with ctx.cd(SRC_DIR):
ctx.run("./manage.py makemessages -l en -l fr", pty=True, echo=True)
@task
def compilemessages(ctx):
with ctx.cd(SRC_DIR):
ctx.run("./manage.py compilemessages -l en -l fr", pty=True, echo=True)
@task
def test(ctx):
with ctx.cd(SRC_DIR):
ctx.run("pytest", pty=True, echo=True, env=TEST_ENV)
@task
def test_cov(ctx):
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):
with ctx.cd(BASE_DIR):
ctx.run("pre-commit run --all-files", pty=True)
@task(pre=[pre_commit, test_cov])
def check(ctx):
pass
@task
def build(ctx):
with ctx.cd(BASE_DIR):
ctx.run(
"docker-compose build django", pty=True, echo=True, env=COMPOSE_BUILD_ENV
)
@task
def publish(ctx):
with ctx.cd(BASE_DIR):
ctx.run(
"docker-compose push django", pty=True, echo=True, env=COMPOSE_BUILD_ENV
)
@task
def deploy(ctx):
ctx.run("ssh ubuntu /mnt/data/checkout/update", pty=True, echo=True)
@task
def check_alive(ctx):
exception = None
for _ in range(5):
try:
res = requests.get("https://charasheet.augendre.info")
res.raise_for_status()
print("Server is up & running")
return
except requests.exceptions.HTTPError as e:
time.sleep(2)
exception = e
raise RuntimeError("Failed to reach the server") from exception
@task(pre=[check, build, publish, deploy], post=[check_alive])
def beam(ctx):
pass
@task
def download_db(ctx):
with ctx.cd(BASE_DIR):
ctx.run("scp ubuntu:/mnt/data/charasheet/db/db.sqlite3 ./db/db.sqlite3")
ctx.run("rm -rf src/media/")
ctx.run("scp -r ubuntu:/mnt/data/charasheet/media/ ./src/media")
with ctx.cd(SRC_DIR):
ctx.run("./manage.py changepassword gaugendre", pty=True)