Add basic quiz functionality

This commit is contained in:
2024-11-23 22:25:24 +01:00
parent 6bf0a56b88
commit 27b8c40c1c
40 changed files with 2471 additions and 53 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -37,3 +37,8 @@ meegenomen in de berekening van de slechtste speler.
TBD TBD
## Nice to haves
- Optie voor antwoord geven in twee klikken (selecteren en volgende).

View File

@@ -0,0 +1,27 @@
# 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-10-20 00:06+0200\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"
#: tvdt/settings.py:109
msgid "Dutch"
msgstr "Nederlands"
#: tvdt/settings.py:109
msgid "English"
msgstr "Engels"

View File

@@ -4,7 +4,7 @@ import os
import sys import sys
def main(): def main() -> None:
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tvdt.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tvdt.settings")
try: try:

171
tvdt/poetry.lock generated
View File

@@ -83,15 +83,51 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "crispy-bootstrap5"
version = "2024.10"
description = "Bootstrap5 template pack for django-crispy-forms"
optional = false
python-versions = ">=3.8"
files = [
{file = "crispy_bootstrap5-2024.10-py3-none-any.whl", hash = "sha256:59e91dac5e45a8c954af3fbcaa6804cd5aef4402f027af2f99a352b096c4016f"},
{file = "crispy_bootstrap5-2024.10.tar.gz", hash = "sha256:55b442fe675dd95ad280123c7fe464f454186e90b8e5642e751f436c87627c44"},
]
[package.dependencies]
django = ">=4.2"
django-crispy-forms = ">=2.3"
[package.extras]
test = ["pytest", "pytest-django"]
[[package]]
name = "crispy-tailwind"
version = "1.0.3"
description = "Tailwind CSS for Django Crispy Forms"
optional = false
python-versions = ">=3.8"
files = [
{file = "crispy-tailwind-1.0.3.tar.gz", hash = "sha256:2bc9f616d406e4b003f25d46fcb0079f1c2522719d97adb107667271d849459a"},
{file = "crispy_tailwind-1.0.3-py3-none-any.whl", hash = "sha256:31427f66b1c4fd0d6fb040f4197cfb97d104cdbe7641ea2dea940c0057c4db4b"},
]
[package.dependencies]
django = ">=4.2"
django-crispy-forms = ">=2.0"
[package.extras]
test = ["pytest", "pytest-django"]
[[package]] [[package]]
name = "django" name = "django"
version = "5.1.2" version = "5.1.3"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
files = [ files = [
{file = "Django-5.1.2-py3-none-any.whl", hash = "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed"}, {file = "Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818"},
{file = "Django-5.1.2.tar.gz", hash = "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0"}, {file = "Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a"},
] ]
[package.dependencies] [package.dependencies]
@@ -103,39 +139,74 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"] argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"] bcrypt = ["bcrypt"]
[[package]]
name = "django-allauth"
version = "65.2.0"
description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication."
optional = false
python-versions = ">=3.8"
files = [
{file = "django_allauth-65.2.0.tar.gz", hash = "sha256:0a3d7baf7beefd6fe8027316302c26ece7433cf4331a3b245d15fc9a7be68b6f"},
]
[package.dependencies]
asgiref = ">=3.8.1"
Django = ">=4.2.16"
[package.extras]
mfa = ["fido2 (>=1.1.2)", "qrcode (>=7.0.0)"]
openid = ["python3-openid (>=3.0.8)"]
saml = ["python3-saml (>=1.15.0,<2.0.0)"]
socialaccount = ["pyjwt[crypto] (>=1.7)", "requests (>=2.0.0)", "requests-oauthlib (>=0.3.0)"]
steam = ["python3-openid (>=3.0.8)"]
[[package]]
name = "django-crispy-forms"
version = "2.3"
description = "Best way to have Django DRY forms"
optional = false
python-versions = ">=3.8"
files = [
{file = "django_crispy_forms-2.3-py3-none-any.whl", hash = "sha256:efc4c31e5202bbec6af70d383a35e12fc80ea769d464fb0e7fe21768bb138a20"},
{file = "django_crispy_forms-2.3.tar.gz", hash = "sha256:2db17ae08527201be1273f0df789e5f92819e23dd28fec69cffba7f3762e1a38"},
]
[package.dependencies]
django = ">=4.2"
[[package]] [[package]]
name = "django-stubs" name = "django-stubs"
version = "5.1.0" version = "5.1.1"
description = "Mypy stubs for Django" description = "Mypy stubs for Django"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "django_stubs-5.1.0-py3-none-any.whl", hash = "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40"}, {file = "django_stubs-5.1.1-py3-none-any.whl", hash = "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac"},
{file = "django_stubs-5.1.0.tar.gz", hash = "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5"}, {file = "django_stubs-5.1.1.tar.gz", hash = "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b"},
] ]
[package.dependencies] [package.dependencies]
asgiref = "*" asgiref = "*"
django = "*" django = "*"
django-stubs-ext = ">=5.1.0" django-stubs-ext = ">=5.1.1"
mypy = {version = ">=1.11.0,<1.12.0", optional = true, markers = "extra == \"compatible-mypy\""} mypy = {version = ">=1.12,<1.14", optional = true, markers = "extra == \"compatible-mypy\""}
types-PyYAML = "*" types-PyYAML = "*"
typing-extensions = ">=4.11.0" typing-extensions = ">=4.11.0"
[package.extras] [package.extras]
compatible-mypy = ["mypy (>=1.11.0,<1.12.0)"] compatible-mypy = ["mypy (>=1.12,<1.14)"]
oracle = ["oracledb"] oracle = ["oracledb"]
redis = ["redis"] redis = ["redis"]
[[package]] [[package]]
name = "django-stubs-ext" name = "django-stubs-ext"
version = "5.1.0" version = "5.1.1"
description = "Monkey-patching and extensions for django-stubs" description = "Monkey-patching and extensions for django-stubs"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "django_stubs_ext-5.1.0-py3-none-any.whl", hash = "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d"}, {file = "django_stubs_ext-5.1.1-py3-none-any.whl", hash = "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c"},
{file = "django_stubs_ext-5.1.0.tar.gz", hash = "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926"}, {file = "django_stubs_ext-5.1.1.tar.gz", hash = "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c"},
] ]
[package.dependencies] [package.dependencies]
@@ -158,38 +229,43 @@ colors = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.11.2" version = "1.13.0"
description = "Optional static typing for Python" description = "Optional static typing for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
{file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
{file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
{file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
{file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
{file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
{file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
{file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
{file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
{file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
{file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
{file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
{file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
{file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
{file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
{file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
{file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
{file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
{file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
{file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
{file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
{file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
{file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
{file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
{file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
{file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
{file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
{file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
{file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
{file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
{file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
{file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
] ]
[package.dependencies] [package.dependencies]
@@ -198,6 +274,7 @@ typing-extensions = ">=4.6.0"
[package.extras] [package.extras]
dmypy = ["psutil (>=4.0)"] dmypy = ["psutil (>=4.0)"]
faster-cache = ["orjson"]
install-types = ["pip"] install-types = ["pip"]
mypyc = ["setuptools (>=50)"] mypyc = ["setuptools (>=50)"]
reports = ["lxml"] reports = ["lxml"]
@@ -215,13 +292,13 @@ files = [
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.1" version = "24.2"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
] ]
[[package]] [[package]]
@@ -253,13 +330,13 @@ type = ["mypy (>=1.11.2)"]
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.1" version = "0.5.2"
description = "A non-validating SQL parser." description = "A non-validating SQL parser."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"},
{file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"},
] ]
[package.extras] [package.extras]
@@ -302,4 +379,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "45eb74e8d8826f72371f13c7696dafb1aaebe2a63097f393341d32bec0e7db4e" content-hash = "95199cfe52c42cc9e58c63f0f09684bff9a129c583fbbeb74e9c2ffa9c531813"

View File

@@ -5,6 +5,10 @@ package-mode = false
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12" python = "^3.12"
Django = "^5.1.2" Django = "^5.1.2"
django-crispy-forms = "^2.3"
crispy-tailwind = "^1.0.3"
crispy-bootstrap5 = "^2024.10"
django-allauth = "^65.1.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
mypy = "^1.11.0" mypy = "^1.11.0"
@@ -12,6 +16,9 @@ black = "^24.10.0"
isort = "^5.13.2" isort = "^5.13.2"
django-stubs = {extras = ["compatible-mypy"], version = "^5.1.0"} django-stubs = {extras = ["compatible-mypy"], version = "^5.1.0"}
[tool.isort]
profile = "black"
[tool.mypy] [tool.mypy]
plugins = ["mypy_django_plugin.main"] plugins = ["mypy_django_plugin.main"]

0
tvdt/quiz/__init__.py Normal file
View File

43
tvdt/quiz/admin.py Normal file
View File

@@ -0,0 +1,43 @@
from django.contrib import admin
from .models import Question, Answer, Candidate, Quiz, Season, GivenAnswer
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):
inlines = [AnswerInline]
@admin.register(Candidate)
class CandidateAdmin(admin.ModelAdmin):
pass
@admin.register(GivenAnswer)
class GivenAnswerAdmin(admin.ModelAdmin):
pass

8
tvdt/quiz/apps.py Normal file
View File

@@ -0,0 +1,8 @@
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")

41
tvdt/quiz/converters.py Normal file
View File

@@ -0,0 +1,41 @@
import base64
import binascii
from .models import Season, Candidate
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
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

8
tvdt/quiz/helpers.py Normal file
View File

@@ -0,0 +1,8 @@
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)
)

View File

@@ -0,0 +1,122 @@
# 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-10-20 00:23+0200\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/models/answer.py:10
msgid "text"
msgstr "tekst"
#: quiz/models/answer.py:15 quiz/models/given_answer.py:21
#: quiz/models/question.py:9 quiz/models/question.py:21
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:25
msgid "candidates"
msgstr "kandidaten"
#: quiz/models/answer.py:23 quiz/models/given_answer.py:24
msgid "answer"
msgstr "antwoord"
#: quiz/models/answer.py:24
msgid "answers"
msgstr "antwoorden"
#: quiz/models/candidate.py:15 quiz/models/quiz.py:9 quiz/models/season.py:12
msgid "name"
msgstr "naam"
#: quiz/models/candidate.py:24 quiz/models/correction.py:14
#: quiz/models/given_answer.py:15 quiz/models/quiz_time.py:11
msgid "candidate"
msgstr "kandidaat"
#: quiz/models/correction.py:20 quiz/models/question.py:11
#: quiz/models/quiz.py:26 quiz/models/quiz_time.py:13
msgid "quiz"
msgstr "test"
#: quiz/models/correction.py:25
msgid "correction"
msgstr "joker"
#: quiz/models/correction.py:26
msgid "corrections"
msgstr "jokers"
#: quiz/models/given_answer.py:30
msgid "given answer"
msgstr "gegeven antwoord"
#: quiz/models/given_answer.py:31
msgid "given answers"
msgstr "gegeven antwoorden"
#: quiz/models/question.py:13
msgid "number"
msgstr "nummer"
#: quiz/models/question.py:14
msgid "enabled"
msgstr "ingeschakeld"
#: quiz/models/question.py:22
msgid "questions"
msgstr "vraag"
#: quiz/models/quiz.py:14 quiz/models/season.py:38
msgid "season"
msgstr "seizoen"
#: quiz/models/quiz.py:27
msgid "quizzes"
msgstr "tests"
#: quiz/models/quiz_time.py:14
msgid "seconds"
msgstr "seconden"
#: quiz/models/quiz_time.py:17
msgid "quiz time"
msgstr "testtijd"
#: quiz/models/quiz_time.py:18
msgid "quiz times"
msgstr "testtijden"
#: 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:39
msgid "seasons"
msgstr "seizoenen"

View File

@@ -0,0 +1,311 @@
# Generated by Django 5.1.2 on 2024-10-19 21:54
import django.db.models.deletion
import quiz.helpers
from django.db import migrations, models
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="naam")),
],
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")),
("number", models.PositiveSmallIntegerField(verbose_name="number")),
("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="naam")),
],
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(
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",
},
),
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="QuizTime",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("seconds", models.PositiveIntegerField(verbose_name="seconds")),
(
"candidate",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="quiz.candidate",
verbose_name="candidate",
),
),
(
"quiz",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="quiz.quiz",
verbose_name="quiz",
),
),
],
options={
"verbose_name": "quiz time",
"verbose_name_plural": "quiz times",
},
),
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="naam")),
(
"season_code",
models.CharField(
default=quiz.helpers.generate_season_code,
max_length=5,
verbose_name="season code",
),
),
(
"active_quiz",
models.ForeignKey(
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.CreateModel(
name="GivenAnswer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"answer",
models.ForeignKey(
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",
),
),
(
"question",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="given_answers",
to="quiz.question",
verbose_name="question",
),
),
],
options={
"verbose_name": "given answer",
"verbose_name_plural": "given answers",
"unique_together": {("candidate", "question")},
},
),
migrations.AlterUniqueTogether(
name="question",
unique_together={("quiz", "number")},
),
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")},
),
]

View File

@@ -0,0 +1,56 @@
# Generated by Django 5.1.2 on 2024-10-19 22:17
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="season",
name="preregister_candidates",
field=models.BooleanField(
default=True, verbose_name="preregister candidates"
),
),
migrations.AlterField(
model_name="answer",
name="candidates",
field=models.ManyToManyField(
blank=True, to="quiz.candidate", verbose_name="candidates"
),
),
migrations.AlterField(
model_name="candidate",
name="name",
field=models.CharField(max_length=16, verbose_name="name"),
),
migrations.AlterField(
model_name="quiz",
name="name",
field=models.CharField(max_length=64, verbose_name="name"),
),
migrations.AlterField(
model_name="season",
name="active_quiz",
field=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",
),
),
migrations.AlterField(
model_name="season",
name="name",
field=models.CharField(max_length=64, verbose_name="name"),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.1.2 on 2024-11-03 19:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0002_season_preregister_candidates_and_more"),
]
operations = [
migrations.AddField(
model_name="givenanswer",
name="created",
field=models.DateTimeField(auto_now_add=True, default=None),
preserve_default=False,
),
migrations.AlterField(
model_name="givenanswer",
name="question",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="given_answers",
to="quiz.question",
verbose_name="question",
),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.3 on 2024-11-23 16:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("quiz", "0003_givenanswer_created_alter_givenanswer_question"),
]
operations = [
migrations.AlterUniqueTogether(
name="question",
unique_together=set(),
),
migrations.AlterOrderWithRespectTo(
name="question",
order_with_respect_to="quiz",
),
migrations.AlterOrderWithRespectTo(
name="answer",
order_with_respect_to="question",
),
migrations.RemoveField(
model_name="question",
name="number",
),
]

View File

View File

@@ -0,0 +1,8 @@
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 .quiz_time import QuizTime
from .season import Season

View File

@@ -0,0 +1,23 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
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"

View File

@@ -0,0 +1,48 @@
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, QuizAlreadyFinished, Question
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)
.exclude(
id__in=GivenAnswer.objects.filter(
candidate=candidate, question__quiz=quiz
).values_list("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")

View File

@@ -0,0 +1,26 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
from .candidate import Candidate
from .quiz import Quiz
class Correction(models.Model):
candidate = models.ForeignKey(
"Candidate",
on_delete=models.CASCADE,
related_name="corrections_used",
verbose_name=_("candidate"),
)
quiz = models.ForeignKey(
"Quiz",
on_delete=models.CASCADE,
related_name="corrections_used",
verbose_name=_("quiz"),
)
class Meta(TypedModelMeta):
unique_together = ("candidate", "quiz")
verbose_name = _("correction")
verbose_name_plural = _("corrections")

View File

@@ -0,0 +1,29 @@
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"),
)
question = models.ForeignKey(
"Question",
on_delete=models.CASCADE,
null=True,
related_name="given_answers",
verbose_name=_("question"),
)
answer = models.ForeignKey(
"Answer", on_delete=models.CASCADE, verbose_name=_("answer")
)
created = models.DateTimeField(auto_now_add=True)
class Meta(TypedModelMeta):
unique_together = ["candidate", "question"]
verbose_name = _("given answer")
verbose_name_plural = _("given answers")

View File

@@ -0,0 +1,31 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
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"))
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"

25
tvdt/quiz/models/quiz.py Normal file
View File

@@ -0,0 +1,25 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
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"),
)
def is_valid_quiz(self) -> bool:
pass
# Check > 0 active questions
# Check every question 1 right answer
def __str__(self) -> str:
return f"{self.season.name} - {self.name}"
class Meta(TypedModelMeta):
verbose_name = _("quiz")
verbose_name_plural = _("quizzes")

View File

@@ -0,0 +1,15 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
class QuizTime(models.Model):
candidate = models.ForeignKey(
"Candidate", on_delete=models.CASCADE, verbose_name=_("candidate")
)
quiz = models.ForeignKey("Quiz", on_delete=models.CASCADE, verbose_name=_("quiz"))
seconds = models.PositiveIntegerField(verbose_name=_("seconds"))
class Meta(TypedModelMeta):
verbose_name = _("quiz time")
verbose_name_plural = _("quiz times")

View File

@@ -0,0 +1,39 @@
import random
import string
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
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")
)
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.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -0,0 +1,33 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{# <script src="https://cdn.tailwindcss.com"></script>#}
<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 %}{% endblock %}</title>
<style>
html, body {
height: 100%;
background-image: url("{% static "quiz/background.png" %}");
background-position: center center;
background-repeat: no-repeat;
background-color: black;
display: grid;
align-items: center;
justify-self: center;
color: white;
}
</style>
</head>
<body>
<div class="container">
{% block body %}{% endblock %}
</div>
{% block script %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,9 @@
{% extends "quiz/base.html" %}
{% load crispy_forms_tags %}
{% block body %}
<p>{{ season.name }} ({{ season.season_code }})</p>
{% crispy form %}
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% 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 %}

View File

@@ -0,0 +1,7 @@
{% extends 'quiz/base.html' %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block title %}{% translate "Tijd voor de test" %}{% endblock %}
{% block body %}
{% crispy form %}
{% endblock %}

3
tvdt/quiz/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
tvdt/quiz/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path, register_converter
from .converters import CandidateConverter, SeasonCodeConverter
from .views import SelectSeasonView
from .views.questionview import QuestionView
from .views.enternameview import EnterNameView
register_converter(SeasonCodeConverter, "season")
register_converter(CandidateConverter, "candidate")
urlpatterns = [
path("", SelectSeasonView.as_view(), name="home"),
path("<season:season>/", EnterNameView.as_view(), name="quiz"),
path("<candidate:candidate>/", QuestionView.as_view(), name="question"),
]

View File

@@ -0,0 +1 @@
from .selectseasonview import SelectSeasonView

View File

@@ -0,0 +1,51 @@
from crispy_forms.helper import FormHelper
from django import forms
from django.http import Http404
from django.shortcuts import render, redirect
from django.urls import reverse
from django.views import View
from ..models import Season, Candidate
class EnterNameForm(forms.Form):
name = forms.CharField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
class EnterNameView(View):
template_name = "quiz/enter_name.html"
forms_class = EnterNameForm
def get(self, request, season: Season, *args, **kwargs):
if season.active_quiz == None:
raise Http404("No quiz active")
return render(
request,
self.template_name,
{"form": self.forms_class(), "season": season},
)
def post(self, request, season: Season, *args, **kwargs):
name = request.POST.get("name")
if season.preregister_candidates:
try:
candidate = Candidate.objects.get(season=season, name=name)
except Candidate.DoesNotExist:
raise Http404("Candidate not found")
else:
candidate, created = Candidate.objects.get_or_create(
season=season, name=name
)
return redirect(
reverse(
"question",
kwargs={"candidate": candidate},
)
)

View File

@@ -0,0 +1,44 @@
from django.contrib import messages
from django.core.exceptions import BadRequest
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import render
from django.utils.translation import gettext as _
from django.views import View
from ..models import Candidate, Answer, GivenAnswer
from ..models.question import NoActiveTestForSeason
class QuestionView(View):
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"))
raise Http404("No active Quiz for seaon")
return render(
request,
"quiz/question.html",
{"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, question=answer.question, answer=answer
)
return self.get(request, candidate, args, kwargs)

View File

@@ -0,0 +1,29 @@
from crispy_forms.helper import FormHelper
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic.edit import FormView
from django import forms
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:
raise Http404("Season does not exist")
return redirect(reverse("quiz", kwargs={"season": season}))

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
from pathlib import Path from pathlib import Path
from django.utils.translation import gettext_lazy as _
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@@ -25,12 +26,13 @@ SECRET_KEY = "django-insecure-v*=mgb7c=)hn=(1eg-uljg6^l5)7(+i-^mt)hppiki6f$nzziu
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS: list[str] = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"quiz.apps.QuizConfig",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
@@ -39,6 +41,22 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
] ]
# crispy
INSTALLED_APPS += [
"crispy_forms",
"crispy_bootstrap5",
]
# allauth
INSTALLED_APPS += [
"allauth",
"allauth.account",
]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@@ -47,6 +65,7 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware",
] ]
ROOT_URLCONF = "tvdt.urls" ROOT_URLCONF = "tvdt.urls"
@@ -67,6 +86,13 @@ TEMPLATES = [
}, },
] ]
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, 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" WSGI_APPLICATION = "tvdt.wsgi.application"
@@ -103,9 +129,10 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/ # https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "nl"
LANGUAGES = [("nl", _("Dutch")), ("en", _("English"))]
TIME_ZONE = "UTC" TIME_ZONE = "Europe/Amsterdam"
USE_I18N = True USE_I18N = True
@@ -116,8 +143,15 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = "static/" STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 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"

View File

@@ -15,9 +15,14 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
urlpatterns = [ urlpatterns = [
path("", include("quiz.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
] path("accounts/", include("allauth.urls")),
path("i18n/", include("django.conf.urls.i18n")),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)