diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/README.md b/README.md index 4fa4996..2fed854 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,8 @@ meegenomen in de berekening van de slechtste speler. TBD + +## Nice to haves + +- Optie voor antwoord geven in twee klikken (selecteren en volgende). + diff --git a/tvdt/locale/nl/LC_MESSAGES/django.po b/tvdt/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..310f18b --- /dev/null +++ b/tvdt/locale/nl/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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" diff --git a/tvdt/manage.py b/tvdt/manage.py index 7b2facb..1991df3 100755 --- a/tvdt/manage.py +++ b/tvdt/manage.py @@ -4,7 +4,7 @@ import os import sys -def main(): +def main() -> None: """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tvdt.settings") try: diff --git a/tvdt/poetry.lock b/tvdt/poetry.lock index e8d3812..f3abf74 100644 --- a/tvdt/poetry.lock +++ b/tvdt/poetry.lock @@ -83,15 +83,51 @@ files = [ {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]] 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." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.1.2-py3-none-any.whl", hash = "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed"}, - {file = "Django-5.1.2.tar.gz", hash = "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0"}, + {file = "Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818"}, + {file = "Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a"}, ] [package.dependencies] @@ -103,39 +139,74 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] 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]] name = "django-stubs" -version = "5.1.0" +version = "5.1.1" description = "Mypy stubs for Django" optional = false python-versions = ">=3.8" files = [ - {file = "django_stubs-5.1.0-py3-none-any.whl", hash = "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40"}, - {file = "django_stubs-5.1.0.tar.gz", hash = "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5"}, + {file = "django_stubs-5.1.1-py3-none-any.whl", hash = "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac"}, + {file = "django_stubs-5.1.1.tar.gz", hash = "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b"}, ] [package.dependencies] asgiref = "*" django = "*" -django-stubs-ext = ">=5.1.0" -mypy = {version = ">=1.11.0,<1.12.0", optional = true, markers = "extra == \"compatible-mypy\""} +django-stubs-ext = ">=5.1.1" +mypy = {version = ">=1.12,<1.14", optional = true, markers = "extra == \"compatible-mypy\""} types-PyYAML = "*" typing-extensions = ">=4.11.0" [package.extras] -compatible-mypy = ["mypy (>=1.11.0,<1.12.0)"] +compatible-mypy = ["mypy (>=1.12,<1.14)"] oracle = ["oracledb"] redis = ["redis"] [[package]] name = "django-stubs-ext" -version = "5.1.0" +version = "5.1.1" description = "Monkey-patching and extensions for django-stubs" optional = false python-versions = ">=3.8" files = [ - {file = "django_stubs_ext-5.1.0-py3-none-any.whl", hash = "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d"}, - {file = "django_stubs_ext-5.1.0.tar.gz", hash = "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926"}, + {file = "django_stubs_ext-5.1.1-py3-none-any.whl", hash = "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c"}, + {file = "django_stubs_ext-5.1.1.tar.gz", hash = "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c"}, ] [package.dependencies] @@ -158,38 +229,43 @@ colors = ["colorama (>=0.4.6)"] [[package]] name = "mypy" -version = "1.11.2" +version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, - {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, - {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.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, - {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, - {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.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, - {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, - {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.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, - {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, - {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.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, - {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, - {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.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, - {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, - {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, - {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {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.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {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.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {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.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {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.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {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.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {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] @@ -198,6 +274,7 @@ typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -215,13 +292,13 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -253,13 +330,13 @@ type = ["mypy (>=1.11.2)"] [[package]] name = "sqlparse" -version = "0.5.1" +version = "0.5.2" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, - {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, + {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, + {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, ] [package.extras] @@ -302,4 +379,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "45eb74e8d8826f72371f13c7696dafb1aaebe2a63097f393341d32bec0e7db4e" +content-hash = "95199cfe52c42cc9e58c63f0f09684bff9a129c583fbbeb74e9c2ffa9c531813" diff --git a/tvdt/pyproject.toml b/tvdt/pyproject.toml index e748a01..c1539fc 100644 --- a/tvdt/pyproject.toml +++ b/tvdt/pyproject.toml @@ -5,6 +5,10 @@ package-mode = false [tool.poetry.dependencies] python = "^3.12" 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] mypy = "^1.11.0" @@ -12,6 +16,9 @@ black = "^24.10.0" isort = "^5.13.2" django-stubs = {extras = ["compatible-mypy"], version = "^5.1.0"} +[tool.isort] +profile = "black" + [tool.mypy] plugins = ["mypy_django_plugin.main"] diff --git a/tvdt/quiz/__init__.py b/tvdt/quiz/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tvdt/quiz/admin.py b/tvdt/quiz/admin.py new file mode 100644 index 0000000..2fc25b1 --- /dev/null +++ b/tvdt/quiz/admin.py @@ -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 diff --git a/tvdt/quiz/apps.py b/tvdt/quiz/apps.py new file mode 100644 index 0000000..59730f9 --- /dev/null +++ b/tvdt/quiz/apps.py @@ -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") diff --git a/tvdt/quiz/converters.py b/tvdt/quiz/converters.py new file mode 100644 index 0000000..531e9bc --- /dev/null +++ b/tvdt/quiz/converters.py @@ -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}" diff --git a/tvdt/quiz/fixtures/krtek.json b/tvdt/quiz/fixtures/krtek.json new file mode 100644 index 0000000..c705540 --- /dev/null +++ b/tvdt/quiz/fixtures/krtek.json @@ -0,0 +1,1157 @@ +[ +{ + "model": "quiz.answer", + "pk": 1, + "fields": { + "text": "Man", + "question": 1, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 2, + "fields": { + "text": "Vrouw", + "question": 1, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 3, + "fields": { + "text": "Geen", + "question": 2, + "is_right_answer": true, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 4, + "fields": { + "text": "1", + "question": 2, + "is_right_answer": false, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 5, + "fields": { + "text": "2", + "question": 2, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 6, + "fields": { + "text": "Geen", + "question": 3, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 7, + "fields": { + "text": "Diens eigen verjaardag", + "question": 3, + "is_right_answer": false, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 8, + "fields": { + "text": "Koningsdag", + "question": 3, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 9, + "fields": { + "text": "Kerst", + "question": 3, + "is_right_answer": true, + "_order": 3, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 10, + "fields": { + "text": "Oud en nieuw", + "question": 3, + "is_right_answer": false, + "_order": 4, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 11, + "fields": { + "text": "Met de auto", + "question": 4, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 12, + "fields": { + "text": "Met het OV", + "question": 4, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 13, + "fields": { + "text": "Claudia", + "question": 5, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 14, + "fields": { + "text": "Eelco", + "question": 5, + "is_right_answer": false, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 15, + "fields": { + "text": "Elise", + "question": 5, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 16, + "fields": { + "text": "Gert-Jan", + "question": 5, + "is_right_answer": false, + "_order": 3, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 17, + "fields": { + "text": "Iris", + "question": 5, + "is_right_answer": false, + "_order": 4, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 18, + "fields": { + "text": "Jari", + "question": 5, + "is_right_answer": false, + "_order": 5, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 19, + "fields": { + "text": "Laura", + "question": 5, + "is_right_answer": false, + "_order": 6, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 20, + "fields": { + "text": "Lotte", + "question": 5, + "is_right_answer": false, + "_order": 7, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 21, + "fields": { + "text": "Myrthe", + "question": 5, + "is_right_answer": false, + "_order": 8, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 22, + "fields": { + "text": "Remy", + "question": 5, + "is_right_answer": false, + "_order": 9, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 23, + "fields": { + "text": "Robbert", + "question": 5, + "is_right_answer": false, + "_order": 10, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 24, + "fields": { + "text": "Tom", + "question": 5, + "is_right_answer": true, + "_order": 11, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 25, + "fields": { + "text": "Geef je vader een knuffel.", + "question": 6, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 26, + "fields": { + "text": "Trek je wat minder aan van anderen.", + "question": 6, + "is_right_answer": false, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 27, + "fields": { + "text": "Luister meer naar je eigen gevoel in plaats van naar wat anderen", + "question": 6, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 28, + "fields": { + "text": "Stel niet alles tot het laatste moment uit.", + "question": 6, + "is_right_answer": false, + "_order": 3, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 29, + "fields": { + "text": "Altijd doorgaan.", + "question": 6, + "is_right_answer": false, + "_order": 4, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 30, + "fields": { + "text": "Probeer ook eens buiten de lijntjes te kleuren", + "question": 6, + "is_right_answer": true, + "_order": 5, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 31, + "fields": { + "text": "Ga als je groot bent op groepsreis!", + "question": 6, + "is_right_answer": false, + "_order": 6, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 32, + "fields": { + "text": "Trek minder aan van de mening van anderen, het is oké om anders", + "question": 6, + "is_right_answer": false, + "_order": 7, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 33, + "fields": { + "text": "Sneakers", + "question": 7, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 34, + "fields": { + "text": "Wandel-/bergschoenen", + "question": 7, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 35, + "fields": { + "text": "Lederen schoenen", + "question": 7, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 36, + "fields": { + "text": "Pantoffels", + "question": 7, + "is_right_answer": false, + "_order": 3, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 37, + "fields": { + "text": "Hakken", + "question": 7, + "is_right_answer": false, + "_order": 4, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 38, + "fields": { + "text": "Geen schoenen, alleen sokken", + "question": 7, + "is_right_answer": false, + "_order": 5, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 39, + "fields": { + "text": "Fiets", + "question": 8, + "is_right_answer": true, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 40, + "fields": { + "text": "Auto", + "question": 8, + "is_right_answer": false, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 41, + "fields": { + "text": "Trein", + "question": 8, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 42, + "fields": { + "text": "Ja", + "question": 9, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 43, + "fields": { + "text": "Nee", + "question": 9, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 44, + "fields": { + "text": "Karen", + "question": 10, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 45, + "fields": { + "text": "Gilles de Coster", + "question": 10, + "is_right_answer": false, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 46, + "fields": { + "text": "Kees Tol", + "question": 10, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 47, + "fields": { + "text": "Harry en John", + "question": 10, + "is_right_answer": false, + "_order": 3, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 48, + "fields": { + "text": "Georgina Verbaan", + "question": 10, + "is_right_answer": false, + "_order": 4, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 49, + "fields": { + "text": "Marc-Marie Huijbregts", + "question": 10, + "is_right_answer": false, + "_order": 5, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 50, + "fields": { + "text": "Fresia Cousiño Arias, Rik van de Westelaken", + "question": 10, + "is_right_answer": false, + "_order": 6, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 51, + "fields": { + "text": "Ellie Lust", + "question": 10, + "is_right_answer": false, + "_order": 7, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 52, + "fields": { + "text": "Bouba", + "question": 10, + "is_right_answer": false, + "_order": 8, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 53, + "fields": { + "text": "Jan Versteegh", + "question": 10, + "is_right_answer": false, + "_order": 9, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 54, + "fields": { + "text": "Dick Jol", + "question": 10, + "is_right_answer": false, + "_order": 10, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 55, + "fields": { + "text": "Karin de Groot", + "question": 10, + "is_right_answer": false, + "_order": 11, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 56, + "fields": { + "text": "Pieter", + "question": 10, + "is_right_answer": false, + "_order": 12, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 57, + "fields": { + "text": "Renée Fokker", + "question": 10, + "is_right_answer": false, + "_order": 13, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 58, + "fields": { + "text": "Sam, Davy", + "question": 10, + "is_right_answer": true, + "_order": 14, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 59, + "fields": { + "text": "Ja", + "question": 11, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 60, + "fields": { + "text": "Nee", + "question": 11, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 61, + "fields": { + "text": "Éénpersoons, losstaand bed", + "question": 12, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 62, + "fields": { + "text": "Éénpersoonsbed, tegen een ander bed aan", + "question": 12, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 63, + "fields": { + "text": "Tweepersoons bed", + "question": 12, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 64, + "fields": { + "text": "5", + "question": 13, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 65, + "fields": { + "text": "6", + "question": 13, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 66, + "fields": { + "text": "7", + "question": 13, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 67, + "fields": { + "text": "8", + "question": 13, + "is_right_answer": false, + "_order": 3, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 68, + "fields": { + "text": "Met de rug naar de accommodatie", + "question": 14, + "is_right_answer": false, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 69, + "fields": { + "text": "Met de rug naar de buitenmuur", + "question": 14, + "is_right_answer": true, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 70, + "fields": { + "text": "Claudia", + "question": 15, + "is_right_answer": true, + "_order": 0, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 71, + "fields": { + "text": "Eelco", + "question": 15, + "is_right_answer": false, + "_order": 1, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 72, + "fields": { + "text": "Elise", + "question": 15, + "is_right_answer": false, + "_order": 2, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 73, + "fields": { + "text": "Gert-Jan", + "question": 15, + "is_right_answer": false, + "_order": 3, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 74, + "fields": { + "text": "Iris", + "question": 15, + "is_right_answer": false, + "_order": 4, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 75, + "fields": { + "text": "Jari", + "question": 15, + "is_right_answer": false, + "_order": 5, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 76, + "fields": { + "text": "Lara", + "question": 15, + "is_right_answer": false, + "_order": 6, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 77, + "fields": { + "text": "Lotte", + "question": 15, + "is_right_answer": false, + "_order": 7, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 78, + "fields": { + "text": "Myrthe", + "question": 15, + "is_right_answer": false, + "_order": 8, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 79, + "fields": { + "text": "Remy", + "question": 15, + "is_right_answer": false, + "_order": 9, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 80, + "fields": { + "text": "Robbert", + "question": 15, + "is_right_answer": false, + "_order": 10, + "candidates": [] + } +}, +{ + "model": "quiz.answer", + "pk": 81, + "fields": { + "text": "Tom", + "question": 15, + "is_right_answer": false, + "_order": 11, + "candidates": [] + } +}, +{ + "model": "quiz.question", + "pk": 1, + "fields": { + "question": "Is de Krtek een man of een vrouw", + "quiz": 1, + "enabled": true, + "_order": 0 + } +}, +{ + "model": "quiz.question", + "pk": 2, + "fields": { + "question": "Hoeveel broers heeft de Krtek?", + "quiz": 1, + "enabled": true, + "_order": 1 + } +}, +{ + "model": "quiz.question", + "pk": 3, + "fields": { + "question": "Wat is de lievelingsfeestdag van de Krtek?", + "quiz": 1, + "enabled": true, + "_order": 2 + } +}, +{ + "model": "quiz.question", + "pk": 4, + "fields": { + "question": "Hoe kwam de Krtek naar Kesteren vandaag?", + "quiz": 1, + "enabled": true, + "_order": 3 + } +}, +{ + "model": "quiz.question", + "pk": 5, + "fields": { + "question": "Met wie keek de Krtek de video bij binnenkomst?", + "quiz": 1, + "enabled": true, + "_order": 4 + } +}, +{ + "model": "quiz.question", + "pk": 6, + "fields": { + "question": "Welk advies zou de Krtek zichzelf als kind geven?", + "quiz": 1, + "enabled": true, + "_order": 5 + } +}, +{ + "model": "quiz.question", + "pk": 7, + "fields": { + "question": "Wat voor soort schoenen droeg de Krtek bij het diner?", + "quiz": 1, + "enabled": true, + "_order": 6 + } +}, +{ + "model": "quiz.question", + "pk": 8, + "fields": { + "question": "Met welk vervoersmiddel reist de Krtek het liefste?", + "quiz": 1, + "enabled": true, + "_order": 7 + } +}, +{ + "model": "quiz.question", + "pk": 9, + "fields": { + "question": "Heeft de Krtek een eigen auto?", + "quiz": 1, + "enabled": true, + "_order": 8 + } +}, +{ + "model": "quiz.question", + "pk": 10, + "fields": { + "question": "Van wie is de quote die de Krtek gepakt heeft", + "quiz": 1, + "enabled": true, + "_order": 9 + } +}, +{ + "model": "quiz.question", + "pk": 11, + "fields": { + "question": "Zou de Krtek molboekjes, jokers, vrijstellingen of topito’s uit iemands rugzak stelen om te kunnen winnen?", + "quiz": 1, + "enabled": true, + "_order": 10 + } +}, +{ + "model": "quiz.question", + "pk": 12, + "fields": { + "question": "In wat voor bed slaapt de Krtek dit weekend?", + "quiz": 1, + "enabled": true, + "_order": 11 + } +}, +{ + "model": "quiz.question", + "pk": 13, + "fields": { + "question": "Hoeveel jaar heeft de Krtek gedaan over de middelbare school?", + "quiz": 1, + "enabled": true, + "_order": 12 + } +}, +{ + "model": "quiz.question", + "pk": 14, + "fields": { + "question": "Waar zat de Krtek aan tafel bij het diner?", + "quiz": 1, + "enabled": true, + "_order": 13 + } +}, +{ + "model": "quiz.question", + "pk": 15, + "fields": { + "question": "Wie is de Krtek", + "quiz": 1, + "enabled": true, + "_order": 14 + } +}, +{ + "model": "quiz.candidate", + "pk": 1, + "fields": { + "season": 1, + "name": "Claudia" + } +}, +{ + "model": "quiz.candidate", + "pk": 2, + "fields": { + "season": 1, + "name": "Eelco" + } +}, +{ + "model": "quiz.candidate", + "pk": 3, + "fields": { + "season": 1, + "name": "Elise" + } +}, +{ + "model": "quiz.candidate", + "pk": 4, + "fields": { + "season": 1, + "name": "Gert-Jan" + } +}, +{ + "model": "quiz.candidate", + "pk": 5, + "fields": { + "season": 1, + "name": "Iris" + } +}, +{ + "model": "quiz.candidate", + "pk": 6, + "fields": { + "season": 1, + "name": "Jari" + } +}, +{ + "model": "quiz.candidate", + "pk": 7, + "fields": { + "season": 1, + "name": "Lara" + } +}, +{ + "model": "quiz.candidate", + "pk": 8, + "fields": { + "season": 1, + "name": "Lotte" + } +}, +{ + "model": "quiz.candidate", + "pk": 9, + "fields": { + "season": 1, + "name": "Myrthe" + } +}, +{ + "model": "quiz.candidate", + "pk": 10, + "fields": { + "season": 1, + "name": "Remy" + } +}, +{ + "model": "quiz.candidate", + "pk": 11, + "fields": { + "season": 1, + "name": "Robbert" + } +}, +{ + "model": "quiz.candidate", + "pk": 12, + "fields": { + "season": 1, + "name": "Tom" + } +}, +{ + "model": "quiz.quiz", + "pk": 1, + "fields": { + "name": "Test 1", + "season": 1 + } +}, +{ + "model": "quiz.season", + "pk": 1, + "fields": { + "name": "Krtek", + "active_quiz": 1, + "season_code": "12345", + "preregister_candidates": true + } +} +] diff --git a/tvdt/quiz/helpers.py b/tvdt/quiz/helpers.py new file mode 100644 index 0000000..10d517e --- /dev/null +++ b/tvdt/quiz/helpers.py @@ -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) + ) diff --git a/tvdt/quiz/locale/nl/LC_MESSAGES/django.po b/tvdt/quiz/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..c0177c4 --- /dev/null +++ b/tvdt/quiz/locale/nl/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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" diff --git a/tvdt/quiz/migrations/0001_initial.py b/tvdt/quiz/migrations/0001_initial.py new file mode 100644 index 0000000..98915f7 --- /dev/null +++ b/tvdt/quiz/migrations/0001_initial.py @@ -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")}, + ), + ] diff --git a/tvdt/quiz/migrations/0002_season_preregister_candidates_and_more.py b/tvdt/quiz/migrations/0002_season_preregister_candidates_and_more.py new file mode 100644 index 0000000..5f6e76b --- /dev/null +++ b/tvdt/quiz/migrations/0002_season_preregister_candidates_and_more.py @@ -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"), + ), + ] diff --git a/tvdt/quiz/migrations/0003_givenanswer_created_alter_givenanswer_question.py b/tvdt/quiz/migrations/0003_givenanswer_created_alter_givenanswer_question.py new file mode 100644 index 0000000..1473ea5 --- /dev/null +++ b/tvdt/quiz/migrations/0003_givenanswer_created_alter_givenanswer_question.py @@ -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", + ), + ), + ] diff --git a/tvdt/quiz/migrations/0004_alter_question_unique_together_and_more.py b/tvdt/quiz/migrations/0004_alter_question_unique_together_and_more.py new file mode 100644 index 0000000..969f4f4 --- /dev/null +++ b/tvdt/quiz/migrations/0004_alter_question_unique_together_and_more.py @@ -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", + ), + ] diff --git a/tvdt/quiz/migrations/__init__.py b/tvdt/quiz/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tvdt/quiz/models/__init__.py b/tvdt/quiz/models/__init__.py new file mode 100644 index 0000000..d35b3c4 --- /dev/null +++ b/tvdt/quiz/models/__init__.py @@ -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 diff --git a/tvdt/quiz/models/answer.py b/tvdt/quiz/models/answer.py new file mode 100644 index 0000000..f92af4f --- /dev/null +++ b/tvdt/quiz/models/answer.py @@ -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" diff --git a/tvdt/quiz/models/candidate.py b/tvdt/quiz/models/candidate.py new file mode 100644 index 0000000..faee14c --- /dev/null +++ b/tvdt/quiz/models/candidate.py @@ -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") diff --git a/tvdt/quiz/models/correction.py b/tvdt/quiz/models/correction.py new file mode 100644 index 0000000..07cb610 --- /dev/null +++ b/tvdt/quiz/models/correction.py @@ -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") diff --git a/tvdt/quiz/models/given_answer.py b/tvdt/quiz/models/given_answer.py new file mode 100644 index 0000000..dbe98a0 --- /dev/null +++ b/tvdt/quiz/models/given_answer.py @@ -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") diff --git a/tvdt/quiz/models/question.py b/tvdt/quiz/models/question.py new file mode 100644 index 0000000..87e55e0 --- /dev/null +++ b/tvdt/quiz/models/question.py @@ -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" diff --git a/tvdt/quiz/models/quiz.py b/tvdt/quiz/models/quiz.py new file mode 100644 index 0000000..2dde36b --- /dev/null +++ b/tvdt/quiz/models/quiz.py @@ -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") diff --git a/tvdt/quiz/models/quiz_time.py b/tvdt/quiz/models/quiz_time.py new file mode 100644 index 0000000..7db0247 --- /dev/null +++ b/tvdt/quiz/models/quiz_time.py @@ -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") diff --git a/tvdt/quiz/models/season.py b/tvdt/quiz/models/season.py new file mode 100644 index 0000000..96710cb --- /dev/null +++ b/tvdt/quiz/models/season.py @@ -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") diff --git a/tvdt/quiz/static/quiz/background.png b/tvdt/quiz/static/quiz/background.png new file mode 100644 index 0000000..fabe57e Binary files /dev/null and b/tvdt/quiz/static/quiz/background.png differ diff --git a/tvdt/quiz/templates/quiz/base.html b/tvdt/quiz/templates/quiz/base.html new file mode 100644 index 0000000..7f93b6f --- /dev/null +++ b/tvdt/quiz/templates/quiz/base.html @@ -0,0 +1,33 @@ +{% load static %} + + + + + {# #} + + + {% block title %}{% endblock %} + + + +
+{% block body %}{% endblock %} +
+{% block script %}{% endblock %} + + diff --git a/tvdt/quiz/templates/quiz/enter_name.html b/tvdt/quiz/templates/quiz/enter_name.html new file mode 100644 index 0000000..1af6abf --- /dev/null +++ b/tvdt/quiz/templates/quiz/enter_name.html @@ -0,0 +1,9 @@ +{% extends "quiz/base.html" %} +{% load crispy_forms_tags %} + +{% block body %} + +

{{ season.name }} ({{ season.season_code }})

+ {% crispy form %} + +{% endblock %} \ No newline at end of file diff --git a/tvdt/quiz/templates/quiz/question.html b/tvdt/quiz/templates/quiz/question.html new file mode 100644 index 0000000..657833f --- /dev/null +++ b/tvdt/quiz/templates/quiz/question.html @@ -0,0 +1,14 @@ +{% extends 'quiz/base.html' %} +{% load i18n %} + +{% block body %} +

{{ question.question }}

+
+ {% csrf_token %} + {% for answer in question.answers.all %} +
+ {% empty %} + {% translate "Weirdly enough this question has no answers..." %} + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/tvdt/quiz/templates/quiz/select_season.html b/tvdt/quiz/templates/quiz/select_season.html new file mode 100644 index 0000000..3921f5b --- /dev/null +++ b/tvdt/quiz/templates/quiz/select_season.html @@ -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 %} diff --git a/tvdt/quiz/tests.py b/tvdt/quiz/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/tvdt/quiz/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tvdt/quiz/urls.py b/tvdt/quiz/urls.py new file mode 100644 index 0000000..fbd8a67 --- /dev/null +++ b/tvdt/quiz/urls.py @@ -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("/", EnterNameView.as_view(), name="quiz"), + path("/", QuestionView.as_view(), name="question"), +] diff --git a/tvdt/quiz/views/__init__.py b/tvdt/quiz/views/__init__.py new file mode 100644 index 0000000..dccd439 --- /dev/null +++ b/tvdt/quiz/views/__init__.py @@ -0,0 +1 @@ +from .selectseasonview import SelectSeasonView diff --git a/tvdt/quiz/views/enternameview.py b/tvdt/quiz/views/enternameview.py new file mode 100644 index 0000000..3ea4b3f --- /dev/null +++ b/tvdt/quiz/views/enternameview.py @@ -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}, + ) + ) diff --git a/tvdt/quiz/views/questionview.py b/tvdt/quiz/views/questionview.py new file mode 100644 index 0000000..a24ecd2 --- /dev/null +++ b/tvdt/quiz/views/questionview.py @@ -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) diff --git a/tvdt/quiz/views/selectseasonview.py b/tvdt/quiz/views/selectseasonview.py new file mode 100644 index 0000000..cce0f53 --- /dev/null +++ b/tvdt/quiz/views/selectseasonview.py @@ -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})) diff --git a/tvdt/tvdt/settings.py b/tvdt/tvdt/settings.py index d2fe4d7..33581d1 100644 --- a/tvdt/tvdt/settings.py +++ b/tvdt/tvdt/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ from pathlib import Path +from django.utils.translation import gettext_lazy as _ # Build paths inside the project like this: BASE_DIR / 'subdir'. 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! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS: list[str] = [] # Application definition INSTALLED_APPS = [ + "quiz.apps.QuizConfig", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -39,6 +41,22 @@ INSTALLED_APPS = [ "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 = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -47,6 +65,7 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", ] 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" @@ -103,9 +129,10 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # 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 @@ -116,8 +143,15 @@ USE_TZ = True # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# email is the new username +ACCOUNT_USER_MODEL_USERNAME_FIELD = None +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_AUTHENTICATION_METHOD = "email" diff --git a/tvdt/tvdt/urls.py b/tvdt/tvdt/urls.py index c848563..e01c529 100644 --- a/tvdt/tvdt/urls.py +++ b/tvdt/tvdt/urls.py @@ -15,9 +15,14 @@ Including another URLconf 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.urls import path +from django.urls import path, include urlpatterns = [ + path("", include("quiz.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)