This commit is contained in:
2024-11-25 19:21:44 +01:00
parent 27b8c40c1c
commit 6ad9b46543
26 changed files with 260 additions and 338 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea/
### Generated by gibo (https://github.com/simonwhitaker/gibo) ### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/76739a38b56907118c5a880d63250c99d5690a5a/Python.gitignore ### https://raw.github.com/github/gitignore/76739a38b56907118c5a880d63250c99d5690a5a/Python.gitignore
@@ -174,6 +175,7 @@ cython_debug/
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*

View File

@@ -16,6 +16,7 @@ kunnen alleen vooraf ingevoerde namen gebruikt worden in een test.
- Vanaf het moment dat de kandidaat op start klikt na het intypen van hun naam - Vanaf het moment dat de kandidaat op start klikt na het intypen van hun naam
gaat de tijd lopen. Deze stopt na het aanklikken van een antwoord op de laatste gaat de tijd lopen. Deze stopt na het aanklikken van een antwoord op de laatste
vraag van de test. vraag van de test.
- Achtergrondmuziek
### Schermen kijken ### Schermen kijken

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-20 00:06+0200\n" "POT-Creation-Date: 2024-11-24 12:18+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,10 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tvdt/settings.py:109 #: tvdt/settings.py:147
msgid "Dutch" msgid "Dutch"
msgstr "Nederlands" msgstr "Nederlands"
#: tvdt/settings.py:109 #: tvdt/settings.py:147
msgid "English" msgid "English"
msgstr "Engels" msgstr "Engels"

20
tvdt/poetry.lock generated
View File

@@ -101,24 +101,6 @@ django-crispy-forms = ">=2.3"
[package.extras] [package.extras]
test = ["pytest", "pytest-django"] 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.3" version = "5.1.3"
@@ -379,4 +361,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "95199cfe52c42cc9e58c63f0f09684bff9a129c583fbbeb74e9c2ffa9c531813" content-hash = "c7f81b41e492344223fddfa4c37985437e3f8d0012c90b968f9dd3fdd0c67833"

View File

@@ -6,7 +6,6 @@ package-mode = false
python = "^3.12" python = "^3.12"
Django = "^5.1.2" Django = "^5.1.2"
django-crispy-forms = "^2.3" django-crispy-forms = "^2.3"
crispy-tailwind = "^1.0.3"
crispy-bootstrap5 = "^2024.10" crispy-bootstrap5 = "^2024.10"
django-allauth = "^65.1.0" django-allauth = "^65.1.0"

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Question, Answer, Candidate, Quiz, Season, GivenAnswer from .models import Answer, Candidate, GivenAnswer, Question, Quiz, Season
class CandidatesAdmin(admin.StackedInline): class CandidatesAdmin(admin.StackedInline):

View File

@@ -1,7 +1,7 @@
import base64 import base64
import binascii import binascii
from .models import Season, Candidate from .models import Candidate, Season
class SeasonCodeConverter: class SeasonCodeConverter:

View File

@@ -1136,6 +1136,30 @@
"name": "Tom" "name": "Tom"
} }
}, },
{
"model": "quiz.candidate",
"pk": 14,
"fields": {
"season": 1,
"name": "Marijn"
}
},
{
"model": "quiz.candidate",
"pk": 15,
"fields": {
"season": 1,
"name": "Renske"
}
},
{
"model": "quiz.candidate",
"pk": 16,
"fields": {
"season": 1,
"name": "Philine"
}
},
{ {
"model": "quiz.quiz", "model": "quiz.quiz",
"pk": 1, "pk": 1,

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-20 00:23+0200\n" "POT-Creation-Date: 2024-11-25 19:18+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,93 +18,77 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: quiz/models/answer.py:10 #: quiz/apps.py:8 quiz/models/correction.py:17 quiz/models/given_answer.py:19
msgid "text" #: quiz/models/question.py:20 quiz/models/quiz.py:24
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" msgid "quiz"
msgstr "test" msgstr "test"
#: quiz/models/correction.py:25 #: quiz/models/answer.py:7
msgid "text"
msgstr "tekst"
#: quiz/models/answer.py:12 quiz/models/question.py:15
#: quiz/models/question.py:28
msgid "question"
msgstr "vraag"
#: quiz/models/answer.py:14
msgid "is right answer"
msgstr "is goede antwoord"
#: quiz/models/answer.py:16 quiz/models/candidate.py:50
msgid "candidates"
msgstr "kandidaten"
#: quiz/models/answer.py:20 quiz/models/given_answer.py:25
msgid "answer"
msgstr "antwoord"
#: quiz/models/answer.py:21
msgid "answers"
msgstr "antwoorden"
#: quiz/models/candidate.py:18 quiz/models/quiz.py:7 quiz/models/season.py:12
msgid "name"
msgstr "naam"
#: quiz/models/candidate.py:49 quiz/models/correction.py:11
#: quiz/models/given_answer.py:11
msgid "candidate"
msgstr "kandidaat"
#: quiz/models/correction.py:22
msgid "correction" msgid "correction"
msgstr "joker" msgstr "joker"
#: quiz/models/correction.py:26 #: quiz/models/correction.py:23
msgid "corrections" msgid "corrections"
msgstr "jokers" msgstr "jokers"
#: quiz/models/given_answer.py:30 #: quiz/models/given_answer.py:36
msgid "given answer" msgid "given answer"
msgstr "gegeven antwoord" msgstr "gegeven antwoord"
#: quiz/models/given_answer.py:31 #: quiz/models/given_answer.py:37
msgid "given answers" msgid "given answers"
msgstr "gegeven antwoorden" 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 #: quiz/models/question.py:22
msgid "enabled"
msgstr "actief"
#: quiz/models/question.py:29
msgid "questions" msgid "questions"
msgstr "vraag" msgstr "vraag"
#: quiz/models/quiz.py:14 quiz/models/season.py:38 #: quiz/models/quiz.py:12 quiz/models/season.py:38
msgid "season" msgid "season"
msgstr "seizoen" msgstr "seizoen"
#: quiz/models/quiz.py:27 #: quiz/models/quiz.py:25
msgid "quizzes" msgid "quizzes"
msgstr "tests" 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 #: quiz/models/season.py:19
msgid "active quiz" msgid "active quiz"
msgstr "actieve test" msgstr "actieve test"
@@ -120,3 +104,35 @@ msgstr "kandidaten voorregistreren"
#: quiz/models/season.py:39 #: quiz/models/season.py:39
msgid "seasons" msgid "seasons"
msgstr "seizoenen" msgstr "seizoenen"
#: quiz/templates/quiz/question.html:11
msgid "Weirdly enough this question has no answers..."
msgstr "Raar genoeg heeft deze vraag geen antwoorden..."
#: quiz/templates/quiz/select_season.html:4
msgid "Tijd voor de test"
msgstr "Tijd voor de test"
#: quiz/views/enternameview.py:14
msgid "Name"
msgstr "Naam"
#: quiz/views/enternameview.py:27
msgid "This season has no active quiz."
msgstr "Dit seizoen heeft geen actieve test."
#: quiz/views/enternameview.py:39
msgid "Candidate does not exist"
msgstr "Kandidaat bestaat niet"
#: quiz/views/questionview.py:23
msgid "No active quiz for season"
msgstr "Geen active test voor seizoen"
#: quiz/views/questionview.py:27
msgid "Quiz done"
msgstr "Test klaar"
#: quiz/views/selectseasonview.py:30
msgid "Invalid season code"
msgstr "Ongeldige seizoencode"

View File

@@ -1,9 +1,10 @@
# Generated by Django 5.1.2 on 2024-10-19 21:54 # Generated by Django 5.1.3 on 2024-11-25 18:17
import django.db.models.deletion import django.db.models.deletion
import quiz.helpers
from django.db import migrations, models from django.db import migrations, models
import quiz.helpers
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -24,7 +25,7 @@ class Migration(migrations.Migration):
verbose_name="ID", verbose_name="ID",
), ),
), ),
("name", models.CharField(max_length=16, verbose_name="naam")), ("name", models.CharField(max_length=16, verbose_name="name")),
], ],
options={ options={
"verbose_name": "candidate", "verbose_name": "candidate",
@@ -44,7 +45,6 @@ class Migration(migrations.Migration):
), ),
), ),
("question", models.CharField(max_length=256, verbose_name="question")), ("question", models.CharField(max_length=256, verbose_name="question")),
("number", models.PositiveSmallIntegerField(verbose_name="number")),
("enabled", models.BooleanField(default=True, verbose_name="enabled")), ("enabled", models.BooleanField(default=True, verbose_name="enabled")),
], ],
options={ options={
@@ -64,7 +64,7 @@ class Migration(migrations.Migration):
verbose_name="ID", verbose_name="ID",
), ),
), ),
("name", models.CharField(max_length=64, verbose_name="naam")), ("name", models.CharField(max_length=64, verbose_name="name")),
], ],
options={ options={
"verbose_name": "quiz", "verbose_name": "quiz",
@@ -91,7 +91,7 @@ class Migration(migrations.Migration):
( (
"candidates", "candidates",
models.ManyToManyField( models.ManyToManyField(
to="quiz.candidate", verbose_name="candidates" blank=True, to="quiz.candidate", verbose_name="candidates"
), ),
), ),
( (
@@ -107,6 +107,7 @@ class Migration(migrations.Migration):
options={ options={
"verbose_name": "answer", "verbose_name": "answer",
"verbose_name_plural": "answers", "verbose_name_plural": "answers",
"order_with_respect_to": "question",
}, },
), ),
migrations.AddField( migrations.AddField(
@@ -120,7 +121,7 @@ class Migration(migrations.Migration):
), ),
), ),
migrations.CreateModel( migrations.CreateModel(
name="QuizTime", name="GivenAnswer",
fields=[ fields=[
( (
"id", "id",
@@ -131,11 +132,21 @@ class Migration(migrations.Migration):
verbose_name="ID", verbose_name="ID",
), ),
), ),
("seconds", models.PositiveIntegerField(verbose_name="seconds")), ("created", models.DateTimeField(auto_now_add=True)),
(
"answer",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="quiz.answer",
verbose_name="answer",
),
),
( (
"candidate", "candidate",
models.ForeignKey( models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name="answers",
to="quiz.candidate", to="quiz.candidate",
verbose_name="candidate", verbose_name="candidate",
), ),
@@ -144,14 +155,16 @@ class Migration(migrations.Migration):
"quiz", "quiz",
models.ForeignKey( models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="quiz.quiz", to="quiz.quiz",
verbose_name="quiz", verbose_name="quiz",
), ),
), ),
], ],
options={ options={
"verbose_name": "quiz time", "verbose_name": "given answer",
"verbose_name_plural": "quiz times", "verbose_name_plural": "given answers",
"ordering": ("quiz", "candidate"),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@@ -166,7 +179,7 @@ class Migration(migrations.Migration):
verbose_name="ID", verbose_name="ID",
), ),
), ),
("name", models.CharField(max_length=64, verbose_name="naam")), ("name", models.CharField(max_length=64, verbose_name="name")),
( (
"season_code", "season_code",
models.CharField( models.CharField(
@@ -175,9 +188,16 @@ class Migration(migrations.Migration):
verbose_name="season code", verbose_name="season code",
), ),
), ),
(
"preregister_candidates",
models.BooleanField(
default=True, verbose_name="preregister candidates"
),
),
( (
"active_quiz", "active_quiz",
models.ForeignKey( models.ForeignKey(
blank=True,
default=None, default=None,
null=True, null=True,
on_delete=django.db.models.deletion.SET_NULL, on_delete=django.db.models.deletion.SET_NULL,
@@ -212,54 +232,9 @@ class Migration(migrations.Migration):
verbose_name="season", verbose_name="season",
), ),
), ),
migrations.CreateModel( migrations.AlterOrderWithRespectTo(
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", name="question",
unique_together={("quiz", "number")}, order_with_respect_to="quiz",
), ),
migrations.CreateModel( migrations.CreateModel(
name="Correction", name="Correction",

View File

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

View File

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

View File

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

View File

@@ -4,5 +4,4 @@ from .correction import Correction
from .given_answer import GivenAnswer from .given_answer import GivenAnswer
from .question import Question from .question import Question
from .quiz import Quiz from .quiz import Quiz
from .quiz_time import QuizTime
from .season import Season from .season import Season

View File

@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta from django_stubs_ext.db.models import TypedModelMeta
from .given_answer import GivenAnswer from .given_answer import GivenAnswer
from .question import NoActiveTestForSeason, QuizAlreadyFinished, Question from .question import NoActiveTestForSeason, Question, QuizAlreadyFinished
class Candidate(models.Model): class Candidate(models.Model):
@@ -23,11 +23,13 @@ class Candidate(models.Model):
raise NoActiveTestForSeason() raise NoActiveTestForSeason()
question = ( question = (
Question.objects.filter(quiz=quiz) Question.objects.filter(quiz=quiz, enabled=True)
.exclude( .exclude(
id__in=GivenAnswer.objects.filter( id__in=GivenAnswer.objects.filter(
candidate=candidate, question__quiz=quiz candidate=candidate,
).values_list("question_id", flat=True) quiz=quiz,
answer__isnull=False,
).values_list("answer__question_id", flat=True)
) )
.first() .first()
) )

View File

@@ -2,9 +2,6 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta from django_stubs_ext.db.models import TypedModelMeta
from .candidate import Candidate
from .quiz import Quiz
class Correction(models.Model): class Correction(models.Model):
candidate = models.ForeignKey( candidate = models.ForeignKey(

View File

@@ -10,20 +10,28 @@ class GivenAnswer(models.Model):
related_name="answers", related_name="answers",
verbose_name=_("candidate"), verbose_name=_("candidate"),
) )
question = models.ForeignKey(
"Question", quiz = models.ForeignKey(
"Quiz",
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=True, null=False,
related_name="given_answers", related_name="+",
verbose_name=_("question"), verbose_name=_("quiz"),
) )
answer = models.ForeignKey( answer = models.ForeignKey(
"Answer", on_delete=models.CASCADE, verbose_name=_("answer") "Answer",
on_delete=models.CASCADE,
verbose_name=_("answer"),
null=True,
) )
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.quiz} - {self.candidate.name} {self.answer}"
class Meta(TypedModelMeta): class Meta(TypedModelMeta):
unique_together = ["candidate", "question"] ordering = ("quiz", "candidate")
verbose_name = _("given answer") verbose_name = _("given answer")
verbose_name_plural = _("given answers") verbose_name_plural = _("given answers")

View File

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

View File

@@ -1,6 +1,6 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-bs-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
{# <script src="https://cdn.tailwindcss.com"></script>#} {# <script src="https://cdn.tailwindcss.com"></script>#}
@@ -17,17 +17,33 @@
background-position: center center; background-position: center center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: black; background-color: black;
color: white;
display: grid; display: grid;
align-items: center; align-items: center;
justify-self: center; justify-self: center;
color: white; }
.asteriskField {
display: none;
} }
</style> </style>
</head> </head>
<body> <body>
<main>
<div class="container"> <div class="container">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible fade show {{ message.tags }}" role="alert">
{% if message.level == DEFAULT_MESSAGE_LEVELS.DEBUG %}
<strong>Debug: </strong>{% endif %}{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div> </div>
{% block script %}{% endblock %} {% block script %}{% endblock %}
</main>
</body> </body>
</html> </html>

View File

@@ -3,7 +3,6 @@
{% block body %} {% block body %}
<p>{{ season.name }} ({{ season.season_code }})</p>
{% crispy form %} {% crispy form %}
{% endblock %} {% endblock %}

View File

@@ -2,8 +2,8 @@ from django.urls import path, register_converter
from .converters import CandidateConverter, SeasonCodeConverter from .converters import CandidateConverter, SeasonCodeConverter
from .views import SelectSeasonView from .views import SelectSeasonView
from .views.questionview import QuestionView
from .views.enternameview import EnterNameView from .views.enternameview import EnterNameView
from .views.questionview import QuestionView
register_converter(SeasonCodeConverter, "season") register_converter(SeasonCodeConverter, "season")
register_converter(CandidateConverter, "candidate") register_converter(CandidateConverter, "candidate")

View File

@@ -1,47 +1,47 @@
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from django import forms from django import forms
from django.http import Http404 from django.contrib import messages
from django.shortcuts import render, redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from django.views import View from django.views import View
from django.views.generic.base import TemplateResponseMixin
from ..models import Season, Candidate from ..models import Candidate, Season
class EnterNameForm(forms.Form): class EnterNameForm(forms.Form):
name = forms.CharField() name = forms.CharField(label=_("Name"))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
class EnterNameView(View): class EnterNameView(View, TemplateResponseMixin):
template_name = "quiz/enter_name.html" template_name = "quiz/enter_name.html"
forms_class = EnterNameForm forms_class = EnterNameForm
def get(self, request, season: Season, *args, **kwargs): def get(self, request, season: Season, *args, **kwargs):
if season.active_quiz == None: if season.active_quiz == None:
raise Http404("No quiz active") messages.info(request, _("This season has no active quiz."))
return redirect("home")
return render( return self.render_to_response({"form": self.forms_class()})
request,
self.template_name,
{"form": self.forms_class(), "season": season},
)
def post(self, request, season: Season, *args, **kwargs): def post(self, request, season: Season, *args, **kwargs):
name = request.POST.get("name") name = request.POST.get("name")
if season.preregister_candidates:
try: try:
candidate = Candidate.objects.get(season=season, name=name) candidate = Candidate.objects.get(season=season, name__iexact=name)
except Candidate.DoesNotExist: except Candidate.DoesNotExist:
raise Http404("Candidate not found") if season.preregister_candidates:
else: messages.warning(request, _("Candidate does not exist"))
candidate, created = Candidate.objects.get_or_create(
season=season, name=name return redirect(reverse("quiz", kwargs={"season": season}))
)
candidate = Candidate.objects.create(season=season, name=name)
return redirect( return redirect(
reverse( reverse(

View File

@@ -1,15 +1,17 @@
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import BadRequest from django.core.exceptions import BadRequest
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import render from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views import View from django.views import View
from django.views.generic.base import TemplateResponseMixin
from ..models import Candidate, Answer, GivenAnswer from ..models import Answer, Candidate, GivenAnswer
from ..models.question import NoActiveTestForSeason from ..models.question import NoActiveTestForSeason, QuizAlreadyFinished
class QuestionView(View): class QuestionView(View, TemplateResponseMixin):
template_name = "quiz/question.html" template_name = "quiz/question.html"
def get( def get(
@@ -18,15 +20,27 @@ class QuestionView(View):
try: try:
question = candidate.get_next_question(candidate) question = candidate.get_next_question(candidate)
except NoActiveTestForSeason: except NoActiveTestForSeason:
messages.error(request, _("No active Quiz for season")) messages.error(request, _("No active quiz for season"))
raise Http404("No active Quiz for seaon") return redirect("home")
except QuizAlreadyFinished:
if not kwargs.get("from_post"):
messages.error(request, _("Quiz done"))
return render( return redirect(reverse("quiz", kwargs={"season": candidate.season}))
request,
"quiz/question.html", # TODO: On first question -> record time
{"candidate": candidate, "question": question}, if (
GivenAnswer.objects.filter(
candidate=candidate, quiz=candidate.season.active_quiz
).count()
== 0
):
GivenAnswer.objects.create(
candidate=candidate, quiz=question.quiz, answer=None
) )
return self.render_to_response({"candidate": candidate, "question": question})
def post(self, request: HttpRequest, candidate: Candidate, *args, **kwargs): def post(self, request: HttpRequest, candidate: Candidate, *args, **kwargs):
answer_id = request.POST.get("answer") answer_id = request.POST.get("answer")
if answer_id == None: if answer_id == None:
@@ -38,7 +52,8 @@ class QuestionView(View):
raise BadRequest raise BadRequest
GivenAnswer.objects.create( GivenAnswer.objects.create(
candidate=candidate, question=answer.question, answer=answer candidate=candidate,
quiz=answer.question.quiz,
answer=answer,
) )
return self.get(request, candidate, from_post=True)
return self.get(request, candidate, args, kwargs)

View File

@@ -1,9 +1,12 @@
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from django import forms
from django.contrib import messages
from django.http import Http404 from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from django import forms from mypy.dmypy.client import request
from ..models import Season from ..models import Season
@@ -24,6 +27,7 @@ class SelectSeasonView(FormView):
try: try:
season = Season.objects.get(season_code=form.cleaned_data["code"].upper()) season = Season.objects.get(season_code=form.cleaned_data["code"].upper())
except Season.DoesNotExist: except Season.DoesNotExist:
raise Http404("Season does not exist") messages.warning(self.request, _("Invalid season code"))
return redirect("home")
return redirect(reverse("quiz", kwargs={"season": season})) return redirect(reverse("quiz", kwargs={"season": season}))

View File

@@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
from pathlib import Path from pathlib import Path
from django.contrib import messages
from django.utils.translation import gettext_lazy as _ 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'.
@@ -125,6 +127,18 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
MESSAGE_TAGS = {
messages.DEBUG: "alert-info",
messages.INFO: "alert-info",
messages.SUCCESS: "alert-success",
messages.WARNING: "alert-warning",
messages.ERROR: "alert-danger",
100: "alert-primary",
110: "alert-secondary",
120: "alert-light",
130: "alert-dark",
}
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/ # https://docs.djangoproject.com/en/5.1/topics/i18n/

View File

@@ -18,7 +18,7 @@ Including another URLconf
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("", include("quiz.urls")), path("", include("quiz.urls")),