mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 04:44:19 +01:00
Add basic quiz functionality
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -37,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).
|
||||||
|
|
||||||
|
|||||||
27
tvdt/locale/nl/LC_MESSAGES/django.po
Normal file
27
tvdt/locale/nl/LC_MESSAGES/django.po
Normal 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"
|
||||||
@@ -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
171
tvdt/poetry.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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
0
tvdt/quiz/__init__.py
Normal file
43
tvdt/quiz/admin.py
Normal file
43
tvdt/quiz/admin.py
Normal 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
8
tvdt/quiz/apps.py
Normal 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
41
tvdt/quiz/converters.py
Normal 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}"
|
||||||
1157
tvdt/quiz/fixtures/krtek.json
Normal file
1157
tvdt/quiz/fixtures/krtek.json
Normal file
File diff suppressed because it is too large
Load Diff
8
tvdt/quiz/helpers.py
Normal file
8
tvdt/quiz/helpers.py
Normal 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)
|
||||||
|
)
|
||||||
122
tvdt/quiz/locale/nl/LC_MESSAGES/django.po
Normal file
122
tvdt/quiz/locale/nl/LC_MESSAGES/django.po
Normal 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"
|
||||||
311
tvdt/quiz/migrations/0001_initial.py
Normal file
311
tvdt/quiz/migrations/0001_initial.py
Normal 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")},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
|
),
|
||||||
|
]
|
||||||
0
tvdt/quiz/migrations/__init__.py
Normal file
0
tvdt/quiz/migrations/__init__.py
Normal file
8
tvdt/quiz/models/__init__.py
Normal file
8
tvdt/quiz/models/__init__.py
Normal 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
|
||||||
23
tvdt/quiz/models/answer.py
Normal file
23
tvdt/quiz/models/answer.py
Normal 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"
|
||||||
48
tvdt/quiz/models/candidate.py
Normal file
48
tvdt/quiz/models/candidate.py
Normal 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")
|
||||||
26
tvdt/quiz/models/correction.py
Normal file
26
tvdt/quiz/models/correction.py
Normal 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")
|
||||||
29
tvdt/quiz/models/given_answer.py
Normal file
29
tvdt/quiz/models/given_answer.py
Normal 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")
|
||||||
31
tvdt/quiz/models/question.py
Normal file
31
tvdt/quiz/models/question.py
Normal 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
25
tvdt/quiz/models/quiz.py
Normal 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")
|
||||||
15
tvdt/quiz/models/quiz_time.py
Normal file
15
tvdt/quiz/models/quiz_time.py
Normal 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")
|
||||||
39
tvdt/quiz/models/season.py
Normal file
39
tvdt/quiz/models/season.py
Normal 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")
|
||||||
BIN
tvdt/quiz/static/quiz/background.png
Normal file
BIN
tvdt/quiz/static/quiz/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
33
tvdt/quiz/templates/quiz/base.html
Normal file
33
tvdt/quiz/templates/quiz/base.html
Normal 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>
|
||||||
9
tvdt/quiz/templates/quiz/enter_name.html
Normal file
9
tvdt/quiz/templates/quiz/enter_name.html
Normal 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 %}
|
||||||
14
tvdt/quiz/templates/quiz/question.html
Normal file
14
tvdt/quiz/templates/quiz/question.html
Normal 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 %}
|
||||||
7
tvdt/quiz/templates/quiz/select_season.html
Normal file
7
tvdt/quiz/templates/quiz/select_season.html
Normal 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
3
tvdt/quiz/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
14
tvdt/quiz/urls.py
Normal file
14
tvdt/quiz/urls.py
Normal 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"),
|
||||||
|
]
|
||||||
1
tvdt/quiz/views/__init__.py
Normal file
1
tvdt/quiz/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .selectseasonview import SelectSeasonView
|
||||||
51
tvdt/quiz/views/enternameview.py
Normal file
51
tvdt/quiz/views/enternameview.py
Normal 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},
|
||||||
|
)
|
||||||
|
)
|
||||||
44
tvdt/quiz/views/questionview.py
Normal file
44
tvdt/quiz/views/questionview.py
Normal 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)
|
||||||
29
tvdt/quiz/views/selectseasonview.py
Normal file
29
tvdt/quiz/views/selectseasonview.py
Normal 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}))
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user