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