mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-05 20:44:19 +01:00
Start of Symfony
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
/.idea/
|
|
||||||
/containers/
|
|
||||||
.gitignore
|
|
||||||
compose.*
|
|
||||||
34
.dockerignore
Normal file
34
.dockerignore
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**/*.log
|
||||||
|
**/*.md
|
||||||
|
**/*.php~
|
||||||
|
**/*.dist.php
|
||||||
|
**/*.dist
|
||||||
|
**/*.cache
|
||||||
|
**/._*
|
||||||
|
**/.dockerignore
|
||||||
|
**/.DS_Store
|
||||||
|
**/.git/
|
||||||
|
**/.gitattributes
|
||||||
|
**/.gitignore
|
||||||
|
**/.gitmodules
|
||||||
|
**/compose.*.yaml
|
||||||
|
**/compose.*.yml
|
||||||
|
**/compose.yaml
|
||||||
|
**/compose.yml
|
||||||
|
**/docker-compose.*.yaml
|
||||||
|
**/docker-compose.*.yml
|
||||||
|
**/docker-compose.yaml
|
||||||
|
**/docker-compose.yml
|
||||||
|
**/Dockerfile
|
||||||
|
**/Thumbs.db
|
||||||
|
.github/
|
||||||
|
docs/
|
||||||
|
public/bundles/
|
||||||
|
tests/
|
||||||
|
var/
|
||||||
|
vendor/
|
||||||
|
.editorconfig
|
||||||
|
.env.*.local
|
||||||
|
.env.local
|
||||||
|
.env.local.php
|
||||||
|
.env.test
|
||||||
57
.editorconfig
Normal file
57
.editorconfig
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
# Change these settings to your own preference
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# We recommend you to keep these unchanged
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{js,html,ts,tsx}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.sh]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.xml.dist]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{yaml,yml}]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[.github/workflows/*.yml]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[.gitmodules]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[.php_cs.dist]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[composer.json]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[{compose,docker-compose}.*.{yaml,yml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.*Dockerfile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.*Caddyfile]
|
||||||
|
indent_style = tab
|
||||||
20
.env
Normal file
20
.env
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# In all environments, the following files are loaded if they exist,
|
||||||
|
# the latter taking precedence over the former:
|
||||||
|
#
|
||||||
|
# * .env contains default values for the environment variables needed by the app
|
||||||
|
# * .env.local uncommitted file with local overrides
|
||||||
|
# * .env.$APP_ENV committed environment-specific defaults
|
||||||
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||||
|
#
|
||||||
|
# Real environment variables win over .env files.
|
||||||
|
#
|
||||||
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||||
|
# https://symfony.com/doc/current/configuration/secrets.html
|
||||||
|
#
|
||||||
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
4
.env.dev
Normal file
4
.env.dev
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_SECRET=e26b9552d9e7f969b160373effaa7690
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.conf text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.ini text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.php text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
bin/console text eol=lf
|
||||||
|
composer.lock text eol=lf merge=ours
|
||||||
|
|
||||||
|
*.ico binary
|
||||||
|
*.png binary
|
||||||
76
.github/workflows/ci.yml
vendored
Normal file
76
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request: ~
|
||||||
|
workflow_dispatch: ~
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
-
|
||||||
|
name: Build Docker images
|
||||||
|
uses: docker/bake-action@v4
|
||||||
|
with:
|
||||||
|
pull: true
|
||||||
|
load: true
|
||||||
|
files: |
|
||||||
|
compose.yaml
|
||||||
|
compose.override.yaml
|
||||||
|
set: |
|
||||||
|
*.cache-from=type=gha,scope=${{github.ref}}
|
||||||
|
*.cache-from=type=gha,scope=refs/heads/main
|
||||||
|
*.cache-to=type=gha,scope=${{github.ref}},mode=max
|
||||||
|
-
|
||||||
|
name: Start services
|
||||||
|
run: docker compose up --wait --no-build
|
||||||
|
-
|
||||||
|
name: Check HTTP reachability
|
||||||
|
run: curl -v --fail-with-body http://localhost
|
||||||
|
-
|
||||||
|
name: Check HTTPS reachability
|
||||||
|
if: false # Remove this line when the homepage will be configured, or change the path to check
|
||||||
|
run: curl -vk --fail-with-body https://localhost
|
||||||
|
-
|
||||||
|
name: Check Mercure reachability
|
||||||
|
run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test
|
||||||
|
-
|
||||||
|
name: Create test database
|
||||||
|
if: false # Remove this line if Doctrine ORM is installed
|
||||||
|
run: docker compose exec -T php bin/console -e test doctrine:database:create
|
||||||
|
-
|
||||||
|
name: Run migrations
|
||||||
|
if: false # Remove this line if Doctrine Migrations is installed
|
||||||
|
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
|
||||||
|
-
|
||||||
|
name: Run PHPUnit
|
||||||
|
if: false # Remove this line if PHPUnit is installed
|
||||||
|
run: docker compose exec -T php bin/phpunit
|
||||||
|
-
|
||||||
|
name: Doctrine Schema Validator
|
||||||
|
if: false # Remove this line if Doctrine ORM is installed
|
||||||
|
run: docker compose exec -T php bin/console -e test doctrine:schema:validate
|
||||||
|
lint:
|
||||||
|
name: Docker Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Lint Dockerfile
|
||||||
|
uses: hadolint/hadolint-action@v3.1.0
|
||||||
210
.gitignore
vendored
210
.gitignore
vendored
@@ -1,174 +1,60 @@
|
|||||||
compose.override.yaml
|
|
||||||
/tvdt/staticfiles/
|
|
||||||
|
|
||||||
.idea/
|
|
||||||
### Generated by gibo (https://github.com/simonwhitaker/gibo)
|
### Generated by gibo (https://github.com/simonwhitaker/gibo)
|
||||||
### https://raw.github.com/github/gitignore/76739a38b56907118c5a880d63250c99d5690a5a/Python.gitignore
|
### https://raw.github.com/github/gitignore/6eeebe6f49678aacd8311ce079842c971b3ebe96/Symfony.gitignore
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Cache and logs (Symfony2)
|
||||||
__pycache__/
|
/app/cache/*
|
||||||
*.py[cod]
|
/app/logs/*
|
||||||
*$py.class
|
!app/cache/.gitkeep
|
||||||
|
!app/logs/.gitkeep
|
||||||
|
|
||||||
# C extensions
|
# Email spool folder
|
||||||
*.so
|
/app/spool/*
|
||||||
|
|
||||||
# Distribution / packaging
|
# Cache, session files and logs (Symfony3)
|
||||||
.Python
|
/var/cache/*
|
||||||
build/
|
/var/logs/*
|
||||||
develop-eggs/
|
/var/sessions/*
|
||||||
dist/
|
!var/cache/.gitkeep
|
||||||
downloads/
|
!var/logs/.gitkeep
|
||||||
eggs/
|
!var/sessions/.gitkeep
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
# Logs (Symfony4)
|
||||||
# Usually these files are written by a python script from a template
|
/var/log/*
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
!var/log/.gitkeep
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
# Parameters
|
||||||
pip-log.txt
|
/app/config/parameters.yml
|
||||||
pip-delete-this-directory.txt
|
/app/config/parameters.ini
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# Managed by Composer
|
||||||
htmlcov/
|
/app/bootstrap.php.cache
|
||||||
.tox/
|
/var/bootstrap.php.cache
|
||||||
.nox/
|
/bin/*
|
||||||
.coverage
|
!bin/console
|
||||||
.coverage.*
|
!bin/symfony_requirements
|
||||||
.cache
|
/vendor/
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
# Assets and user uploads
|
||||||
*.mo
|
/web/bundles/
|
||||||
*.pot
|
/web/uploads/
|
||||||
|
|
||||||
# Django stuff:
|
# PHPUnit
|
||||||
*.log
|
/app/phpunit.xml
|
||||||
local_settings.py
|
/phpunit.xml
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
# Build data
|
||||||
instance/
|
/build/
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
# Composer PHAR
|
||||||
.scrapy
|
/composer.phar
|
||||||
|
|
||||||
# Sphinx documentation
|
# Backup entities generated with doctrine:generate:entities command
|
||||||
docs/_build/
|
**/Entity/*~
|
||||||
|
|
||||||
# PyBuilder
|
# Embedded web-server pid file
|
||||||
.pybuilder/
|
/.web-server-pid
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
### Generated by gibo (https://github.com/simonwhitaker/gibo)
|
### Generated by gibo (https://github.com/simonwhitaker/gibo)
|
||||||
### https://raw.github.com/github/gitignore/76739a38b56907118c5a880d63250c99d5690a5a/Global/macOS.gitignore
|
### https://raw.github.com/github/gitignore/6eeebe6f49678aacd8311ce079842c971b3ebe96/Global/macOS.gitignore
|
||||||
|
|
||||||
# General
|
# General
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -197,3 +83,13 @@ Icon
|
|||||||
Network Trash Folder
|
Network Trash Folder
|
||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env.local
|
||||||
|
/.env.local.php
|
||||||
|
/.env.*.local
|
||||||
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
/public/bundles/
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|||||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
94
Dockerfile
Normal file
94
Dockerfile
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Versions
|
||||||
|
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
|
||||||
|
|
||||||
|
# The different stages of this Dockerfile are meant to be built into separate images
|
||||||
|
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
|
||||||
|
# https://docs.docker.com/compose/compose-file/#target
|
||||||
|
|
||||||
|
|
||||||
|
# Base FrankenPHP image
|
||||||
|
FROM frankenphp_upstream AS frankenphp_base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
VOLUME /app/var/
|
||||||
|
|
||||||
|
# persistent / runtime deps
|
||||||
|
# hadolint ignore=DL3008
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
acl \
|
||||||
|
file \
|
||||||
|
gettext \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
install-php-extensions \
|
||||||
|
@composer \
|
||||||
|
apcu \
|
||||||
|
intl \
|
||||||
|
opcache \
|
||||||
|
zip \
|
||||||
|
;
|
||||||
|
|
||||||
|
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
||||||
|
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||||
|
|
||||||
|
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
|
||||||
|
|
||||||
|
###> recipes ###
|
||||||
|
###< recipes ###
|
||||||
|
|
||||||
|
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
|
||||||
|
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||||
|
COPY --link frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint"]
|
||||||
|
|
||||||
|
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
||||||
|
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile" ]
|
||||||
|
|
||||||
|
# Dev FrankenPHP image
|
||||||
|
FROM frankenphp_base AS frankenphp_dev
|
||||||
|
|
||||||
|
ENV APP_ENV=dev XDEBUG_MODE=off
|
||||||
|
|
||||||
|
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
install-php-extensions \
|
||||||
|
xdebug \
|
||||||
|
;
|
||||||
|
|
||||||
|
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
|
||||||
|
|
||||||
|
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ]
|
||||||
|
|
||||||
|
# Prod FrankenPHP image
|
||||||
|
FROM frankenphp_base AS frankenphp_prod
|
||||||
|
|
||||||
|
ENV APP_ENV=prod
|
||||||
|
ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
|
||||||
|
|
||||||
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
|
|
||||||
|
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
|
||||||
|
COPY --link frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
|
||||||
|
|
||||||
|
# prevent the reinstallation of vendors at every changes in the source code
|
||||||
|
COPY --link composer.* symfony.* ./
|
||||||
|
RUN set -eux; \
|
||||||
|
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
|
||||||
|
|
||||||
|
# copy sources
|
||||||
|
COPY --link . ./
|
||||||
|
RUN rm -Rf frankenphp/
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
mkdir -p var/cache var/log; \
|
||||||
|
composer dump-autoload --classmap-authoritative --no-dev; \
|
||||||
|
composer dump-env prod; \
|
||||||
|
composer run-script --no-dev post-install-cmd; \
|
||||||
|
chmod +x bin/console; sync;
|
||||||
59
Makefile
59
Makefile
@@ -1,59 +0,0 @@
|
|||||||
DOCKER_EXEC=docker compose exec app
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help:
|
|
||||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-10s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
|
||||||
|
|
||||||
.PHONY: init
|
|
||||||
init: install up migrate fixtures ## setup the application from scratch
|
|
||||||
|
|
||||||
.PHONY: up
|
|
||||||
up: ## Starts the app
|
|
||||||
@docker compose up -d --force-recreate
|
|
||||||
|
|
||||||
.PHONY: down
|
|
||||||
down: ## Stops the app
|
|
||||||
@docker compose down --remove-orphans
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: ## (Re)build the containers
|
|
||||||
@echo ✨ Building container
|
|
||||||
@docker compose build
|
|
||||||
|
|
||||||
.PHONY: shell
|
|
||||||
shell: ## Opens a shell in the app container
|
|
||||||
@${DOCKER_EXEC} bash
|
|
||||||
|
|
||||||
.PHONY: migrate
|
|
||||||
migrate: ## Migrate the database to the latest version
|
|
||||||
@echo ✨ Appling migrations
|
|
||||||
@${DOCKER_EXEC} python manage.py migrate
|
|
||||||
|
|
||||||
.PHONY: compilemessages
|
|
||||||
messages: ## Compile translations
|
|
||||||
@echo ✨ Finding translations
|
|
||||||
@${DOCKER_EXEC} python manage.py makemessages -l nl
|
|
||||||
@echo ✨ Compiling translations
|
|
||||||
@${DOCKER_EXEC} python manage.py compilemessages --ignore .venv
|
|
||||||
|
|
||||||
.PHONY: install
|
|
||||||
install: ## Install dependencies in container
|
|
||||||
@echo ✨ Installing dependencies
|
|
||||||
@docker compose run --rm --entrypoint="" app poetry install --without prod --sync
|
|
||||||
|
|
||||||
.PHONY: fixtures
|
|
||||||
fixtures:
|
|
||||||
@echo ✨ Loading fixtures
|
|
||||||
@${DOCKER_EXEC} python manage.py loaddata krtek
|
|
||||||
|
|
||||||
.PHONY: _clean
|
|
||||||
_clean:
|
|
||||||
@echo ✨ Stopping containers
|
|
||||||
@docker compose down -v
|
|
||||||
@echo ✨ Removing compiled files
|
|
||||||
@rm -f tvdt/*/locale/*/LC_MESSAGES/django.mo tvdt/locale/*/LC_MESSAGES/django.mo
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean: _clean init
|
|
||||||
|
|
||||||
21
bin/console
Executable file
21
bin/console
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
|
||||||
|
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||||
|
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||||
|
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
|
||||||
|
return new Application($kernel);
|
||||||
|
};
|
||||||
24
compose.override.yaml
Normal file
24
compose.override.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Development environment override
|
||||||
|
services:
|
||||||
|
php:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: frankenphp_dev
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
- ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro
|
||||||
|
# If you develop on Mac or Windows you can remove the vendor/ directory
|
||||||
|
# from the bind-mount for better performance by enabling the next line:
|
||||||
|
#- /app/vendor
|
||||||
|
environment:
|
||||||
|
MERCURE_EXTRA_DIRECTIVES: demo
|
||||||
|
# See https://xdebug.org/docs/all_settings#mode
|
||||||
|
XDEBUG_MODE: "${XDEBUG_MODE:-off}"
|
||||||
|
extra_hosts:
|
||||||
|
# Ensure that host.docker.internal is correctly defined on Linux
|
||||||
|
- host.docker.internal:host-gateway
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
###> symfony/mercure-bundle ###
|
||||||
|
###< symfony/mercure-bundle ###
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
# Production environment override
|
||||||
services:
|
services:
|
||||||
app:
|
php:
|
||||||
image: tvdt/app:prod
|
|
||||||
build:
|
build:
|
||||||
dockerfile: containers/python/Containerfile.prod
|
context: .
|
||||||
|
target: frankenphp_prod
|
||||||
|
environment:
|
||||||
|
APP_SECRET: ${APP_SECRET}
|
||||||
|
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
||||||
|
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
||||||
|
|||||||
56
compose.yaml
56
compose.yaml
@@ -1,25 +1,43 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
php:
|
||||||
build:
|
image: ${IMAGES_PREFIX:-}app-php
|
||||||
dockerfile: containers/python/Containerfile.dev
|
restart: unless-stopped
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgres://tvdt:tvdt@db:5432/tvdt
|
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
|
||||||
DEBUG: true
|
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||||
depends_on:
|
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||||
- db
|
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
|
||||||
db:
|
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||||
image: postgres:17.2
|
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
|
||||||
environment:
|
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
|
||||||
POSTGRES_PASSWORD: tvdt
|
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure}
|
||||||
POSTGRES_USER: tvdt
|
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||||
POSTGRES_DB: tvdt
|
# The two next lines can be removed after initial installation
|
||||||
|
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
|
||||||
|
STABILITY: ${STABILITY:-stable}
|
||||||
volumes:
|
volumes:
|
||||||
- data:/var/lib/postgresql/data
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
# HTTP
|
||||||
|
- target: 80
|
||||||
|
published: ${HTTP_PORT:-80}
|
||||||
|
protocol: tcp
|
||||||
|
# HTTPS
|
||||||
|
- target: 443
|
||||||
|
published: ${HTTPS_PORT:-443}
|
||||||
|
protocol: tcp
|
||||||
|
# HTTP/3
|
||||||
|
- target: 443
|
||||||
|
published: ${HTTP3_PORT:-443}
|
||||||
|
protocol: udp
|
||||||
|
|
||||||
|
# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
|
||||||
|
###> symfony/mercure-bundle ###
|
||||||
|
###< symfony/mercure-bundle ###
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
|
###> symfony/mercure-bundle ###
|
||||||
|
###< symfony/mercure-bundle ###
|
||||||
|
|||||||
71
composer.json
Normal file
71
composer.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/skeleton",
|
||||||
|
"type": "project",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "A minimal Symfony project recommended to create bare bones applications",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.3.15",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
|
"symfony/console": "7.2.*",
|
||||||
|
"symfony/dotenv": "7.2.*",
|
||||||
|
"symfony/flex": "^2",
|
||||||
|
"symfony/framework-bundle": "7.2.*",
|
||||||
|
"symfony/runtime": "7.2.*",
|
||||||
|
"symfony/yaml": "7.2.*"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"php-http/discovery": true,
|
||||||
|
"symfony/flex": true,
|
||||||
|
"symfony/runtime": true
|
||||||
|
},
|
||||||
|
"bump-after-update": true,
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
|
"symfony/polyfill-iconv": "*",
|
||||||
|
"symfony/polyfill-php72": "*",
|
||||||
|
"symfony/polyfill-php73": "*",
|
||||||
|
"symfony/polyfill-php74": "*",
|
||||||
|
"symfony/polyfill-php80": "*",
|
||||||
|
"symfony/polyfill-php81": "*",
|
||||||
|
"symfony/polyfill-php82": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"auto-scripts": {
|
||||||
|
"cache:clear": "symfony-cmd",
|
||||||
|
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||||
|
},
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/symfony": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"symfony": {
|
||||||
|
"allow-contrib": false,
|
||||||
|
"require": "7.2.*",
|
||||||
|
"docker": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2487
composer.lock
generated
Normal file
2487
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
config/bundles.php
Normal file
5
config/bundles.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
|
];
|
||||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
|
#prefix_seed: your_vendor_name/app_name
|
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default.
|
||||||
|
# The data in this cache should persist between deploys.
|
||||||
|
# Other options include:
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
#app: cache.adapter.redis
|
||||||
|
#default_redis_provider: redis://localhost
|
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||||
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default
|
||||||
|
#pools:
|
||||||
|
#my.dedicated.cache: null
|
||||||
15
config/packages/framework.yaml
Normal file
15
config/packages/framework.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
|
framework:
|
||||||
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
|
session: true
|
||||||
|
|
||||||
|
#esi: true
|
||||||
|
#fragments: true
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
test: true
|
||||||
|
session:
|
||||||
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
10
config/packages/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
#default_uri: http://localhost
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
||||||
5
config/preload.php
Normal file
5
config/preload.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||||
|
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||||
|
}
|
||||||
5
config/routes.yaml
Normal file
5
config/routes.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
controllers:
|
||||||
|
resource:
|
||||||
|
path: ../src/Controller/
|
||||||
|
namespace: App\Controller
|
||||||
|
type: attribute
|
||||||
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
when@dev:
|
||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||||
|
prefix: /_error
|
||||||
24
config/services.yaml
Normal file
24
config/services.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# This file is the entry point to configure your own services.
|
||||||
|
# Files in the packages/ subdirectory configure your dependencies.
|
||||||
|
|
||||||
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
|
parameters:
|
||||||
|
|
||||||
|
services:
|
||||||
|
# default configuration for services in *this* file
|
||||||
|
_defaults:
|
||||||
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
|
|
||||||
|
# makes classes in src/ available to be used as services
|
||||||
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
|
App\:
|
||||||
|
resource: '../src/'
|
||||||
|
exclude:
|
||||||
|
- '../src/DependencyInjection/'
|
||||||
|
- '../src/Entity/'
|
||||||
|
- '../src/Kernel.php'
|
||||||
|
|
||||||
|
# add more service definitions when explicit configuration is needed
|
||||||
|
# please note that last definitions always *replace* previous ones
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
FROM python:3.13 AS dev
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gettext
|
|
||||||
|
|
||||||
RUN pip install poetry~=1.8
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
|
||||||
POETRY_VIRTUALENVS_CREATE=1
|
|
||||||
|
|
||||||
ENV VIRTUAL_ENV=/app/.venv \
|
|
||||||
PATH="/app/.venv/bin:$PATH"
|
|
||||||
|
|
||||||
ENTRYPOINT ["python","manage.py", "runserver", "0.0.0.0:8000"]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
FROM python:3.13 AS builder
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
|
|
||||||
gettext
|
|
||||||
|
|
||||||
RUN pip install --no-cache-dir poetry==1.8
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV POETRY_NO_INTERACTION=1 \
|
|
||||||
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
|
||||||
POETRY_VIRTUALENVS_CREATE=1 \
|
|
||||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
|
||||||
|
|
||||||
COPY tvdt/pyproject.toml tvdt/poetry.lock ./
|
|
||||||
|
|
||||||
RUN poetry install --without dev
|
|
||||||
|
|
||||||
COPY ./tvdt/ .
|
|
||||||
|
|
||||||
ENV VIRTUAL_ENV=/app/.venv \
|
|
||||||
PATH="/app/.venv/bin:$PATH"
|
|
||||||
|
|
||||||
RUN python manage.py compilemessages --ignore .venv \
|
|
||||||
&& python manage.py collectstatic
|
|
||||||
|
|
||||||
FROM python:3.13 AS runtime
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV VIRTUAL_ENV=/app/.venv \
|
|
||||||
PATH="/app/.venv/bin:$PATH"
|
|
||||||
|
|
||||||
COPY --from=builder /app /app
|
|
||||||
|
|
||||||
ENTRYPOINT ["gunicorn", "-b", "0.0.0.0:8000", "tvdt.wsgi", ""]
|
|
||||||
57
frankenphp/Caddyfile
Normal file
57
frankenphp/Caddyfile
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
{$CADDY_GLOBAL_OPTIONS}
|
||||||
|
|
||||||
|
frankenphp {
|
||||||
|
{$FRANKENPHP_CONFIG}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{$CADDY_EXTRA_CONFIG}
|
||||||
|
|
||||||
|
{$SERVER_NAME:localhost} {
|
||||||
|
log {
|
||||||
|
{$CADDY_SERVER_LOG_OPTIONS}
|
||||||
|
# Redact the authorization query parameter that can be set by Mercure
|
||||||
|
format filter {
|
||||||
|
request>uri query {
|
||||||
|
replace authorization REDACTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root /app/public
|
||||||
|
encode zstd br gzip
|
||||||
|
|
||||||
|
mercure {
|
||||||
|
# Transport to use (default to Bolt)
|
||||||
|
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||||
|
# Publisher JWT key
|
||||||
|
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
|
||||||
|
# Subscriber JWT key
|
||||||
|
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
|
||||||
|
# Allow anonymous subscribers (double-check that it's what you want)
|
||||||
|
anonymous
|
||||||
|
# Enable the subscription API (double-check that it's what you want)
|
||||||
|
subscriptions
|
||||||
|
# Extra directives
|
||||||
|
{$MERCURE_EXTRA_DIRECTIVES}
|
||||||
|
}
|
||||||
|
|
||||||
|
vulcain
|
||||||
|
|
||||||
|
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||||
|
|
||||||
|
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
||||||
|
header ?Permissions-Policy "browsing-topics=()"
|
||||||
|
|
||||||
|
@phpRoute {
|
||||||
|
not path /.well-known/mercure*
|
||||||
|
not file {path}
|
||||||
|
}
|
||||||
|
rewrite @phpRoute index.php
|
||||||
|
|
||||||
|
@frontController path index.php
|
||||||
|
php @frontController
|
||||||
|
|
||||||
|
file_server
|
||||||
|
}
|
||||||
13
frankenphp/conf.d/10-app.ini
Normal file
13
frankenphp/conf.d/10-app.ini
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
expose_php = 0
|
||||||
|
date.timezone = UTC
|
||||||
|
apc.enable_cli = 1
|
||||||
|
session.use_strict_mode = 1
|
||||||
|
zend.detect_unicode = 0
|
||||||
|
|
||||||
|
; https://symfony.com/doc/current/performance.html
|
||||||
|
realpath_cache_size = 4096K
|
||||||
|
realpath_cache_ttl = 600
|
||||||
|
opcache.interned_strings_buffer = 16
|
||||||
|
opcache.max_accelerated_files = 20000
|
||||||
|
opcache.memory_consumption = 256
|
||||||
|
opcache.enable_file_override = 1
|
||||||
5
frankenphp/conf.d/20-app.dev.ini
Normal file
5
frankenphp/conf.d/20-app.dev.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
|
||||||
|
; See https://github.com/docker/for-linux/issues/264
|
||||||
|
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
|
||||||
|
; Add `start_with_request=yes` to start debug session on each request
|
||||||
|
xdebug.client_host = host.docker.internal
|
||||||
2
frankenphp/conf.d/20-app.prod.ini
Normal file
2
frankenphp/conf.d/20-app.prod.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
opcache.preload_user = root
|
||||||
|
opcache.preload = /app/config/preload.php
|
||||||
62
frankenphp/docker-entrypoint.sh
Executable file
62
frankenphp/docker-entrypoint.sh
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
||||||
|
# Install the project the first time PHP is started
|
||||||
|
# After the installation, the following block can be deleted
|
||||||
|
if [ ! -f composer.json ]; then
|
||||||
|
rm -Rf tmp/
|
||||||
|
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
|
||||||
|
|
||||||
|
cd tmp
|
||||||
|
cp -Rp . ..
|
||||||
|
cd -
|
||||||
|
rm -Rf tmp/
|
||||||
|
|
||||||
|
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
|
||||||
|
composer config --json extra.symfony.docker 'true'
|
||||||
|
|
||||||
|
if grep -q ^DATABASE_URL= .env; then
|
||||||
|
echo 'To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build -d --wait'
|
||||||
|
sleep infinity
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
|
||||||
|
composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q ^DATABASE_URL= .env; then
|
||||||
|
echo 'Waiting for database to be ready...'
|
||||||
|
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||||
|
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||||
|
if [ $? -eq 255 ]; then
|
||||||
|
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||||
|
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||||
|
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||||
|
echo 'The database is not up or not reachable:'
|
||||||
|
echo "$DATABASE_ERROR"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo 'The database is now ready and reachable'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||||
|
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||||
|
|
||||||
|
echo 'PHP app ready!'
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec docker-php-entrypoint "$@"
|
||||||
4
frankenphp/worker.Caddyfile
Normal file
4
frankenphp/worker.Caddyfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
worker {
|
||||||
|
file ./public/index.php
|
||||||
|
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
||||||
|
}
|
||||||
9
public/index.php
Normal file
9
public/index.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
};
|
||||||
11
src/Kernel.php
Normal file
11
src/Kernel.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
|
|
||||||
|
class Kernel extends BaseKernel
|
||||||
|
{
|
||||||
|
use MicroKernelTrait;
|
||||||
|
}
|
||||||
59
symfony.lock
Normal file
59
symfony.lock
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"symfony/console": {
|
||||||
|
"version": "7.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "5.3",
|
||||||
|
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/console"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/flex": {
|
||||||
|
"version": "2.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.4",
|
||||||
|
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env",
|
||||||
|
".env.dev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/framework-bundle": {
|
||||||
|
"version": "7.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.2",
|
||||||
|
"ref": "87bcf6f7c55201f345d8895deda46d2adbdbaa89"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/cache.yaml",
|
||||||
|
"config/packages/framework.yaml",
|
||||||
|
"config/preload.php",
|
||||||
|
"config/routes/framework.yaml",
|
||||||
|
"config/services.yaml",
|
||||||
|
"public/index.php",
|
||||||
|
"src/Controller/.gitignore",
|
||||||
|
"src/Kernel.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/routing": {
|
||||||
|
"version": "7.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/routing.yaml",
|
||||||
|
"config/routes.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class BackofficeConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "backoffice"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from quiz.models import Quiz
|
|
||||||
|
|
||||||
|
|
||||||
class QuizConverter:
|
|
||||||
regex = r"\d+"
|
|
||||||
|
|
||||||
def to_python(self, value: str) -> Quiz:
|
|
||||||
try:
|
|
||||||
return Quiz.objects.get(id=value)
|
|
||||||
except Quiz.DoesNotExist:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
def to_url(self, value: Quiz | int) -> str:
|
|
||||||
return str(value.id) if isinstance(value, Quiz) else value
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" data-bs-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
|
||||||
crossorigin="anonymous">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
||||||
<title>
|
|
||||||
{% block title %}
|
|
||||||
{% translate "Tijd voor de test" %}
|
|
||||||
{% endblock title %}
|
|
||||||
</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% block nav %}
|
|
||||||
{% include "backoffice/nav.html" %}
|
|
||||||
{% endblock nav %}
|
|
||||||
<main>
|
|
||||||
<div class="container">
|
|
||||||
{% include "messages.html" %}
|
|
||||||
{% block body %}
|
|
||||||
{% endblock body %}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
{% block script %}
|
|
||||||
{% endblock script %}
|
|
||||||
</html>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{% extends "backoffice/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block body %}
|
|
||||||
<h2>{% translate "Your Seasons" %}</h2>
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">{% translate "Name" %}</th>
|
|
||||||
<th scope="col">{% translate "Active Quiz" %}</th>
|
|
||||||
<th scope="col">{% translate "Season Code" %}</th>
|
|
||||||
<th scope="col">{% translate "Preregister?" %}</th>
|
|
||||||
<th scope="col">{% translate "Manage" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for season in seasons %}
|
|
||||||
<tr class="align-middle">
|
|
||||||
<td>{{ season.name }}</td>
|
|
||||||
<td>
|
|
||||||
{% if season.active_quiz %}
|
|
||||||
{{ season.active_quiz.name }}
|
|
||||||
{% else %}
|
|
||||||
{% translate "No active quiz" %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a {% if season.active_quiz %}href="{% url "enter_name" season %}"{% else %}class="disabled" {% endif %}>{{ season.season_code }}</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
disabled
|
|
||||||
{% if season.preregister_candidates %}checked{% endif %}
|
|
||||||
aria-label="Preregister Enabled">
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url "backoffice:season" season %}">{% translate "Manage" %}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
EMPTY
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock body %}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="#">Tijd voor de test</a>
|
|
||||||
<button class="navbar-toggler"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#navbarSupportedContent"
|
|
||||||
aria-controls="navbarSupportedContent"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
<li class="nav-item">
|
|
||||||
{% url "backoffice:index" as expected %}
|
|
||||||
<a class="nav-link{% if expected == request.path %} active{% endif %}"
|
|
||||||
href="{{ expected }}">Seizoenen</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="navbar-nav ml-auto mb-e mb-lg-0">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'admin:index' %}">Django Admin</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
|
||||||
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
|
||||||
<hr class="d-lg-none my-2 text-white-50">
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<form class="d-flex" action="{% url 'set_language' %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input name="next" type="hidden" value="{{ redirect_to }}" />
|
|
||||||
<select name="language" class="form-select me-2">
|
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
|
||||||
{% get_available_languages as LANGUAGES %}
|
|
||||||
{% get_language_info_list for LANGUAGES as languages %}
|
|
||||||
{% for language in languages %}
|
|
||||||
<option value="{{ language.code }}"
|
|
||||||
{% if language.code == LANGUAGE_CODE %}selected="selected"{% endif %}>
|
|
||||||
{{ language.name_local }} ({{ language.code }})
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-outline-secondary" type="submit">Go</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
{% extends "backoffice/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block body %}
|
|
||||||
<p>
|
|
||||||
<h2>{% translate "Quiz" %}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
|
|
||||||
</p>
|
|
||||||
<div id="questions">
|
|
||||||
<p>
|
|
||||||
<h4>{% translate "Questions" %}</h4>
|
|
||||||
</p>
|
|
||||||
<div class="accordion">
|
|
||||||
{% for question in quiz.questions.all %}
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header">
|
|
||||||
<button class="accordion-button collapsed"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#question-{{ forloop.counter0 }}"
|
|
||||||
aria-controls="question-{{ forloop.counter0 }}">
|
|
||||||
{% with question_error=question.errors %}
|
|
||||||
{% if question_error %}
|
|
||||||
<span data-bs-toggle="tooltip"
|
|
||||||
title="{{ question_error }}"
|
|
||||||
class="badge text-bg-danger rounded-pill me-2">!</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{{ forloop.counter }}. {{ question.question }}
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="question-{{ forloop.counter0 }}"
|
|
||||||
class="accordion-collapse collapse">
|
|
||||||
<div class="accordion-body">
|
|
||||||
{% for answer in question.answers.all %}
|
|
||||||
<li {% if answer.is_right_answer %}class="text-decoration-underline"{% endif %}>{{ answer.text }}</li>
|
|
||||||
{% empty %}
|
|
||||||
{% translate "There are no answers for this question" %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% empty %}
|
|
||||||
EMPTY
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="scores">
|
|
||||||
<p>
|
|
||||||
<h4>{% translate "Score" %}</h4>
|
|
||||||
</p>
|
|
||||||
<div class="btn-toolbar" role="toolbar">
|
|
||||||
<div class="btn-group btn-group-lg me-2">
|
|
||||||
<a class="btn btn-primary">{% translate "Start Elimination" %}</a>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group btn-group-lg">
|
|
||||||
<a class="btn btn-secondary">{% translate "Prepare Custom Elimination" %}</a>
|
|
||||||
<a class="btn btn-secondary">{% translate "Load Prepared Elimination" %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>{% translate "Number of dropouts:" %} {{ quiz.dropouts }}</p>
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">{% translate "Candidate" %}</th>
|
|
||||||
<th scope="col">{% translate "Correct Answers" %}</th>
|
|
||||||
<th scope="col">{% translate "Corrections" %}</th>
|
|
||||||
<th scope="col">{% translate "Score" %}</th>
|
|
||||||
<th scope="col">{% translate "Time" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% with result=quiz.get_score %}
|
|
||||||
{% for candidate in result %}
|
|
||||||
<tr class="table-{% if forloop.revcounter > quiz.dropouts %}success{% else %}danger{% endif %}">
|
|
||||||
<td>{{ candidate.name }}</td>
|
|
||||||
<td>{{ candidate.correct }}</td>
|
|
||||||
<td>{{ candidate.corrections }}</td>
|
|
||||||
<td>{{ candidate.score }}</td>
|
|
||||||
<td>{{ candidate.time }}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% endblock body %}
|
|
||||||
{% block script %}
|
|
||||||
<script>
|
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
|
||||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
|
||||||
</script>
|
|
||||||
{% endblock script %}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends "backoffice/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block body %}
|
|
||||||
<p>
|
|
||||||
<h2>{% translate "Season" %}: {{ season.name }}</h2>
|
|
||||||
</p>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 col-12">
|
|
||||||
<h4>{% translate "Quizzes" %}</h4>
|
|
||||||
<div class="list-group">
|
|
||||||
{% for quiz in season.quizzes.all %}
|
|
||||||
<a class="list-group-item list-group-item-action{% if season.active_quiz == quiz %} active{% endif %}"
|
|
||||||
href="{% url "backoffice:quiz" quiz %}">{{ quiz.name }}</a>
|
|
||||||
{% empty %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 col-12">
|
|
||||||
<h4>{% translate "Candidates" %}</h4>
|
|
||||||
<ul>
|
|
||||||
{% for candidate in season.candidates.all %}<li>{{ candidate.name }}</li>{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock body %}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.urls import path, register_converter
|
|
||||||
|
|
||||||
from tvdt.converters import SeasonCodeConverter
|
|
||||||
|
|
||||||
from .converters import QuizConverter
|
|
||||||
from .views import BackofficeIndexView, QuizView, SeasonView
|
|
||||||
|
|
||||||
register_converter(SeasonCodeConverter, "season")
|
|
||||||
register_converter(QuizConverter, "quiz")
|
|
||||||
|
|
||||||
app_name = "backoffice"
|
|
||||||
urlpatterns = [
|
|
||||||
path("", login_required(BackofficeIndexView.as_view()), name="index"),
|
|
||||||
path(
|
|
||||||
"<season:season>/",
|
|
||||||
login_required(SeasonView.as_view()),
|
|
||||||
name="season",
|
|
||||||
),
|
|
||||||
path("<quiz:quiz>/", login_required(QuizView.as_view()), name="quiz"),
|
|
||||||
]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .home import BackofficeIndexView
|
|
||||||
from .quiz import QuizView
|
|
||||||
from .season import SeasonView
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from django.http import HttpRequest
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class BackofficeIndexView(TemplateView):
|
|
||||||
template_name = "backoffice/index.html"
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs):
|
|
||||||
seasons = request.user.seasons.all()
|
|
||||||
return self.render_to_response({"seasons": seasons})
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from django.http import HttpRequest
|
|
||||||
from django.views import View
|
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
|
||||||
|
|
||||||
from quiz.models import Quiz
|
|
||||||
|
|
||||||
|
|
||||||
class QuizView(View, TemplateResponseMixin):
|
|
||||||
template_name = "backoffice/quiz.html"
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, quiz: Quiz, *args, **kwargs):
|
|
||||||
|
|
||||||
return self.render_to_response({"quiz": quiz})
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from django.http import HttpRequest
|
|
||||||
from django.views import View
|
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
|
||||||
|
|
||||||
from quiz.models import Season
|
|
||||||
|
|
||||||
|
|
||||||
class SeasonView(View, TemplateResponseMixin):
|
|
||||||
template_name = "backoffice/season.html"
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, season: Season, *args, **kwargs):
|
|
||||||
return self.render_to_response({"season": season})
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2024-12-11 23:22+0100\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/base.html:18
|
|
||||||
msgid "Tijd voor de test"
|
|
||||||
msgstr "Tijd voor de test"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/index.html:4
|
|
||||||
msgid "Your Seasons"
|
|
||||||
msgstr "Jouw seizoenen"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/index.html:8
|
|
||||||
msgid "Name"
|
|
||||||
msgstr "Naam"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/index.html:9
|
|
||||||
msgid "Active Quiz"
|
|
||||||
msgstr "Actieve test"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/index.html:10
|
|
||||||
msgid "Season Code"
|
|
||||||
msgstr "Seizoenscode"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/index.html:11
|
|
||||||
msgid "Preregister?"
|
|
||||||
msgstr "Voorregistreren?"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/index.html:12
|
|
||||||
#: backoffice/templates/backoffice/index.html:37
|
|
||||||
msgid "Manage"
|
|
||||||
msgstr "Beheer"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/index.html:23
|
|
||||||
msgid "No active quiz"
|
|
||||||
msgstr "Geen actieve test"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:5
|
|
||||||
msgid "Quiz"
|
|
||||||
msgstr "Test"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:9
|
|
||||||
msgid "Questions"
|
|
||||||
msgstr "Vragen"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:36
|
|
||||||
msgid "There are no answers for this question"
|
|
||||||
msgstr "Er zijn geen antwoorden voor deze vraag"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:48
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:66
|
|
||||||
msgid "Score"
|
|
||||||
msgstr "Score"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:52
|
|
||||||
msgid "Start Elimination"
|
|
||||||
msgstr "Start eliminatie"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:55
|
|
||||||
msgid "Prepare Custom Elimination"
|
|
||||||
msgstr "Aangepaste eliminatie voorbereiden"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:56
|
|
||||||
msgid "Load Prepared Elimination"
|
|
||||||
msgstr "Aangepaste eliminatie inladen"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:59
|
|
||||||
msgid "Number of dropouts:"
|
|
||||||
msgstr "Aantal afvallers:"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:63
|
|
||||||
msgid "Candidate"
|
|
||||||
msgstr "Kandidaat"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:64
|
|
||||||
msgid "Correct Answers"
|
|
||||||
msgstr "Goede antwoorden"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:65
|
|
||||||
msgid "Corrections"
|
|
||||||
msgstr "Jokers"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/quiz.html:67
|
|
||||||
msgid "Time"
|
|
||||||
msgstr "Tijd"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/season.html:5
|
|
||||||
msgid "Season"
|
|
||||||
msgstr "Seizoen"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/season.html:9
|
|
||||||
|
|
||||||
msgid "Quizzes"
|
|
||||||
msgstr "Tests"
|
|
||||||
|
|
||||||
#: backoffice/templates/backoffice/season.html:21
|
|
||||||
msgid "Candidates"
|
|
||||||
msgstr "Kandidaten"
|
|
||||||
|
|
||||||
#: tvdt/settings.py:173
|
|
||||||
msgid "Dutch"
|
|
||||||
msgstr "Nederlands"
|
|
||||||
|
|
||||||
#: tvdt/settings.py:173
|
|
||||||
msgid "English"
|
|
||||||
msgstr "Engels"
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""Django's command-line utility for administrative tasks."""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Run administrative tasks."""
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tvdt.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()
|
|
||||||
1163
tvdt/poetry.lock
generated
1163
tvdt/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
|||||||
[tool.poetry]
|
|
||||||
name = "tvdt"
|
|
||||||
package-mode = false
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.12"
|
|
||||||
Django = "^5.1.2"
|
|
||||||
django-crispy-forms = "^2.3"
|
|
||||||
crispy-bootstrap5 = "^2024.10"
|
|
||||||
django-allauth = {extras = ["socialaccount"], version = "^65.2.0"}
|
|
||||||
django-stubs = {extras = ["compatible-mypy"], version = "^5.1.0"}
|
|
||||||
environs = {extras = ["django"], version = "^11.2.1"}
|
|
||||||
psycopg2 = "^2.9.10"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
mypy = "^1.11.0"
|
|
||||||
black = "^24.10.0"
|
|
||||||
isort = "^5.13.2"
|
|
||||||
djlint = "^1.36.3"
|
|
||||||
|
|
||||||
[tool.poetry.group.prod.dependencies]
|
|
||||||
gunicorn = "^23.0.0"
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
plugins = ["mypy_django_plugin.main"]
|
|
||||||
|
|
||||||
[tool.django-stubs]
|
|
||||||
django_settings_module = "tvdt.settings"
|
|
||||||
|
|
||||||
[tool.djlint]
|
|
||||||
profile="django"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from .models import Answer, Candidate, Correction, GivenAnswer, Question, Quiz, Season
|
|
||||||
|
|
||||||
|
|
||||||
class CandidatesAdmin(admin.StackedInline):
|
|
||||||
model = Candidate
|
|
||||||
extra = 1
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Season)
|
|
||||||
class SeasonAdmin(admin.ModelAdmin):
|
|
||||||
inlines = [CandidatesAdmin]
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionInline(admin.TabularInline):
|
|
||||||
model = Question
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Quiz)
|
|
||||||
class QuizAdmin(admin.ModelAdmin):
|
|
||||||
inlines = [QuestionInline]
|
|
||||||
|
|
||||||
|
|
||||||
class AnswerInline(admin.TabularInline):
|
|
||||||
model = Answer
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Question)
|
|
||||||
class QuestionAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ["question", "quiz__season__name", "quiz__name", "_order"]
|
|
||||||
ordering = ["quiz__season", "quiz", "_order"]
|
|
||||||
inlines = [AnswerInline]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Candidate)
|
|
||||||
class CandidateAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(GivenAnswer)
|
|
||||||
class GivenAnswerAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Correction)
|
|
||||||
class CorrextionAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class QuizConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "quiz"
|
|
||||||
verbose_name = _("quiz")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
def get_theme(request) -> dict:
|
|
||||||
return {"theme": "wie_is_de_mol"}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import base64
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
from .models import Candidate, Season
|
|
||||||
|
|
||||||
|
|
||||||
class CandidateConverter:
|
|
||||||
regex = r"[A-Za-z\d]{5}\/[\w\-=]+"
|
|
||||||
|
|
||||||
def to_python(self, value: str) -> Candidate:
|
|
||||||
season_code, base64_name = value.split("/")
|
|
||||||
|
|
||||||
try:
|
|
||||||
name = base64.urlsafe_b64decode(base64_name).decode()
|
|
||||||
except binascii.Error:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
try:
|
|
||||||
season = Season.objects.get(season_code=season_code)
|
|
||||||
|
|
||||||
candidate = Candidate.objects.get(name=name, season=season)
|
|
||||||
return candidate
|
|
||||||
except [Season.DoesNotExist, Candidate.DoesNotExist]:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
def to_url(self, candidate: Candidate) -> str:
|
|
||||||
base64_candidate = base64.urlsafe_b64encode(candidate.name.encode()).decode()
|
|
||||||
return f"{candidate.season.season_code}/{base64_candidate}"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
|||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
|
|
||||||
def generate_season_code(length: int = 5) -> str:
|
|
||||||
return "".join(
|
|
||||||
random.choice(string.ascii_uppercase + string.digits) for _ in range(length)
|
|
||||||
)
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2024-12-11 23:22+0100\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
||||||
|
|
||||||
#: quiz/apps.py:8 quiz/models/correction.py:17 quiz/models/given_answer.py:19
|
|
||||||
#: quiz/models/question.py:23 quiz/models/quiz.py:60
|
|
||||||
msgid "quiz"
|
|
||||||
msgstr "test"
|
|
||||||
|
|
||||||
#: quiz/models/answer.py:10
|
|
||||||
msgid "text"
|
|
||||||
msgstr "tekst"
|
|
||||||
|
|
||||||
#: quiz/models/answer.py:15 quiz/models/question.py:18
|
|
||||||
#: quiz/models/question.py:58
|
|
||||||
msgid "question"
|
|
||||||
msgstr "vraag"
|
|
||||||
|
|
||||||
#: quiz/models/answer.py:17
|
|
||||||
msgid "is right answer"
|
|
||||||
msgstr "is goede antwoord"
|
|
||||||
|
|
||||||
#: quiz/models/answer.py:19 quiz/models/candidate.py:50
|
|
||||||
msgid "candidates"
|
|
||||||
msgstr "kandidaten"
|
|
||||||
|
|
||||||
#: quiz/models/answer.py:23 quiz/models/given_answer.py:25
|
|
||||||
msgid "answer"
|
|
||||||
msgstr "antwoord"
|
|
||||||
|
|
||||||
#: quiz/models/answer.py:24
|
|
||||||
msgid "answers"
|
|
||||||
msgstr "antwoorden"
|
|
||||||
|
|
||||||
#: quiz/models/candidate.py:18 quiz/models/quiz.py:12 quiz/models/season.py:12
|
|
||||||
msgid "name"
|
|
||||||
msgstr "naam"
|
|
||||||
|
|
||||||
#: quiz/models/candidate.py:49 quiz/models/correction.py:11
|
|
||||||
#: quiz/models/given_answer.py:11
|
|
||||||
msgid "candidate"
|
|
||||||
msgstr "kandidaat"
|
|
||||||
|
|
||||||
#: quiz/models/correction.py:19
|
|
||||||
msgid "amount"
|
|
||||||
msgstr "aantal"
|
|
||||||
|
|
||||||
#: quiz/models/correction.py:23
|
|
||||||
msgid "correction"
|
|
||||||
msgstr "joker"
|
|
||||||
|
|
||||||
#: quiz/models/correction.py:24
|
|
||||||
msgid "corrections"
|
|
||||||
msgstr "jokers"
|
|
||||||
|
|
||||||
#: quiz/models/given_answer.py:36
|
|
||||||
msgid "given answer"
|
|
||||||
msgstr "gegeven antwoord"
|
|
||||||
|
|
||||||
#: quiz/models/given_answer.py:37
|
|
||||||
msgid "given answers"
|
|
||||||
msgstr "gegeven antwoorden"
|
|
||||||
|
|
||||||
#: quiz/models/question.py:25
|
|
||||||
msgid "enabled"
|
|
||||||
msgstr "actief"
|
|
||||||
|
|
||||||
#: quiz/models/question.py:42
|
|
||||||
msgid "Error: Question has no answers"
|
|
||||||
msgstr "Fout: Raar genoeg heeft deze vraag geen antwoorden..."
|
|
||||||
|
|
||||||
#: quiz/models/question.py:47
|
|
||||||
msgid "Error: This question has no right answer!"
|
|
||||||
msgstr "Fout: Raar genoeg heeft deze vraag geen antwoorden..."
|
|
||||||
|
|
||||||
#: quiz/models/question.py:50
|
|
||||||
msgid "Warning: This question has multiple correct answers"
|
|
||||||
msgstr "Waarschuwing: Raar genoeg heeft deze vraag geen antwoorden..."
|
|
||||||
|
|
||||||
#: quiz/models/question.py:59
|
|
||||||
msgid "questions"
|
|
||||||
msgstr "vraag"
|
|
||||||
|
|
||||||
#: quiz/models/quiz.py:17 quiz/models/season.py:43
|
|
||||||
msgid "season"
|
|
||||||
msgstr "seizoen"
|
|
||||||
|
|
||||||
#: quiz/models/quiz.py:21
|
|
||||||
msgid "dropouts"
|
|
||||||
msgstr "afvallers"
|
|
||||||
|
|
||||||
#: quiz/models/quiz.py:61
|
|
||||||
msgid "quizzes"
|
|
||||||
msgstr "tests"
|
|
||||||
|
|
||||||
#: quiz/models/season.py:19
|
|
||||||
msgid "active quiz"
|
|
||||||
msgstr "actieve test"
|
|
||||||
|
|
||||||
#: quiz/models/season.py:23
|
|
||||||
msgid "season code"
|
|
||||||
msgstr "seizoencode"
|
|
||||||
|
|
||||||
#: quiz/models/season.py:26
|
|
||||||
msgid "preregister candidates"
|
|
||||||
msgstr "kandidaten voorregistreren"
|
|
||||||
|
|
||||||
#: quiz/models/season.py:30
|
|
||||||
msgid "owners"
|
|
||||||
msgstr "eigenaren"
|
|
||||||
|
|
||||||
#: quiz/models/season.py:44
|
|
||||||
msgid "seasons"
|
|
||||||
msgstr "seizoenen"
|
|
||||||
|
|
||||||
#: quiz/templates/quiz/base.html:16
|
|
||||||
msgid "Tijd voor de test"
|
|
||||||
msgstr "Tijd voor de test"
|
|
||||||
|
|
||||||
#: quiz/templates/quiz/question.html:15
|
|
||||||
msgid "Weirdly enough this question has no answers..."
|
|
||||||
msgstr "Raar genoeg heeft deze vraag geen antwoorden..."
|
|
||||||
|
|
||||||
#: quiz/views/enternameview.py:15
|
|
||||||
msgid "Name"
|
|
||||||
msgstr "Naam"
|
|
||||||
|
|
||||||
#: quiz/views/enternameview.py:28
|
|
||||||
msgid "This season has no active quiz."
|
|
||||||
msgstr "Dit seizoen heeft geen actieve test."
|
|
||||||
|
|
||||||
#: quiz/views/enternameview.py:40
|
|
||||||
msgid "Candidate does not exist"
|
|
||||||
msgstr "Kandidaat bestaat niet"
|
|
||||||
|
|
||||||
#: quiz/views/questionview.py:23
|
|
||||||
msgid "No active quiz for season"
|
|
||||||
msgstr "Geen active test voor seizoen"
|
|
||||||
|
|
||||||
#: quiz/views/questionview.py:27
|
|
||||||
msgid "Quiz done"
|
|
||||||
msgstr "Test klaar"
|
|
||||||
|
|
||||||
#: quiz/views/selectseasonview.py:30
|
|
||||||
msgid "Invalid season code"
|
|
||||||
msgstr "Ongeldige seizoencode"
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
# Generated by Django 5.1.3 on 2024-11-25 18:17
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import quiz.helpers
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = []
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Candidate",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=16, verbose_name="name")),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "candidate",
|
|
||||||
"verbose_name_plural": "candidates",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Question",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("question", models.CharField(max_length=256, verbose_name="question")),
|
|
||||||
("enabled", models.BooleanField(default=True, verbose_name="enabled")),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "question",
|
|
||||||
"verbose_name_plural": "questions",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Quiz",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=64, verbose_name="name")),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "quiz",
|
|
||||||
"verbose_name_plural": "quizzes",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Answer",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("text", models.CharField(max_length=64, verbose_name="text")),
|
|
||||||
(
|
|
||||||
"is_right_answer",
|
|
||||||
models.BooleanField(verbose_name="is right answer"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"candidates",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True, to="quiz.candidate", verbose_name="candidates"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"question",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="answers",
|
|
||||||
to="quiz.question",
|
|
||||||
verbose_name="question",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "answer",
|
|
||||||
"verbose_name_plural": "answers",
|
|
||||||
"order_with_respect_to": "question",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="question",
|
|
||||||
name="quiz",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="questions",
|
|
||||||
to="quiz.quiz",
|
|
||||||
verbose_name="quiz",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="GivenAnswer",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
|
||||||
(
|
|
||||||
"answer",
|
|
||||||
models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="quiz.answer",
|
|
||||||
verbose_name="answer",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"candidate",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="answers",
|
|
||||||
to="quiz.candidate",
|
|
||||||
verbose_name="candidate",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"quiz",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="+",
|
|
||||||
to="quiz.quiz",
|
|
||||||
verbose_name="quiz",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "given answer",
|
|
||||||
"verbose_name_plural": "given answers",
|
|
||||||
"ordering": ("quiz", "candidate"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Season",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=64, verbose_name="name")),
|
|
||||||
(
|
|
||||||
"season_code",
|
|
||||||
models.CharField(
|
|
||||||
default=quiz.helpers.generate_season_code,
|
|
||||||
max_length=5,
|
|
||||||
verbose_name="season code",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"preregister_candidates",
|
|
||||||
models.BooleanField(
|
|
||||||
default=True, verbose_name="preregister candidates"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"active_quiz",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="+",
|
|
||||||
to="quiz.quiz",
|
|
||||||
verbose_name="active quiz",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "season",
|
|
||||||
"verbose_name_plural": "seasons",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="quiz",
|
|
||||||
name="season",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="quizzes",
|
|
||||||
to="quiz.season",
|
|
||||||
verbose_name="season",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="candidate",
|
|
||||||
name="season",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="candidates",
|
|
||||||
to="quiz.season",
|
|
||||||
verbose_name="season",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterOrderWithRespectTo(
|
|
||||||
name="question",
|
|
||||||
order_with_respect_to="quiz",
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Correction",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"candidate",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="corrections_used",
|
|
||||||
to="quiz.candidate",
|
|
||||||
verbose_name="candidate",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"quiz",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="corrections_used",
|
|
||||||
to="quiz.quiz",
|
|
||||||
verbose_name="quiz",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "correction",
|
|
||||||
"verbose_name_plural": "corrections",
|
|
||||||
"unique_together": {("candidate", "quiz")},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="candidate",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["season", "name"], name="quiz_candid_season__d83118_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name="candidate",
|
|
||||||
unique_together={("season", "name")},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 5.1.3 on 2024-11-30 18:21
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("quiz", "0001_initial"),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="season",
|
|
||||||
name="owner",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
related_name="seasons",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
verbose_name="owners",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Generated by Django 5.1.3 on 2024-12-01 14:23
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("quiz", "0002_season_owner"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="correction",
|
|
||||||
name="amount",
|
|
||||||
field=models.FloatField(default=1, verbose_name="amount"),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="correction",
|
|
||||||
name="candidate",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="corrections",
|
|
||||||
to="quiz.candidate",
|
|
||||||
verbose_name="candidate",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="correction",
|
|
||||||
name="quiz",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="corrections",
|
|
||||||
to="quiz.quiz",
|
|
||||||
verbose_name="quiz",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.1.3 on 2024-12-01 16:57
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("quiz", "0003_correction_amount_alter_correction_candidate_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="quiz",
|
|
||||||
name="dropouts",
|
|
||||||
field=models.PositiveSmallIntegerField(default=1, verbose_name="dropouts"),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .answer import Answer
|
|
||||||
from .candidate import Candidate
|
|
||||||
from .correction import Correction
|
|
||||||
from .given_answer import GivenAnswer
|
|
||||||
from .question import Question
|
|
||||||
from .quiz import Quiz
|
|
||||||
from .season import Season
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from typing import final
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_stubs_ext.db.models import TypedModelMeta
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
class Answer(models.Model):
|
|
||||||
text = models.CharField(max_length=64, verbose_name=_("text"))
|
|
||||||
question = models.ForeignKey(
|
|
||||||
"Question",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="answers",
|
|
||||||
verbose_name=_("question"),
|
|
||||||
)
|
|
||||||
is_right_answer = models.BooleanField(verbose_name=_("is right answer"))
|
|
||||||
candidates = models.ManyToManyField(
|
|
||||||
"Candidate", verbose_name=_("candidates"), blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(TypedModelMeta):
|
|
||||||
verbose_name = _("answer")
|
|
||||||
verbose_name_plural = _("answers")
|
|
||||||
|
|
||||||
order_with_respect_to = "question"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
from typing import Self
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_stubs_ext.db.models import TypedModelMeta
|
|
||||||
|
|
||||||
from .given_answer import GivenAnswer
|
|
||||||
from .question import NoActiveTestForSeason, Question, QuizAlreadyFinished
|
|
||||||
|
|
||||||
|
|
||||||
class Candidate(models.Model):
|
|
||||||
season = models.ForeignKey(
|
|
||||||
"Season",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="candidates",
|
|
||||||
verbose_name="season",
|
|
||||||
)
|
|
||||||
name = models.CharField(max_length=16, verbose_name=_("name"))
|
|
||||||
|
|
||||||
def get_next_question(self, candidate: Self) -> "Question":
|
|
||||||
quiz = candidate.season.active_quiz
|
|
||||||
if quiz is None:
|
|
||||||
raise NoActiveTestForSeason()
|
|
||||||
|
|
||||||
question = (
|
|
||||||
Question.objects.filter(quiz=quiz, enabled=True)
|
|
||||||
.exclude(
|
|
||||||
id__in=GivenAnswer.objects.filter(
|
|
||||||
candidate=candidate,
|
|
||||||
quiz=quiz,
|
|
||||||
answer__isnull=False,
|
|
||||||
).values_list("answer__question_id", flat=True)
|
|
||||||
)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if question is None:
|
|
||||||
raise QuizAlreadyFinished()
|
|
||||||
|
|
||||||
return question
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.name} ({self.season})"
|
|
||||||
|
|
||||||
class Meta(TypedModelMeta):
|
|
||||||
unique_together = ["season", "name"]
|
|
||||||
indexes = [models.Index(fields=["season", "name"])]
|
|
||||||
|
|
||||||
verbose_name = _("candidate")
|
|
||||||
verbose_name_plural = _("candidates")
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_stubs_ext.db.models import TypedModelMeta
|
|
||||||
|
|
||||||
|
|
||||||
class Correction(models.Model):
|
|
||||||
candidate = models.ForeignKey(
|
|
||||||
"Candidate",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="corrections",
|
|
||||||
verbose_name=_("candidate"),
|
|
||||||
)
|
|
||||||
quiz = models.ForeignKey(
|
|
||||||
"Quiz",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="corrections",
|
|
||||||
verbose_name=_("quiz"),
|
|
||||||
)
|
|
||||||
amount = models.FloatField(verbose_name=_("amount"), default=1)
|
|
||||||
|
|
||||||
class Meta(TypedModelMeta):
|
|
||||||
unique_together = ("candidate", "quiz")
|
|
||||||
verbose_name = _("correction")
|
|
||||||
verbose_name_plural = _("corrections")
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_stubs_ext.db.models import TypedModelMeta
|
|
||||||
|
|
||||||
|
|
||||||
class GivenAnswer(models.Model):
|
|
||||||
candidate = models.ForeignKey(
|
|
||||||
"Candidate",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="answers",
|
|
||||||
verbose_name=_("candidate"),
|
|
||||||
)
|
|
||||||
|
|
||||||
quiz = models.ForeignKey(
|
|
||||||
"Quiz",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
null=False,
|
|
||||||
related_name="+",
|
|
||||||
verbose_name=_("quiz"),
|
|
||||||
)
|
|
||||||
|
|
||||||
answer = models.ForeignKey(
|
|
||||||
"Answer",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("answer"),
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.quiz} - {self.candidate.name} {self.answer}"
|
|
||||||
|
|
||||||
class Meta(TypedModelMeta):
|
|
||||||
ordering = ("quiz", "candidate")
|
|
||||||
|
|
||||||
verbose_name = _("given answer")
|
|
||||||
verbose_name_plural = _("given answers")
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.db.models import QuerySet
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_stubs_ext.db.models import TypedModelMeta
|
|
||||||
|
|
||||||
from quiz.models import Answer
|
|
||||||
|
|
||||||
|
|
||||||
class NoActiveTestForSeason(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class QuizAlreadyFinished(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Question(models.Model):
|
|
||||||
question = models.CharField(max_length=256, verbose_name=_("question"))
|
|
||||||
quiz = models.ForeignKey(
|
|
||||||
"Quiz",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="questions",
|
|
||||||
verbose_name=_("quiz"),
|
|
||||||
)
|
|
||||||
enabled = models.BooleanField(default=True, verbose_name=_("enabled"))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def order(self):
|
|
||||||
return self._order
|
|
||||||
|
|
||||||
@property
|
|
||||||
def right_answer(self) -> QuerySet[Answer]:
|
|
||||||
return self.answers.filter(is_right_answer=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_right_answer(self) -> bool:
|
|
||||||
return self.answers.filter(is_right_answer=True).count() > 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def errors(self) -> str | None:
|
|
||||||
if self.answers.count() == 0:
|
|
||||||
return _("Error: Question has no answers")
|
|
||||||
|
|
||||||
n_correct_answers = self.answers.filter(is_right_answer=True).count()
|
|
||||||
|
|
||||||
if n_correct_answers == 0:
|
|
||||||
return _("Error: This question has no right answer!")
|
|
||||||
|
|
||||||
if n_correct_answers > 1:
|
|
||||||
return _("Warning: This question has multiple correct answers")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self._order + 1}. {self.question} ({self.quiz}) ({self.answers.count()} answers, {self.answers.filter(is_right_answer=True).count()} correct)"
|
|
||||||
|
|
||||||
class Meta(TypedModelMeta):
|
|
||||||
verbose_name = _("question")
|
|
||||||
verbose_name_plural = _("questions")
|
|
||||||
|
|
||||||
order_with_respect_to = "quiz"
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.db.models import F, OuterRef, Subquery
|
|
||||||
from django.db.models.aggregates import Count, Max, Min
|
|
||||||
from django.db.models.functions import Coalesce
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_stubs_ext.db.models import TypedModelMeta
|
|
||||||
|
|
||||||
from quiz.models import Candidate, Correction
|
|
||||||
|
|
||||||
|
|
||||||
class Quiz(models.Model):
|
|
||||||
name = models.CharField(max_length=64, verbose_name=_("name"))
|
|
||||||
season = models.ForeignKey(
|
|
||||||
"Season",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="quizzes",
|
|
||||||
verbose_name=_("season"),
|
|
||||||
)
|
|
||||||
|
|
||||||
dropouts = models.PositiveSmallIntegerField(
|
|
||||||
verbose_name=_("dropouts"),
|
|
||||||
default=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_valid_quiz(self) -> bool:
|
|
||||||
return True
|
|
||||||
# Check > 0 active questions
|
|
||||||
# Check every question 1 right answer
|
|
||||||
|
|
||||||
def get_score(self):
|
|
||||||
time_query = (
|
|
||||||
Candidate.objects.filter(id=OuterRef("id"), answers__quiz=self)
|
|
||||||
.annotate(time=Max("answers__created") - Min("answers__created"))
|
|
||||||
.values("time")
|
|
||||||
)
|
|
||||||
corrections = Correction.objects.filter(
|
|
||||||
quiz=self, candidate=OuterRef("id")
|
|
||||||
).values("amount")
|
|
||||||
|
|
||||||
scores = (
|
|
||||||
Candidate.objects.filter(
|
|
||||||
answers__answer__is_right_answer=True,
|
|
||||||
answers__quiz=self,
|
|
||||||
)
|
|
||||||
.values("id", "name")
|
|
||||||
.annotate(
|
|
||||||
correct=Count("answers"),
|
|
||||||
corrections=Coalesce(Subquery(corrections), 0.0),
|
|
||||||
score=F("correct") + F("corrections"),
|
|
||||||
time=Subquery(time_query),
|
|
||||||
)
|
|
||||||
.order_by("-score", "time")
|
|
||||||
)
|
|
||||||
return scores
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.season.name} - {self.name}"
|
|
||||||
|
|
||||||
class Meta(TypedModelMeta):
|
|
||||||
verbose_name = _("quiz")
|
|
||||||
verbose_name_plural = _("quizzes")
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_stubs_ext.db.models import TypedModelMeta
|
|
||||||
|
|
||||||
from ..helpers import generate_season_code
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class Season(models.Model):
|
|
||||||
name = models.CharField(max_length=64, verbose_name=_("name"))
|
|
||||||
active_quiz = models.ForeignKey(
|
|
||||||
"Quiz",
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
verbose_name=_("active quiz"),
|
|
||||||
related_name="+",
|
|
||||||
)
|
|
||||||
season_code = models.CharField(
|
|
||||||
max_length=5, default=generate_season_code, verbose_name=_("season code")
|
|
||||||
)
|
|
||||||
preregister_candidates = models.BooleanField(
|
|
||||||
default=True, verbose_name=_("preregister candidates")
|
|
||||||
)
|
|
||||||
owner = models.ManyToManyField(
|
|
||||||
User,
|
|
||||||
verbose_name=_("owners"),
|
|
||||||
related_name="seasons",
|
|
||||||
)
|
|
||||||
|
|
||||||
def renew_season_code(self) -> str:
|
|
||||||
self.season_code = generate_season_code()
|
|
||||||
self.save()
|
|
||||||
return self.season_code
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta(TypedModelMeta):
|
|
||||||
verbose_name = _("season")
|
|
||||||
verbose_name_plural = _("seasons")
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 322 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 397 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 402 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 164 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 496 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 445 KiB |
@@ -1,51 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" data-bs-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
|
||||||
crossorigin="anonymous">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<title>
|
|
||||||
{% block title %}
|
|
||||||
{% translate "Tijd voor de test" %}
|
|
||||||
{% endblock title %}
|
|
||||||
</title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
{% with "quiz/"|add:theme|add:"/background.png" as background %}
|
|
||||||
background-image: url("{% static background %}");
|
|
||||||
{% endwith %}
|
|
||||||
background-position: center center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asteriskField {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div class="container">
|
|
||||||
{% include "messages.html" %}
|
|
||||||
{% block body %}
|
|
||||||
{% endblock body %}
|
|
||||||
</div>
|
|
||||||
{% block script %}
|
|
||||||
{% endblock script %}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{% extends "quiz/base.html" %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% block body %}
|
|
||||||
{% crispy form %}
|
|
||||||
{% endblock body %}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{% extends "quiz/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block body %}
|
|
||||||
<h2>{{ question.question }}</h2>
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% for answer in question.answers.all %}
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-outline-success"
|
|
||||||
type="submit"
|
|
||||||
name="answer"
|
|
||||||
value="{{ answer.id }}">{{ answer.text }}</button>
|
|
||||||
</div>
|
|
||||||
{% empty %}
|
|
||||||
{% translate "Weirdly enough this question has no answers..." %}
|
|
||||||
{% endfor %}
|
|
||||||
</form>
|
|
||||||
{% endblock body %}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{% extends "quiz/base.html" %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block body %}
|
|
||||||
{% crispy form %}
|
|
||||||
{% endblock body %}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from django.urls import path, register_converter
|
|
||||||
|
|
||||||
from tvdt.converters import SeasonCodeConverter
|
|
||||||
|
|
||||||
from .converters import CandidateConverter
|
|
||||||
from .views import SelectSeasonView
|
|
||||||
from .views.enternameview import EnterNameView
|
|
||||||
from .views.questionview import QuestionView
|
|
||||||
|
|
||||||
register_converter(SeasonCodeConverter, "season")
|
|
||||||
register_converter(CandidateConverter, "candidate")
|
|
||||||
urlpatterns = [
|
|
||||||
path("", SelectSeasonView.as_view(), name="index"),
|
|
||||||
path("<season:season>/", EnterNameView.as_view(), name="enter_name"),
|
|
||||||
path("<candidate:candidate>/", QuestionView.as_view(), name="question"),
|
|
||||||
# path("<>")
|
|
||||||
]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .selectseasonview import SelectSeasonView
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from crispy_forms.helper import FormHelper
|
|
||||||
from django import forms
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.utils.translation import gettext_lazy
|
|
||||||
from django.views import View
|
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
|
||||||
|
|
||||||
from ..models import Candidate, Season
|
|
||||||
|
|
||||||
|
|
||||||
class EnterNameForm(forms.Form):
|
|
||||||
name = forms.CharField(label=_("Name"))
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.helper = FormHelper()
|
|
||||||
|
|
||||||
|
|
||||||
class EnterNameView(View, TemplateResponseMixin):
|
|
||||||
template_name = "quiz/enter_name.html"
|
|
||||||
forms_class = EnterNameForm
|
|
||||||
|
|
||||||
def get(self, request, season: Season, *args, **kwargs):
|
|
||||||
if season.active_quiz == None:
|
|
||||||
messages.info(request, _("This season has no active quiz."))
|
|
||||||
return redirect("home")
|
|
||||||
|
|
||||||
return self.render_to_response({"form": self.forms_class()})
|
|
||||||
|
|
||||||
def post(self, request, season: Season, *args, **kwargs):
|
|
||||||
name = request.POST.get("name")
|
|
||||||
|
|
||||||
try:
|
|
||||||
candidate = Candidate.objects.get(season=season, name__iexact=name)
|
|
||||||
except Candidate.DoesNotExist:
|
|
||||||
if season.preregister_candidates:
|
|
||||||
messages.warning(request, _("Candidate does not exist"))
|
|
||||||
|
|
||||||
return redirect(reverse("quiz", kwargs={"season": season}))
|
|
||||||
|
|
||||||
candidate = Candidate.objects.create(season=season, name=name)
|
|
||||||
|
|
||||||
return redirect(
|
|
||||||
reverse(
|
|
||||||
"question",
|
|
||||||
kwargs={"candidate": candidate},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
from django.contrib import messages
|
|
||||||
from django.core.exceptions import BadRequest
|
|
||||||
from django.http import Http404, HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views import View
|
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
|
||||||
|
|
||||||
from ..models import Answer, Candidate, GivenAnswer
|
|
||||||
from ..models.question import NoActiveTestForSeason, QuizAlreadyFinished
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionView(View, TemplateResponseMixin):
|
|
||||||
template_name = "quiz/question.html"
|
|
||||||
|
|
||||||
def get(
|
|
||||||
self, request: HttpRequest, candidate: Candidate, *args, **kwargs
|
|
||||||
) -> HttpResponse:
|
|
||||||
try:
|
|
||||||
question = candidate.get_next_question(candidate)
|
|
||||||
except NoActiveTestForSeason:
|
|
||||||
messages.error(request, _("No active quiz for season"))
|
|
||||||
return redirect("home")
|
|
||||||
except QuizAlreadyFinished:
|
|
||||||
if not kwargs.get("from_post"):
|
|
||||||
messages.error(request, _("Quiz done"))
|
|
||||||
|
|
||||||
return redirect(reverse("enter_name", kwargs={"season": candidate.season}))
|
|
||||||
|
|
||||||
# TODO: On first question -> record time
|
|
||||||
if (
|
|
||||||
GivenAnswer.objects.filter(
|
|
||||||
candidate=candidate, quiz=candidate.season.active_quiz
|
|
||||||
).count()
|
|
||||||
== 0
|
|
||||||
):
|
|
||||||
GivenAnswer.objects.create(
|
|
||||||
candidate=candidate, quiz=question.quiz, answer=None
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.render_to_response({"candidate": candidate, "question": question})
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, candidate: Candidate, *args, **kwargs):
|
|
||||||
answer_id = request.POST.get("answer")
|
|
||||||
if answer_id == None:
|
|
||||||
raise BadRequest
|
|
||||||
|
|
||||||
try:
|
|
||||||
answer = Answer.objects.get(id=answer_id)
|
|
||||||
except Answer.DoesNotExist:
|
|
||||||
raise BadRequest
|
|
||||||
|
|
||||||
GivenAnswer.objects.create(
|
|
||||||
candidate=candidate,
|
|
||||||
quiz=answer.question.quiz,
|
|
||||||
answer=answer,
|
|
||||||
)
|
|
||||||
return self.get(request, candidate, from_post=True)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
from crispy_forms.helper import FormHelper
|
|
||||||
from django import forms
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.http import Http404
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
from mypy.dmypy.client import request
|
|
||||||
|
|
||||||
from ..models import Season
|
|
||||||
|
|
||||||
|
|
||||||
class SelectSeasonForm(forms.Form):
|
|
||||||
code = forms.CharField(max_length=5)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.helper = FormHelper()
|
|
||||||
|
|
||||||
|
|
||||||
class SelectSeasonView(FormView):
|
|
||||||
form_class = SelectSeasonForm
|
|
||||||
template_name = "quiz/select_season.html"
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
try:
|
|
||||||
season = Season.objects.get(season_code=form.cleaned_data["code"].upper())
|
|
||||||
except Season.DoesNotExist:
|
|
||||||
messages.warning(self.request, _("Invalid season code"))
|
|
||||||
return redirect("home")
|
|
||||||
|
|
||||||
from environs import Env
|
|
||||||
|
|
||||||
env = Env()
|
|
||||||
env.read_env()
|
|
||||||
print(env.dump())
|
|
||||||
|
|
||||||
return redirect(reverse("enter_name", kwargs={"season": season}))
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{% if messages %}
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert alert-dismissible fade show {{ message.tags }}"
|
|
||||||
role="alert">
|
|
||||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.DEBUG %}<strong>Debug:</strong>{% endif %}
|
|
||||||
{{ message }}
|
|
||||||
<button type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="alert"
|
|
||||||
aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
ASGI config for tvdt project.
|
|
||||||
|
|
||||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tvdt.settings")
|
|
||||||
|
|
||||||
application = get_asgi_application()
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from quiz.models import Season
|
|
||||||
|
|
||||||
|
|
||||||
class SeasonCodeConverter:
|
|
||||||
regex = r"[A-Za-z\d]{5}"
|
|
||||||
|
|
||||||
def to_python(self, value: str) -> Season:
|
|
||||||
try:
|
|
||||||
return Season.objects.get(season_code=value.upper())
|
|
||||||
except Season.DoesNotExist:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
def to_url(self, value: Season) -> str:
|
|
||||||
return value.season_code
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
"""
|
|
||||||
Django settings for tvdt project.
|
|
||||||
|
|
||||||
Generated by 'django-backoffice startproject' using Django 5.1.2.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.1/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from environs import Env
|
|
||||||
|
|
||||||
env = Env()
|
|
||||||
env.read_env()
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = env.str("SECRET_KEY")
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
# Override in .env for local development
|
|
||||||
DEBUG = env.bool("DEBUG", default=False)
|
|
||||||
|
|
||||||
ALLOWED_HOSTS: list[str] = ["*"]
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
"quiz.apps.QuizConfig",
|
|
||||||
"backoffice.apps.BackofficeConfig",
|
|
||||||
"django.contrib.admin",
|
|
||||||
"django.contrib.auth",
|
|
||||||
"django.contrib.contenttypes",
|
|
||||||
"django.contrib.sessions",
|
|
||||||
"django.contrib.messages",
|
|
||||||
"django.contrib.staticfiles",
|
|
||||||
]
|
|
||||||
|
|
||||||
# crispy
|
|
||||||
INSTALLED_APPS += [
|
|
||||||
"crispy_forms",
|
|
||||||
"crispy_bootstrap5",
|
|
||||||
]
|
|
||||||
|
|
||||||
# allauth
|
|
||||||
INSTALLED_APPS += [
|
|
||||||
"allauth",
|
|
||||||
"allauth.account",
|
|
||||||
"allauth.socialaccount",
|
|
||||||
"allauth.socialaccount.providers.google",
|
|
||||||
"allauth.socialaccount.providers.github",
|
|
||||||
]
|
|
||||||
|
|
||||||
SOCIALACCOUNT_PROVIDERS = {
|
|
||||||
"google": {
|
|
||||||
"APP": {
|
|
||||||
"client_id": env.str("GOOGLE_CLIENT_ID"),
|
|
||||||
"secret": env.str("GOOGLE_SECRET"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"github": {
|
|
||||||
"VERIFIED_EMAIL": True,
|
|
||||||
"APP": {
|
|
||||||
"client_id": env.str("GITHUB_CLIENT_ID"),
|
|
||||||
"secret": env.str("GITHUB_SECRET"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
|
||||||
|
|
||||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
"allauth.account.middleware.AccountMiddleware",
|
|
||||||
"django.middleware.security.SecurityMiddleware",
|
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
|
||||||
"django.middleware.common.CommonMiddleware",
|
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = "tvdt.urls"
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
||||||
"DIRS": [
|
|
||||||
BASE_DIR / "templates",
|
|
||||||
],
|
|
||||||
"APP_DIRS": True,
|
|
||||||
"OPTIONS": {
|
|
||||||
"context_processors": [
|
|
||||||
"django.template.context_processors.debug",
|
|
||||||
"django.template.context_processors.request",
|
|
||||||
"django.contrib.auth.context_processors.auth",
|
|
||||||
"django.contrib.messages.context_processors.messages",
|
|
||||||
"quiz.context_processors.get_theme",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
|
||||||
# Needed to login by username in Django backoffice, regardless of `allauth`
|
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
|
||||||
# `allauth` specific authentication methods, such as login by email
|
|
||||||
"allauth.account.auth_backends.AuthenticationBackend",
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = "tvdt.wsgi.application"
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {"default": env.dj_db_url("DATABASE_URL")}
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = (
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if not DEBUG
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
|
|
||||||
MESSAGE_TAGS = {
|
|
||||||
messages.DEBUG: "alert-info",
|
|
||||||
messages.INFO: "alert-info",
|
|
||||||
messages.SUCCESS: "alert-success",
|
|
||||||
messages.WARNING: "alert-warning",
|
|
||||||
messages.ERROR: "alert-danger",
|
|
||||||
100: "alert-primary",
|
|
||||||
110: "alert-secondary",
|
|
||||||
120: "alert-light",
|
|
||||||
130: "alert-dark",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = "nl"
|
|
||||||
LANGUAGES = [("nl", _("Dutch")), ("en", _("English"))]
|
|
||||||
|
|
||||||
TIME_ZONE = "Europe/Amsterdam"
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
LOCALE_PATHS = [BASE_DIR / "locale"]
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
|
||||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
|
||||||
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
||||||
|
|
||||||
# email is the new username
|
|
||||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
|
||||||
ACCOUNT_EMAIL_REQUIRED = True
|
|
||||||
ACCOUNT_USERNAME_REQUIRED = False
|
|
||||||
ACCOUNT_AUTHENTICATION_METHOD = "email"
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
"""
|
|
||||||
URL configuration for tvdt project.
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.staticfiles.urls import urlpatterns as static_urlpatterns
|
|
||||||
from django.urls import include, path
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("", include("quiz.urls")),
|
|
||||||
path("backoffice/", include("backoffice.urls")),
|
|
||||||
path("admin/", admin.site.urls),
|
|
||||||
path("accounts/", include("allauth.urls")),
|
|
||||||
path("i18n/", include("django.conf.urls.i18n")),
|
|
||||||
]
|
|
||||||
urlpatterns += static_urlpatterns
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
WSGI config for tvdt 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/5.1/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tvdt.settings")
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
||||||
Reference in New Issue
Block a user