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)
|
||||
### https://raw.github.com/github/gitignore/76739a38b56907118c5a880d63250c99d5690a5a/Python.gitignore
|
||||
### https://raw.github.com/github/gitignore/6eeebe6f49678aacd8311ce079842c971b3ebe96/Symfony.gitignore
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
# Cache and logs (Symfony2)
|
||||
/app/cache/*
|
||||
/app/logs/*
|
||||
!app/cache/.gitkeep
|
||||
!app/logs/.gitkeep
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
# Email spool folder
|
||||
/app/spool/*
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# Cache, session files and logs (Symfony3)
|
||||
/var/cache/*
|
||||
/var/logs/*
|
||||
/var/sessions/*
|
||||
!var/cache/.gitkeep
|
||||
!var/logs/.gitkeep
|
||||
!var/sessions/.gitkeep
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# Logs (Symfony4)
|
||||
/var/log/*
|
||||
!var/log/.gitkeep
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# Parameters
|
||||
/app/config/parameters.yml
|
||||
/app/config/parameters.ini
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
# Managed by Composer
|
||||
/app/bootstrap.php.cache
|
||||
/var/bootstrap.php.cache
|
||||
/bin/*
|
||||
!bin/console
|
||||
!bin/symfony_requirements
|
||||
/vendor/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
# Assets and user uploads
|
||||
/web/bundles/
|
||||
/web/uploads/
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
# PHPUnit
|
||||
/app/phpunit.xml
|
||||
/phpunit.xml
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# Build data
|
||||
/build/
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
# Composer PHAR
|
||||
/composer.phar
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
# Backup entities generated with doctrine:generate:entities command
|
||||
**/Entity/*~
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
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/
|
||||
# Embedded web-server pid file
|
||||
/.web-server-pid
|
||||
### 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
|
||||
.DS_Store
|
||||
@@ -197,3 +83,13 @@ Icon
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.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:
|
||||
app:
|
||||
image: tvdt/app:prod
|
||||
php:
|
||||
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:
|
||||
app:
|
||||
build:
|
||||
dockerfile: containers/python/Containerfile.dev
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
php:
|
||||
image: ${IMAGES_PREFIX:-}app-php
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: postgres://tvdt:tvdt@db:5432/tvdt
|
||||
DEBUG: true
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:17.2
|
||||
environment:
|
||||
POSTGRES_PASSWORD: tvdt
|
||||
POSTGRES_USER: tvdt
|
||||
POSTGRES_DB: tvdt
|
||||
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
|
||||
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
|
||||
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
|
||||
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure}
|
||||
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
# The two next lines can be removed after initial installation
|
||||
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
|
||||
STABILITY: ${STABILITY:-stable}
|
||||
volumes:
|
||||
- data:/var/lib/postgresql/data
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
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:
|
||||
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