mirror of
https://github.com/Crocmagnon/charasheet.git
synced 2024-11-22 06:28:03 +01:00
Initial commit
This commit is contained in:
commit
5e7d22ac20
45 changed files with 1243 additions and 0 deletions
2
.envrc
Normal file
2
.envrc
Normal 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
56
.eslintrc
Normal 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
36
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal 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
8
.idea/.gitignore
vendored
Normal 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
37
.idea/charasheet.iml
Normal 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="<map> <entry> <string>ENV_FILE</string> <string>$MODULE_DIR$/envs/local-envs.env</string> </entry> <entry> <string>DATABASE_URL</string> <string>sqlite:///$MODULE_DIR$/db/db.sqlite3</string> </entry> </map>" />
|
||||||
|
<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>
|
19
.idea/inspectionProfiles/Project_Default.xml
Normal file
19
.idea/inspectionProfiles/Project_Default.xml
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/vcs.xml
Normal 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
45
.idea/watcherTasks.xml
Normal 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
71
.pre-commit-config.yaml
Normal 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
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"printWidth": 120,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
136
Dockerfile
Normal file
136
Dockerfile
Normal 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
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# charasheet
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
```shell
|
||||||
|
pre-commit install --install-hooks
|
||||||
|
poetry install
|
||||||
|
inv test
|
||||||
|
```
|
0
contrib/.gitkeep
Normal file
0
contrib/.gitkeep
Normal file
0
db/.gitkeep
Normal file
0
db/.gitkeep
Normal file
8
docker-compose-build.yaml
Normal file
8
docker-compose-build.yaml
Normal 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
14
docker-compose.yaml
Normal 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
15
docker/uwsgi.ini
Normal 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
|
17
envs/docker-local-envs.env
Normal file
17
envs/docker-local-envs.env
Normal 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
12
envs/local-envs.env
Normal 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
12
envs/test-envs.env
Normal 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
105
pyproject.toml
Normal 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.
|
||||||
|
]
|
0
src/charasheet/__init__.py
Normal file
0
src/charasheet/__init__.py
Normal file
11
src/charasheet/middleware.py
Normal file
11
src/charasheet/middleware.py
Normal 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
193
src/charasheet/settings.py
Normal 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
33
src/charasheet/urls.py
Normal 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
16
src/charasheet/wsgi.py
Normal 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
0
src/common/__init__.py
Normal file
6
src/common/admin.py
Normal file
6
src/common/admin.py
Normal 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
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"
|
130
src/common/migrations/0001_initial.py
Normal file
130
src/common/migrations/0001_initial.py
Normal 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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
src/common/migrations/__init__.py
Normal file
0
src/common/migrations/__init__.py
Normal file
1
src/common/migrations/max_migration.txt
Normal file
1
src/common/migrations/max_migration.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0001_initial
|
7
src/common/models.py
Normal file
7
src/common/models.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
"""Default custom user model for My Awesome Project."""
|
||||||
|
|
||||||
|
pass
|
1
src/common/static/vendor/htmx-1.8.2.min.js
vendored
Normal file
1
src/common/static/vendor/htmx-1.8.2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
28
src/common/templates/common/base.html
Normal file
28
src/common/templates/common/base.html
Normal 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>
|
1
src/common/templates/common/hello-random.html
Normal file
1
src/common/templates/common/hello-random.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<p hx-get="{% url "hello_world" %}">Hello, world! Click me - {{ value }}</p>
|
0
src/common/tests/__init__.py
Normal file
0
src/common/tests/__init__.py
Normal file
39
src/common/tests/test_admin.py
Normal file
39
src/common/tests/test_admin.py
Normal 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
12
src/common/views.py
Normal 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
7
src/conftest.py
Normal 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
21
src/manage.py
Executable 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
102
tasks.py
Normal 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)
|
Loading…
Reference in a new issue