commit 5e7d22ac2018ddcb5f45c3f87c1687a4c599db2b Author: Gabriel Augendre Date: Fri Oct 28 22:16:23 2022 +0200 Initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..434712c --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +export ENV_FILE="$(realpath ./envs/local-envs.env)" +export DATABASE_URL="sqlite:///$(realpath ./db/db.sqlite3)" diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..d2cd3d3 --- /dev/null +++ b/.eslintrc @@ -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 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b6612c --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..68b540a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "grafana/grafonnet-lib"] + path = grafana/grafonnet-lib + url = https://github.com/grafana/grafonnet-lib.git diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/charasheet.iml b/.idea/charasheet.iml new file mode 100644 index 0000000..03d83bd --- /dev/null +++ b/.idea/charasheet.iml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..6bb2a4a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..62e8c7e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..98b3257 --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..81487f3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d6245f5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "printWidth": 120, + "endOfLine": "auto" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c0f1630 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b262679 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# charasheet + +## Quick start +```shell +pre-commit install --install-hooks +poetry install +inv test +``` diff --git a/contrib/.gitkeep b/contrib/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml new file mode 100644 index 0000000..e91a57b --- /dev/null +++ b/docker-compose-build.yaml @@ -0,0 +1,8 @@ +version: "2.4" + +services: + django: + extends: + file: docker-compose.yaml + service: django + platform: linux/amd64 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..6e5f744 --- /dev/null +++ b/docker-compose.yaml @@ -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" diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini new file mode 100644 index 0000000..a841ae5 --- /dev/null +++ b/docker/uwsgi.ini @@ -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 diff --git a/envs/docker-local-envs.env b/envs/docker-local-envs.env new file mode 100644 index 0000000..0fab4ff --- /dev/null +++ b/envs/docker-local-envs.env @@ -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 diff --git a/envs/local-envs.env b/envs/local-envs.env new file mode 100644 index 0000000..ad36665 --- /dev/null +++ b/envs/local-envs.env @@ -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 diff --git a/envs/test-envs.env b/envs/test-envs.env new file mode 100644 index 0000000..ea55ff9 --- /dev/null +++ b/envs/test-envs.env @@ -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3fce841 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,105 @@ +############################################################################### +# poetry +############################################################################### +[tool.poetry] +name = "charasheet" +version = "0.1.0" +description = "" +authors = ["Gabriel Augendre "] + +[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. +] diff --git a/src/charasheet/__init__.py b/src/charasheet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/charasheet/middleware.py b/src/charasheet/middleware.py new file mode 100644 index 0000000..873ca33 --- /dev/null +++ b/src/charasheet/middleware.py @@ -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 diff --git a/src/charasheet/settings.py b/src/charasheet/settings.py new file mode 100644 index 0000000..462a4be --- /dev/null +++ b/src/charasheet/settings.py @@ -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" diff --git a/src/charasheet/urls.py b/src/charasheet/urls.py new file mode 100644 index 0000000..153339a --- /dev/null +++ b/src/charasheet/urls.py @@ -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"))) diff --git a/src/charasheet/wsgi.py b/src/charasheet/wsgi.py new file mode 100644 index 0000000..1e5fe4f --- /dev/null +++ b/src/charasheet/wsgi.py @@ -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() diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/admin.py b/src/common/admin.py new file mode 100644 index 0000000..6d53b53 --- /dev/null +++ b/src/common/admin.py @@ -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) diff --git a/src/common/apps.py b/src/common/apps.py new file mode 100644 index 0000000..3e1544e --- /dev/null +++ b/src/common/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "common" diff --git a/src/common/migrations/0001_initial.py b/src/common/migrations/0001_initial.py new file mode 100644 index 0000000..ec88ab9 --- /dev/null +++ b/src/common/migrations/0001_initial.py @@ -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()), + ], + ), + ] diff --git a/src/common/migrations/__init__.py b/src/common/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/migrations/max_migration.txt b/src/common/migrations/max_migration.txt new file mode 100644 index 0000000..cbab66d --- /dev/null +++ b/src/common/migrations/max_migration.txt @@ -0,0 +1 @@ +0001_initial diff --git a/src/common/models.py b/src/common/models.py new file mode 100644 index 0000000..dab8651 --- /dev/null +++ b/src/common/models.py @@ -0,0 +1,7 @@ +from django.contrib.auth.models import AbstractUser + + +class User(AbstractUser): + """Default custom user model for My Awesome Project.""" + + pass diff --git a/src/common/static/vendor/htmx-1.8.2.min.js b/src/common/static/vendor/htmx-1.8.2.min.js new file mode 100644 index 0000000..0310354 --- /dev/null +++ b/src/common/static/vendor/htmx-1.8.2.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var U={onLoad:t,process:mt,on:X,off:F,trigger:$,ajax:or,find:R,findAll:O,closest:N,values:function(e,t){var r=jt(e,t||"post");return r.values},remove:q,addClass:L,removeClass:T,toggleClass:H,takeClass:A,defineExtension:dr,removeExtension:vr,logAll:C,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){return new WebSocket(e,[])},version:"1.8.2"};var r={addTriggerHandler:ft,bodyContains:K,canAccessLocalStorage:E,filterValues:zt,hasAttribute:o,getAttributeValue:V,getClosestMatch:h,getExpressionVars:rr,getHeaders:_t,getInputValues:jt,getInternalData:W,getSwapSpecification:Gt,getTriggerSpecs:Xe,getTarget:re,makeFragment:g,mergeObjects:Y,makeSettleInfo:Zt,oobSwap:ae,selectAndSwap:Oe,settleImmediately:At,shouldCancel:Ve,triggerEvent:$,triggerErrorEvent:J,withExtensions:wt};var n=["get","post","put","delete","patch"];var i=n.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function f(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function V(e,t){return f(e,t)||f(e,"data-"+t)}function u(e){return e.parentElement}function _(){return document}function h(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function a(e,t,r){var n=V(t,r);var i=V(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function z(t,r){var n=null;h(t,function(e){return n=a(t,e,r)});if(n!=="unset"){return n}}function d(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function s(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function l(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=_().createDocumentFragment()}return i}function g(e){if(U.config.useTemplateFragments){var t=l("",0);return t.querySelector("template").content}else{var r=s(e);switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return l(""+e+"
",1);case"col":return l(""+e+"
",2);case"tr":return l(""+e+"
",2);case"td":case"th":return l(""+e+"
",3);case"script":return l("
"+e+"
",1);default:return l(e,0)}}}function Z(e){if(e){e()}}function p(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function m(e){return p(e,"Function")}function x(e){return p(e,"Object")}function W(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function y(e){var t=[];if(e){for(var r=0;r=0}function K(e){if(e.getRootNode()instanceof ShadowRoot){return _().body.contains(e.getRootNode().host)}else{return _().body.contains(e)}}function w(e){return e.trim().split(/\s+/)}function Y(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function S(e){try{return JSON.parse(e)}catch(e){St(e);return null}}function E(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function e(e){return Qt(_().body,function(){return eval(e)})}function t(t){var e=U.on("htmx:load",function(e){t(e.detail.elt)});return e}function C(){U.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function R(e,t){if(t){return e.querySelector(t)}else{return R(_(),e)}}function O(e,t){if(t){return e.querySelectorAll(t)}else{return O(_(),e)}}function q(e,t){e=D(e);if(t){setTimeout(function(){q(e)},t)}else{e.parentElement.removeChild(e)}}function L(e,t,r){e=D(e);if(r){setTimeout(function(){L(e,t)},r)}else{e.classList&&e.classList.add(t)}}function T(e,t,r){e=D(e);if(r){setTimeout(function(){T(e,t)},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function H(e,t){e=D(e);e.classList.toggle(t)}function A(e,t){e=D(e);G(e.parentElement.children,function(e){T(e,t)});L(e,t)}function N(e,t){e=D(e);if(e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&u(e))}}function I(e,t){if(t.indexOf("closest ")===0){return[N(e,t.substr(8))]}else if(t.indexOf("find ")===0){return[R(e,t.substr(5))]}else if(t.indexOf("next ")===0){return[k(e,t.substr(5))]}else if(t.indexOf("previous ")===0){return[M(e,t.substr(9))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return _().querySelectorAll(t)}}var k=function(e,t){var r=_().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function Q(e,t){if(t){return I(e,t)[0]}else{return I(_().body,e)[0]}}function D(e){if(p(e,"String")){return R(e)}else{return e}}function P(e,t,r){if(m(t)){return{target:_().body,event:e,listener:t}}else{return{target:D(e),event:t,listener:r}}}function X(t,r,n){pr(function(){var e=P(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=m(r);return e?r:n}function F(t,r,n){pr(function(){var e=P(t,r,n);e.target.removeEventListener(e.event,e.listener)});return m(r)?r:n}var ee=_().createElement("output");function j(e,t){var r=z(e,t);if(r){if(r==="this"){return[te(e,t)]}else{var n=I(e,r);if(n.length===0){St('The selector "'+r+'" on '+t+" returned no matches!");return[ee]}else{return n}}}}function te(e,t){return h(e,function(e){return V(e,t)!=null})}function re(e){var t=z(e,"hx-target");if(t){if(t==="this"){return te(e,"hx-target")}else{return Q(e,t)}}else{var r=W(e);if(r.boosted){return _().body}else{return e}}}function B(e){var t=U.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=_().querySelectorAll(t);if(r){G(r,function(e){var t;var r=i.cloneNode(true);t=_().createDocumentFragment();t.appendChild(r);if(!ie(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!$(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Ce(o,e,e,t,a)}G(a.elts,function(e){$(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);J(_().body,"htmx:oobErrorNoTarget",{content:i})}return e}function oe(e,t,r){var n=z(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var t=n.querySelector(e.tagName+"[id='"+e.id+"']");if(t&&t!==n){var r=e.cloneNode();ne(e,t);i.tasks.push(function(){ne(e,r)})}}})}function ue(e){return function(){T(e,U.config.addedClass);mt(e);ht(e);fe(e);$(e,"htmx:load")}}function fe(e){var t="[autofocus]";var r=d(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function ce(e,t,r,n){le(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;L(i,U.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(ue(i))}}}function he(e,t){var r=0;while(r-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Oe(e,t,r,n,i){i.title=Re(n);var a=g(n);if(a){oe(r,a,i);a=Ee(r,a);se(a);return Ce(e,r,t,a,i)}}function qe(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=S(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!x(o)){o={value:o}}$(r,a,o)}}}else{$(r,n,[])}}var Le=/\s/;var Te=/[\s,]/;var He=/[_$a-zA-Z]/;var Ae=/[_$a-zA-Z0-9]/;var Ne=['"',"'","/"];var Ie=/[^\s]/;function ke(e){var t=[];var r=0;while(r0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Qt(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){J(_().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Me(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function c(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var Pe="input, textarea, select";function Xe(e){var t=V(e,"hx-trigger");var r=[];if(t){var n=ke(t);do{c(n,Ie);var f=n.length;var i=c(n,/[,\[\s]/);if(i!==""){if(i==="every"){var a={trigger:"every"};c(n,Ie);a.pollInterval=v(c(n,/[,\[\s]/));c(n,Ie);var o=De(e,n,"event");if(o){a.eventFilter=o}r.push(a)}else if(i.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:i.substr(4)})}else{var s={trigger:i};var o=De(e,n,"event");if(o){s.eventFilter=o}while(n.length>0&&n[0]!==","){c(n,Ie);var l=n.shift();if(l==="changed"){s.changed=true}else if(l==="once"){s.once=true}else if(l==="consume"){s.consume=true}else if(l==="delay"&&n[0]===":"){n.shift();s.delay=v(c(n,Te))}else if(l==="from"&&n[0]===":"){n.shift();var u=c(n,Te);if(u==="closest"||u==="find"||u==="next"||u==="previous"){n.shift();u+=" "+c(n,Te)}s.from=u}else if(l==="target"&&n[0]===":"){n.shift();s.target=c(n,Te)}else if(l==="throttle"&&n[0]===":"){n.shift();s.throttle=v(c(n,Te))}else if(l==="queue"&&n[0]===":"){n.shift();s.queue=c(n,Te)}else if((l==="root"||l==="threshold")&&n[0]===":"){n.shift();s[l]=c(n,Te)}else{J(e,"htmx:syntax:error",{token:n.shift()})}}r.push(s)}}if(n.length===f){J(e,"htmx:syntax:error",{token:n.shift()})}c(n,Ie)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,'input[type="button"]')){return[{trigger:"click"}]}else if(d(e,Pe)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Fe(e){W(e).cancelled=true}function je(e,t,r){var n=W(e);n.timeout=setTimeout(function(){if(K(e)&&n.cancelled!==true){if(!ze(r,yt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}je(e,t,r)}},r.pollInterval)}function Be(e){return location.hostname===e.hostname&&f(e,"href")&&f(e,"href").indexOf("#")!==0}function Ue(t,r,e){if(t.tagName==="A"&&Be(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=f(t,"href")}else{var a=f(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=f(t,"action")}e.forEach(function(e){We(t,function(e){lr(n,i,t,e)},r,e,true)})}}function Ve(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(d(t,'input[type="submit"], button')&&N(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function _e(e,t){return W(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ze(e,t){var r=e.eventFilter;if(r){try{return r(t)!==true}catch(e){J(_().body,"htmx:eventFilter:error",{error:e,source:r.source});return true}}return false}function We(a,o,e,s,l){var t;if(s.from){t=I(a,s.from)}else{t=[a]}G(t,function(n){var i=function(e){if(!K(a)){n.removeEventListener(s.trigger,i);return}if(_e(a,e)){return}if(l||Ve(e,a)){e.preventDefault()}if(ze(s,e)){return}var t=W(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}var r=W(a);if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!d(e.target,s.target)){return}}if(s.once){if(r.triggeredOnce){return}else{r.triggeredOnce=true}}if(s.changed){if(r.lastValue===a.value){return}else{r.lastValue=a.value}}if(r.delayed){clearTimeout(r.delayed)}if(r.throttle){return}if(s.throttle){if(!r.throttle){o(a,e);r.throttle=setTimeout(function(){r.throttle=null},s.throttle)}}else if(s.delay){r.delayed=setTimeout(function(){o(a,e)},s.delay)}else{o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var Ge=false;var Je=null;function $e(){if(!Je){Je=function(){Ge=true};window.addEventListener("scroll",Je);setInterval(function(){if(Ge){Ge=false;G(_().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){Ze(e)})}},200)}}function Ze(t){if(!o(t,"data-hx-revealed")&&b(t)){t.setAttribute("data-hx-revealed","true");var e=W(t);if(e.initHash){$(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){$(t,"revealed")},{once:true})}}}function Ke(e,t,r){var n=w(r);for(var i=0;i=0){var t=tt(n);setTimeout(function(){Ye(s,r,n+1)},t)}};t.onopen=function(e){n=0};W(s).webSocket=t;t.addEventListener("message",function(e){if(Qe(s)){return}var t=e.data;wt(s,function(e){t=e.transformResponse(t,null,s)});var r=Zt(s);var n=g(t);var i=y(n.children);for(var a=0;a0){$(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(Ve(e,u)){e.preventDefault()}})}else{J(u,"htmx:noWebSocketSourceError")}}function tt(e){var t=U.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}St('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function rt(e,t,r){var n=w(r);for(var i=0;iU.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){J(_().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Ot(e){if(!E()){return null}var t=S(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){$(_().body,"htmx:historyCacheMissLoad",o);var e=g(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Ct();var r=Zt(t);var n=Re(this.response);if(n){var i=R("title");if(i){i.innerHTML=n}else{window.document.title=n}}Se(t,e,r);At(r.tasks);Et=a;$(_().body,"htmx:historyRestore",{path:a})}else{J(_().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function It(e){Lt();e=e||location.pathname+location.search;var t=Ot(e);if(t){var r=g(t.content);var n=Ct();var i=Zt(n);Se(n,r,i);At(i.tasks);document.title=t.title;window.scrollTo(0,t.scroll);Et=e;$(_().body,"htmx:historyRestore",{path:e})}else{if(U.config.refreshOnHistoryMiss){window.location.reload(true)}else{Nt(e)}}}function kt(e){var t=j(e,"hx-indicator");if(t==null){t=[e]}G(t,function(e){var t=W(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,U.config.requestClass)});return t}function Mt(e){G(e,function(e){var t=W(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,U.config.requestClass)}})}function Dt(e,t){for(var r=0;r=0}function Gt(e,t){var r=t?t:z(e,"hx-swap");var n={swapStyle:W(e).boosted?"innerHTML":U.config.defaultSwapStyle,swapDelay:U.config.defaultSwapDelay,settleDelay:U.config.defaultSettleDelay};if(W(e).boosted&&!Wt(e)){n["show"]="top"}if(r){var i=w(r);if(i.length>0){n["swapStyle"]=i[0];for(var a=1;a0?l.join(":"):null;n["scroll"]=f;n["scrollTarget"]=u}if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var u=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=u}if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}}}}return n}function Jt(e){return z(e,"hx-encoding")==="multipart/form-data"||d(e,"form")&&f(e,"enctype")==="multipart/form-data"}function $t(t,r,n){var i=null;wt(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Jt(r)){return Vt(n)}else{return Ut(n)}}}function Zt(e){return{tasks:[],elts:[e]}}function Kt(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=Q(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=Q(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:U.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:U.config.scrollBehavior})}}}function Yt(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=V(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Qt(e,function(){return Function("return ("+a+")")()},{})}else{s=S(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Yt(u(e),t,r,n)}function Qt(e,t,r){if(U.config.allowEval){return t()}else{J(e,"htmx:evalDisallowedError");return r}}function er(e,t){return Yt(e,"hx-vars",true,t)}function tr(e,t){return Yt(e,"hx-vals",false,t)}function rr(e){return Y(er(e),tr(e))}function nr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function ir(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){J(_().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function ar(e,t){return e.getAllResponseHeaders().match(t)}function or(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||p(r,"String")){return lr(e,t,null,null,{targetOverride:D(r),returnPromise:true})}else{return lr(e,t,D(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:D(r.target),swapOverride:r.swap,returnPromise:true})}}else{return lr(e,t,null,null,{returnPromise:true})}}function sr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function lr(e,t,n,f,r){var c=null;var h=null;r=r!=null?r:{};if(r.returnPromise&&typeof Promise!=="undefined"){var d=new Promise(function(e,t){c=e;h=t})}if(n==null){n=_().body}var v=r.handler||fr;if(!K(n)){return}var g=r.targetOverride||re(n);if(g==null||g==ee){J(n,"htmx:targetError",{target:V(n,"hx-target")});return}var p=n;var i=W(n);var a=z(n,"hx-sync");var m=null;var x=false;if(a){var y=a.split(":");var b=y[0].trim();if(b==="this"){p=te(n,"hx-sync")}else{p=Q(n,b)}a=(y[1]||"drop").trim();i=W(p);if(a==="drop"&&i.xhr&&i.abortable!==true){return}else if(a==="abort"){if(i.xhr){return}else{x=true}}else if(a==="replace"){$(p,"htmx:abort")}else if(a.indexOf("queue")===0){var w=a.split(" ");m=(w[1]||"last").trim()}}if(i.xhr){if(i.abortable){$(p,"htmx:abort")}else{if(m==null){if(f){var S=W(f);if(S&&S.triggerSpec&&S.triggerSpec.queue){m=S.triggerSpec.queue}}if(m==null){m="last"}}if(i.queuedRequests==null){i.queuedRequests=[]}if(m==="first"&&i.queuedRequests.length===0){i.queuedRequests.push(function(){lr(e,t,n,f,r)})}else if(m==="all"){i.queuedRequests.push(function(){lr(e,t,n,f,r)})}else if(m==="last"){i.queuedRequests=[];i.queuedRequests.push(function(){lr(e,t,n,f,r)})}return}}var o=new XMLHttpRequest;i.xhr=o;i.abortable=x;var s=function(){i.xhr=null;i.abortable=false;if(i.queuedRequests!=null&&i.queuedRequests.length>0){var e=i.queuedRequests.shift();e()}};var E=z(n,"hx-prompt");if(E){var C=prompt(E);if(C===null||!$(n,"htmx:prompt",{prompt:C,target:g})){Z(c);s();return d}}var R=z(n,"hx-confirm");if(R){if(!confirm(R)){Z(c);s();return d}}var O=_t(n,g,C);if(r.headers){O=Y(O,r.headers)}var q=jt(n,e);var L=q.errors;var T=q.values;if(r.values){T=Y(T,r.values)}var H=rr(n);var A=Y(T,H);var N=zt(A,n);if(e!=="get"&&!Jt(n)){O["Content-Type"]="application/x-www-form-urlencoded"}if(t==null||t===""){t=_().location.href}var I=Yt(n,"hx-request");var l={parameters:N,unfilteredParameters:A,headers:O,target:g,verb:e,errors:L,withCredentials:r.credentials||I.credentials||U.config.withCredentials,timeout:r.timeout||I.timeout||U.config.timeout,path:t,triggeringEvent:f};if(!$(n,"htmx:configRequest",l)){Z(c);s();return d}t=l.path;e=l.verb;O=l.headers;N=l.parameters;L=l.errors;if(L&&L.length>0){$(n,"htmx:validation:halted",l);Z(c);s();return d}var k=t.split("#");var M=k[0];var D=k[1];var P=null;if(e==="get"){P=M;var X=Object.keys(N).length!==0;if(X){if(P.indexOf("?")<0){P+="?"}else{P+="&"}P+=Ut(N);if(D){P+="#"+D}}o.open("GET",P,true)}else{o.open(e.toUpperCase(),t,true)}o.overrideMimeType("text/html");o.withCredentials=l.withCredentials;o.timeout=l.timeout;if(I.noHeaders){}else{for(var F in O){if(O.hasOwnProperty(F)){var j=O[F];nr(o,F,j)}}}var u={xhr:o,target:g,requestConfig:l,etc:r,pathInfo:{requestPath:t,finalRequestPath:P||t,anchor:D}};o.onload=function(){try{var e=sr(n);u.pathInfo.responsePath=ir(o);v(n,u);Mt(B);$(n,"htmx:afterRequest",u);$(n,"htmx:afterOnLoad",u);if(!K(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(K(r)){t=r}}if(t){$(t,"htmx:afterRequest",u);$(t,"htmx:afterOnLoad",u)}}Z(c);s()}catch(e){J(n,"htmx:onLoadError",Y({error:e},u));throw e}};o.onerror=function(){Mt(B);J(n,"htmx:afterRequest",u);J(n,"htmx:sendError",u);Z(h);s()};o.onabort=function(){Mt(B);J(n,"htmx:afterRequest",u);J(n,"htmx:sendAbort",u);Z(h);s()};o.ontimeout=function(){Mt(B);J(n,"htmx:afterRequest",u);J(n,"htmx:timeout",u);Z(h);s()};if(!$(n,"htmx:beforeRequest",u)){Z(c);s();return d}var B=kt(n);G(["loadstart","loadend","progress","abort"],function(t){G([o,o.upload],function(e){e.addEventListener(t,function(e){$(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});$(n,"htmx:beforeSend",u);o.send(e==="get"?null:$t(o,n,N));return d}function ur(e,t){var r=t.xhr;var n=null;var i=null;if(ar(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(ar(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(ar(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=z(e,"hx-push-url");var f=z(e,"hx-replace-url");var c=W(e).boosted;var l=null;var u=null;if(s){l="push";u=s}else if(f){l="replace";u=f}else if(c){l="push";u=o||a}if(u){if(u==="false"){return{}}if(u==="true"){u=o||a}if(t.pathInfo.anchor&&u.indexOf("#")===-1){u=u+"#"+t.pathInfo.anchor}return{type:l,path:u}}else{return{}}}function fr(s,l){var u=l.xhr;var f=l.target;var n=l.etc;if(!$(s,"htmx:beforeOnLoad",l))return;if(ar(u,/HX-Trigger:/i)){qe(u,"HX-Trigger",s)}if(ar(u,/HX-Location:/i)){Lt();var e=u.getResponseHeader("HX-Location");var c;if(e.indexOf("{")===0){c=S(e);e=c["path"];delete c["path"]}or("GET",e,c).then(()=>{Tt(e)});return}if(ar(u,/HX-Redirect:/i)){location.href=u.getResponseHeader("HX-Redirect");return}if(ar(u,/HX-Refresh:/i)){if("true"===u.getResponseHeader("HX-Refresh")){location.reload();return}}if(ar(u,/HX-Retarget:/i)){l.target=_().querySelector(u.getResponseHeader("HX-Retarget"))}var h=ur(s,l);var i=u.status>=200&&u.status<400&&u.status!==204;var d=u.response;var t=u.status>=400;var r=Y({shouldSwap:i,serverResponse:d,isError:t},l);if(!$(f,"htmx:beforeSwap",r))return;f=r.target;d=r.serverResponse;t=r.isError;l.failed=t;l.successful=!t;if(r.shouldSwap){if(u.status===286){Fe(s)}wt(s,function(e){d=e.transformResponse(d,u,s)});if(h.type){Lt()}var a=n.swapOverride;if(ar(u,/HX-Reswap:/i)){a=u.getResponseHeader("HX-Reswap")}var c=Gt(s,a);f.classList.add(U.config.swappingClass);var o=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var n=Zt(f);Oe(c.swapStyle,f,s,d,n);if(t.elt&&!K(t.elt)&&t.elt.id){var r=document.getElementById(t.elt.id);var i={preventScroll:c.focusScroll!==undefined?!c.focusScroll:!U.config.defaultFocusScroll};if(r){if(t.start&&r.setSelectionRange){r.setSelectionRange(t.start,t.end)}r.focus(i)}}f.classList.remove(U.config.swappingClass);G(n.elts,function(e){if(e.classList){e.classList.add(U.config.settlingClass)}$(e,"htmx:afterSwap",l)});if(ar(u,/HX-Trigger-After-Swap:/i)){var a=s;if(!K(s)){a=_().body}qe(u,"HX-Trigger-After-Swap",a)}var o=function(){G(n.tasks,function(e){e.call()});G(n.elts,function(e){if(e.classList){e.classList.remove(U.config.settlingClass)}$(e,"htmx:afterSettle",l)});if(h.type){if(h.type==="push"){Tt(h.path);$(_().body,"htmx:pushedIntoHistory",{path:h.path})}else{Ht(h.path);$(_().body,"htmx:replacedInHistory",{path:h.path})}}if(l.pathInfo.anchor){var e=R("#"+l.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title){var t=R("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Kt(n.elts,c);if(ar(u,/HX-Trigger-After-Settle:/i)){var r=s;if(!K(s)){r=_().body}qe(u,"HX-Trigger-After-Settle",r)}};if(c.settleDelay>0){setTimeout(o,c.settleDelay)}else{o()}}catch(e){J(s,"htmx:swapError",l);throw e}};if(c.swapDelay>0){setTimeout(o,c.swapDelay)}else{o()}}if(t){J(s,"htmx:responseError",Y({error:"Response Status Error Code "+u.status+" from "+l.pathInfo.path},l))}}var cr={};function hr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function dr(e,t){if(t.init){t.init(r)}cr[e]=Y(hr(),t)}function vr(e){delete cr[e]}function gr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=V(e,"hx-ext");if(t){G(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=cr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return gr(u(e),r,n)}function pr(e){if(_().readyState!=="loading"){e()}else{_().addEventListener("DOMContentLoaded",e)}}function mr(){if(U.config.includeIndicatorStyles!==false){_().head.insertAdjacentHTML("beforeend","")}}function xr(){var e=_().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function yr(){var e=xr();if(e){U.config=Y(U.config,e)}}pr(function(){yr();mr();var e=_().body;mt(e);var t=_().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=W(t);if(r&&r.xhr){r.xhr.abort()}});window.onpopstate=function(e){if(e.state&&e.state.htmx){It();G(t,function(e){$(e,"htmx:restored",{document:_(),triggerEvent:$})})}};setTimeout(function(){$(e,"htmx:load",{})},0)});return U}()}); \ No newline at end of file diff --git a/src/common/templates/common/base.html b/src/common/templates/common/base.html new file mode 100644 index 0000000..c213801 --- /dev/null +++ b/src/common/templates/common/base.html @@ -0,0 +1,28 @@ +{% load static django_htmx %} + + + + + + Character Sheet + + + {% include "common/hello-random.html" %} + + {% django_htmx_script %} + {% if debug %} + + {% endif %} + + diff --git a/src/common/templates/common/hello-random.html b/src/common/templates/common/hello-random.html new file mode 100644 index 0000000..e20872d --- /dev/null +++ b/src/common/templates/common/hello-random.html @@ -0,0 +1 @@ +

Hello, world! Click me - {{ value }}

diff --git a/src/common/tests/__init__.py b/src/common/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/tests/test_admin.py b/src/common/tests/test_admin.py new file mode 100644 index 0000000..ff9b60e --- /dev/null +++ b/src/common/tests/test_admin.py @@ -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 diff --git a/src/common/views.py b/src/common/views.py new file mode 100644 index 0000000..4dd684e --- /dev/null +++ b/src/common/views.py @@ -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) diff --git a/src/conftest.py b/src/conftest.py new file mode 100644 index 0000000..bddc4e9 --- /dev/null +++ b/src/conftest.py @@ -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") diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..4d52876 --- /dev/null +++ b/src/manage.py @@ -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() diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..43487d8 --- /dev/null +++ b/tasks.py @@ -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)