mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 04:44:19 +01:00
Add basic quiz functionality
This commit is contained in:
0
tvdt/quiz/__init__.py
Normal file
0
tvdt/quiz/__init__.py
Normal file
43
tvdt/quiz/admin.py
Normal file
43
tvdt/quiz/admin.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Question, Answer, Candidate, Quiz, Season, GivenAnswer
|
||||
|
||||
|
||||
class CandidatesAdmin(admin.StackedInline):
|
||||
model = Candidate
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(Season)
|
||||
class SeasonAdmin(admin.ModelAdmin):
|
||||
inlines = [CandidatesAdmin]
|
||||
|
||||
|
||||
class QuestionInline(admin.TabularInline):
|
||||
model = Question
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(Quiz)
|
||||
class QuizAdmin(admin.ModelAdmin):
|
||||
inlines = [QuestionInline]
|
||||
|
||||
|
||||
class AnswerInline(admin.TabularInline):
|
||||
model = Answer
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(Question)
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
inlines = [AnswerInline]
|
||||
|
||||
|
||||
@admin.register(Candidate)
|
||||
class CandidateAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(GivenAnswer)
|
||||
class GivenAnswerAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
8
tvdt/quiz/apps.py
Normal file
8
tvdt/quiz/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class QuizConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "quiz"
|
||||
verbose_name = _("quiz")
|
||||
41
tvdt/quiz/converters.py
Normal file
41
tvdt/quiz/converters.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from .models import Season, Candidate
|
||||
|
||||
|
||||
class SeasonCodeConverter:
|
||||
regex = r"[A-Za-z\d]{5}"
|
||||
|
||||
def to_python(self, value: str) -> Season:
|
||||
try:
|
||||
return Season.objects.get(season_code=value.upper())
|
||||
except Season.DoesNotExist:
|
||||
raise ValueError
|
||||
|
||||
def to_url(self, value: Season) -> str:
|
||||
return value.season_code
|
||||
|
||||
|
||||
class CandidateConverter:
|
||||
regex = r"[A-Za-z\d]{5}\/[\w\-=]+"
|
||||
|
||||
def to_python(self, value: str) -> Candidate:
|
||||
season_code, base64_name = value.split("/")
|
||||
|
||||
try:
|
||||
name = base64.urlsafe_b64decode(base64_name).decode()
|
||||
except binascii.Error:
|
||||
raise ValueError
|
||||
|
||||
try:
|
||||
season = Season.objects.get(season_code=season_code)
|
||||
|
||||
candidate = Candidate.objects.get(name=name, season=season)
|
||||
return candidate
|
||||
except [Season.DoesNotExist, Candidate.DoesNotExist]:
|
||||
raise ValueError
|
||||
|
||||
def to_url(self, candidate: Candidate) -> str:
|
||||
base64_candidate = base64.urlsafe_b64encode(candidate.name.encode()).decode()
|
||||
return f"{candidate.season.season_code}/{base64_candidate}"
|
||||
1157
tvdt/quiz/fixtures/krtek.json
Normal file
1157
tvdt/quiz/fixtures/krtek.json
Normal file
File diff suppressed because it is too large
Load Diff
8
tvdt/quiz/helpers.py
Normal file
8
tvdt/quiz/helpers.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def generate_season_code(length: int = 5) -> str:
|
||||
return "".join(
|
||||
random.choice(string.ascii_uppercase + string.digits) for _ in range(length)
|
||||
)
|
||||
122
tvdt/quiz/locale/nl/LC_MESSAGES/django.po
Normal file
122
tvdt/quiz/locale/nl/LC_MESSAGES/django.po
Normal file
@@ -0,0 +1,122 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-20 00:23+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: quiz/models/answer.py:10
|
||||
msgid "text"
|
||||
msgstr "tekst"
|
||||
|
||||
#: quiz/models/answer.py:15 quiz/models/given_answer.py:21
|
||||
#: quiz/models/question.py:9 quiz/models/question.py:21
|
||||
msgid "question"
|
||||
msgstr "vraag"
|
||||
|
||||
#: quiz/models/answer.py:17
|
||||
msgid "is right answer"
|
||||
msgstr "is goede antwoord"
|
||||
|
||||
#: quiz/models/answer.py:19 quiz/models/candidate.py:25
|
||||
msgid "candidates"
|
||||
msgstr "kandidaten"
|
||||
|
||||
#: quiz/models/answer.py:23 quiz/models/given_answer.py:24
|
||||
msgid "answer"
|
||||
msgstr "antwoord"
|
||||
|
||||
#: quiz/models/answer.py:24
|
||||
msgid "answers"
|
||||
msgstr "antwoorden"
|
||||
|
||||
#: quiz/models/candidate.py:15 quiz/models/quiz.py:9 quiz/models/season.py:12
|
||||
msgid "name"
|
||||
msgstr "naam"
|
||||
|
||||
#: quiz/models/candidate.py:24 quiz/models/correction.py:14
|
||||
#: quiz/models/given_answer.py:15 quiz/models/quiz_time.py:11
|
||||
msgid "candidate"
|
||||
msgstr "kandidaat"
|
||||
|
||||
#: quiz/models/correction.py:20 quiz/models/question.py:11
|
||||
#: quiz/models/quiz.py:26 quiz/models/quiz_time.py:13
|
||||
msgid "quiz"
|
||||
msgstr "test"
|
||||
|
||||
#: quiz/models/correction.py:25
|
||||
msgid "correction"
|
||||
msgstr "joker"
|
||||
|
||||
#: quiz/models/correction.py:26
|
||||
msgid "corrections"
|
||||
msgstr "jokers"
|
||||
|
||||
#: quiz/models/given_answer.py:30
|
||||
msgid "given answer"
|
||||
msgstr "gegeven antwoord"
|
||||
|
||||
#: quiz/models/given_answer.py:31
|
||||
msgid "given answers"
|
||||
msgstr "gegeven antwoorden"
|
||||
|
||||
#: quiz/models/question.py:13
|
||||
msgid "number"
|
||||
msgstr "nummer"
|
||||
|
||||
#: quiz/models/question.py:14
|
||||
msgid "enabled"
|
||||
msgstr "ingeschakeld"
|
||||
|
||||
#: quiz/models/question.py:22
|
||||
msgid "questions"
|
||||
msgstr "vraag"
|
||||
|
||||
#: quiz/models/quiz.py:14 quiz/models/season.py:38
|
||||
msgid "season"
|
||||
msgstr "seizoen"
|
||||
|
||||
#: quiz/models/quiz.py:27
|
||||
msgid "quizzes"
|
||||
msgstr "tests"
|
||||
|
||||
#: quiz/models/quiz_time.py:14
|
||||
msgid "seconds"
|
||||
msgstr "seconden"
|
||||
|
||||
#: quiz/models/quiz_time.py:17
|
||||
msgid "quiz time"
|
||||
msgstr "testtijd"
|
||||
|
||||
#: quiz/models/quiz_time.py:18
|
||||
msgid "quiz times"
|
||||
msgstr "testtijden"
|
||||
|
||||
#: quiz/models/season.py:19
|
||||
msgid "active quiz"
|
||||
msgstr "actieve test"
|
||||
|
||||
#: quiz/models/season.py:23
|
||||
msgid "season code"
|
||||
msgstr "seizoencode"
|
||||
|
||||
#: quiz/models/season.py:26
|
||||
msgid "preregister candidates"
|
||||
msgstr "kandidaten voorregistreren"
|
||||
|
||||
#: quiz/models/season.py:39
|
||||
msgid "seasons"
|
||||
msgstr "seizoenen"
|
||||
311
tvdt/quiz/migrations/0001_initial.py
Normal file
311
tvdt/quiz/migrations/0001_initial.py
Normal file
@@ -0,0 +1,311 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-19 21:54
|
||||
|
||||
import django.db.models.deletion
|
||||
import quiz.helpers
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Candidate",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=16, verbose_name="naam")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "candidate",
|
||||
"verbose_name_plural": "candidates",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Question",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("question", models.CharField(max_length=256, verbose_name="question")),
|
||||
("number", models.PositiveSmallIntegerField(verbose_name="number")),
|
||||
("enabled", models.BooleanField(default=True, verbose_name="enabled")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "question",
|
||||
"verbose_name_plural": "questions",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Quiz",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=64, verbose_name="naam")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "quiz",
|
||||
"verbose_name_plural": "quizzes",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Answer",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("text", models.CharField(max_length=64, verbose_name="text")),
|
||||
(
|
||||
"is_right_answer",
|
||||
models.BooleanField(verbose_name="is right answer"),
|
||||
),
|
||||
(
|
||||
"candidates",
|
||||
models.ManyToManyField(
|
||||
to="quiz.candidate", verbose_name="candidates"
|
||||
),
|
||||
),
|
||||
(
|
||||
"question",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="answers",
|
||||
to="quiz.question",
|
||||
verbose_name="question",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "answer",
|
||||
"verbose_name_plural": "answers",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="question",
|
||||
name="quiz",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="questions",
|
||||
to="quiz.quiz",
|
||||
verbose_name="quiz",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="QuizTime",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("seconds", models.PositiveIntegerField(verbose_name="seconds")),
|
||||
(
|
||||
"candidate",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="quiz.candidate",
|
||||
verbose_name="candidate",
|
||||
),
|
||||
),
|
||||
(
|
||||
"quiz",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="quiz.quiz",
|
||||
verbose_name="quiz",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "quiz time",
|
||||
"verbose_name_plural": "quiz times",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Season",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=64, verbose_name="naam")),
|
||||
(
|
||||
"season_code",
|
||||
models.CharField(
|
||||
default=quiz.helpers.generate_season_code,
|
||||
max_length=5,
|
||||
verbose_name="season code",
|
||||
),
|
||||
),
|
||||
(
|
||||
"active_quiz",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="quiz.quiz",
|
||||
verbose_name="active quiz",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "season",
|
||||
"verbose_name_plural": "seasons",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="quiz",
|
||||
name="season",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="quizzes",
|
||||
to="quiz.season",
|
||||
verbose_name="season",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="candidate",
|
||||
name="season",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="candidates",
|
||||
to="quiz.season",
|
||||
verbose_name="season",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="GivenAnswer",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"answer",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="quiz.answer",
|
||||
verbose_name="answer",
|
||||
),
|
||||
),
|
||||
(
|
||||
"candidate",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="answers",
|
||||
to="quiz.candidate",
|
||||
verbose_name="candidate",
|
||||
),
|
||||
),
|
||||
(
|
||||
"question",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="given_answers",
|
||||
to="quiz.question",
|
||||
verbose_name="question",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "given answer",
|
||||
"verbose_name_plural": "given answers",
|
||||
"unique_together": {("candidate", "question")},
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="question",
|
||||
unique_together={("quiz", "number")},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Correction",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"candidate",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="corrections_used",
|
||||
to="quiz.candidate",
|
||||
verbose_name="candidate",
|
||||
),
|
||||
),
|
||||
(
|
||||
"quiz",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="corrections_used",
|
||||
to="quiz.quiz",
|
||||
verbose_name="quiz",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "correction",
|
||||
"verbose_name_plural": "corrections",
|
||||
"unique_together": {("candidate", "quiz")},
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="candidate",
|
||||
index=models.Index(
|
||||
fields=["season", "name"], name="quiz_candid_season__d83118_idx"
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="candidate",
|
||||
unique_together={("season", "name")},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-19 22:17
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("quiz", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="season",
|
||||
name="preregister_candidates",
|
||||
field=models.BooleanField(
|
||||
default=True, verbose_name="preregister candidates"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="answer",
|
||||
name="candidates",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="quiz.candidate", verbose_name="candidates"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="candidate",
|
||||
name="name",
|
||||
field=models.CharField(max_length=16, verbose_name="name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="quiz",
|
||||
name="name",
|
||||
field=models.CharField(max_length=64, verbose_name="name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="season",
|
||||
name="active_quiz",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="quiz.quiz",
|
||||
verbose_name="active quiz",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="season",
|
||||
name="name",
|
||||
field=models.CharField(max_length=64, verbose_name="name"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-03 19:21
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("quiz", "0002_season_preregister_candidates_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="givenanswer",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, default=None),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="givenanswer",
|
||||
name="question",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="given_answers",
|
||||
to="quiz.question",
|
||||
verbose_name="question",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-23 16:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("quiz", "0003_givenanswer_created_alter_givenanswer_question"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="question",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterOrderWithRespectTo(
|
||||
name="question",
|
||||
order_with_respect_to="quiz",
|
||||
),
|
||||
migrations.AlterOrderWithRespectTo(
|
||||
name="answer",
|
||||
order_with_respect_to="question",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="question",
|
||||
name="number",
|
||||
),
|
||||
]
|
||||
0
tvdt/quiz/migrations/__init__.py
Normal file
0
tvdt/quiz/migrations/__init__.py
Normal file
8
tvdt/quiz/models/__init__.py
Normal file
8
tvdt/quiz/models/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .answer import Answer
|
||||
from .candidate import Candidate
|
||||
from .correction import Correction
|
||||
from .given_answer import GivenAnswer
|
||||
from .question import Question
|
||||
from .quiz import Quiz
|
||||
from .quiz_time import QuizTime
|
||||
from .season import Season
|
||||
23
tvdt/quiz/models/answer.py
Normal file
23
tvdt/quiz/models/answer.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
|
||||
class Answer(models.Model):
|
||||
text = models.CharField(max_length=64, verbose_name=_("text"))
|
||||
question = models.ForeignKey(
|
||||
"Question",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="answers",
|
||||
verbose_name=_("question"),
|
||||
)
|
||||
is_right_answer = models.BooleanField(verbose_name=_("is right answer"))
|
||||
candidates = models.ManyToManyField(
|
||||
"Candidate", verbose_name=_("candidates"), blank=True
|
||||
)
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
verbose_name = _("answer")
|
||||
verbose_name_plural = _("answers")
|
||||
|
||||
order_with_respect_to = "question"
|
||||
48
tvdt/quiz/models/candidate.py
Normal file
48
tvdt/quiz/models/candidate.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from typing import Self
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
from .given_answer import GivenAnswer
|
||||
from .question import NoActiveTestForSeason, QuizAlreadyFinished, Question
|
||||
|
||||
|
||||
class Candidate(models.Model):
|
||||
season = models.ForeignKey(
|
||||
"Season",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="candidates",
|
||||
verbose_name="season",
|
||||
)
|
||||
name = models.CharField(max_length=16, verbose_name=_("name"))
|
||||
|
||||
def get_next_question(self, candidate: Self) -> "Question":
|
||||
quiz = candidate.season.active_quiz
|
||||
if quiz is None:
|
||||
raise NoActiveTestForSeason()
|
||||
|
||||
question = (
|
||||
Question.objects.filter(quiz=quiz)
|
||||
.exclude(
|
||||
id__in=GivenAnswer.objects.filter(
|
||||
candidate=candidate, question__quiz=quiz
|
||||
).values_list("question_id", flat=True)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if question is None:
|
||||
raise QuizAlreadyFinished()
|
||||
|
||||
return question
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} ({self.season})"
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
unique_together = ["season", "name"]
|
||||
indexes = [models.Index(fields=["season", "name"])]
|
||||
|
||||
verbose_name = _("candidate")
|
||||
verbose_name_plural = _("candidates")
|
||||
26
tvdt/quiz/models/correction.py
Normal file
26
tvdt/quiz/models/correction.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
from .candidate import Candidate
|
||||
from .quiz import Quiz
|
||||
|
||||
|
||||
class Correction(models.Model):
|
||||
candidate = models.ForeignKey(
|
||||
"Candidate",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="corrections_used",
|
||||
verbose_name=_("candidate"),
|
||||
)
|
||||
quiz = models.ForeignKey(
|
||||
"Quiz",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="corrections_used",
|
||||
verbose_name=_("quiz"),
|
||||
)
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
unique_together = ("candidate", "quiz")
|
||||
verbose_name = _("correction")
|
||||
verbose_name_plural = _("corrections")
|
||||
29
tvdt/quiz/models/given_answer.py
Normal file
29
tvdt/quiz/models/given_answer.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
|
||||
class GivenAnswer(models.Model):
|
||||
candidate = models.ForeignKey(
|
||||
"Candidate",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="answers",
|
||||
verbose_name=_("candidate"),
|
||||
)
|
||||
question = models.ForeignKey(
|
||||
"Question",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
related_name="given_answers",
|
||||
verbose_name=_("question"),
|
||||
)
|
||||
answer = models.ForeignKey(
|
||||
"Answer", on_delete=models.CASCADE, verbose_name=_("answer")
|
||||
)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
unique_together = ["candidate", "question"]
|
||||
|
||||
verbose_name = _("given answer")
|
||||
verbose_name_plural = _("given answers")
|
||||
31
tvdt/quiz/models/question.py
Normal file
31
tvdt/quiz/models/question.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
|
||||
class NoActiveTestForSeason(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class QuizAlreadyFinished(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Question(models.Model):
|
||||
question = models.CharField(max_length=256, verbose_name=_("question"))
|
||||
quiz = models.ForeignKey(
|
||||
"Quiz",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="questions",
|
||||
verbose_name=_("quiz"),
|
||||
)
|
||||
enabled = models.BooleanField(default=True, verbose_name=_("enabled"))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self._order + 1}. {self.question} ({self.quiz}) ({self.answers.count()} answers, {self.answers.filter(is_right_answer=True).count()} correct)"
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
verbose_name = _("question")
|
||||
verbose_name_plural = _("questions")
|
||||
|
||||
order_with_respect_to = "quiz"
|
||||
25
tvdt/quiz/models/quiz.py
Normal file
25
tvdt/quiz/models/quiz.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
|
||||
class Quiz(models.Model):
|
||||
name = models.CharField(max_length=64, verbose_name=_("name"))
|
||||
season = models.ForeignKey(
|
||||
"Season",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="quizzes",
|
||||
verbose_name=_("season"),
|
||||
)
|
||||
|
||||
def is_valid_quiz(self) -> bool:
|
||||
pass
|
||||
# Check > 0 active questions
|
||||
# Check every question 1 right answer
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.season.name} - {self.name}"
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
verbose_name = _("quiz")
|
||||
verbose_name_plural = _("quizzes")
|
||||
15
tvdt/quiz/models/quiz_time.py
Normal file
15
tvdt/quiz/models/quiz_time.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
|
||||
class QuizTime(models.Model):
|
||||
candidate = models.ForeignKey(
|
||||
"Candidate", on_delete=models.CASCADE, verbose_name=_("candidate")
|
||||
)
|
||||
quiz = models.ForeignKey("Quiz", on_delete=models.CASCADE, verbose_name=_("quiz"))
|
||||
seconds = models.PositiveIntegerField(verbose_name=_("seconds"))
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
verbose_name = _("quiz time")
|
||||
verbose_name_plural = _("quiz times")
|
||||
39
tvdt/quiz/models/season.py
Normal file
39
tvdt/quiz/models/season.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
from ..helpers import generate_season_code
|
||||
|
||||
|
||||
class Season(models.Model):
|
||||
name = models.CharField(max_length=64, verbose_name=_("name"))
|
||||
active_quiz = models.ForeignKey(
|
||||
"Quiz",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name=_("active quiz"),
|
||||
related_name="+",
|
||||
)
|
||||
season_code = models.CharField(
|
||||
max_length=5, default=generate_season_code, verbose_name=_("season code")
|
||||
)
|
||||
preregister_candidates = models.BooleanField(
|
||||
default=True, verbose_name=_("preregister candidates")
|
||||
)
|
||||
|
||||
def renew_season_code(self) -> str:
|
||||
self.season_code = generate_season_code()
|
||||
self.save()
|
||||
return self.season_code
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
verbose_name = _("season")
|
||||
verbose_name_plural = _("seasons")
|
||||
BIN
tvdt/quiz/static/quiz/background.png
Normal file
BIN
tvdt/quiz/static/quiz/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
33
tvdt/quiz/templates/quiz/base.html
Normal file
33
tvdt/quiz/templates/quiz/base.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
{# <script src="https://cdn.tailwindcss.com"></script>#}
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"></script>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
background-image: url("{% static "quiz/background.png" %}");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-color: black;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-self: center;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{% block script %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
9
tvdt/quiz/templates/quiz/enter_name.html
Normal file
9
tvdt/quiz/templates/quiz/enter_name.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "quiz/base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<p>{{ season.name }} ({{ season.season_code }})</p>
|
||||
{% crispy form %}
|
||||
|
||||
{% endblock %}
|
||||
14
tvdt/quiz/templates/quiz/question.html
Normal file
14
tvdt/quiz/templates/quiz/question.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends 'quiz/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<h2>{{ question.question }}</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for answer in question.answers.all %}
|
||||
<div><button class="btn btn-outline-success" type="submit" name="answer" value="{{ answer.id }}">{{ answer.text }}</button></div>
|
||||
{% empty %}
|
||||
{% translate "Weirdly enough this question has no answers..." %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
7
tvdt/quiz/templates/quiz/select_season.html
Normal file
7
tvdt/quiz/templates/quiz/select_season.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'quiz/base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% translate "Tijd voor de test" %}{% endblock %}
|
||||
{% block body %}
|
||||
{% crispy form %}
|
||||
{% endblock %}
|
||||
3
tvdt/quiz/tests.py
Normal file
3
tvdt/quiz/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
tvdt/quiz/urls.py
Normal file
14
tvdt/quiz/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.urls import path, register_converter
|
||||
|
||||
from .converters import CandidateConverter, SeasonCodeConverter
|
||||
from .views import SelectSeasonView
|
||||
from .views.questionview import QuestionView
|
||||
from .views.enternameview import EnterNameView
|
||||
|
||||
register_converter(SeasonCodeConverter, "season")
|
||||
register_converter(CandidateConverter, "candidate")
|
||||
urlpatterns = [
|
||||
path("", SelectSeasonView.as_view(), name="home"),
|
||||
path("<season:season>/", EnterNameView.as_view(), name="quiz"),
|
||||
path("<candidate:candidate>/", QuestionView.as_view(), name="question"),
|
||||
]
|
||||
1
tvdt/quiz/views/__init__.py
Normal file
1
tvdt/quiz/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .selectseasonview import SelectSeasonView
|
||||
51
tvdt/quiz/views/enternameview.py
Normal file
51
tvdt/quiz/views/enternameview.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from crispy_forms.helper import FormHelper
|
||||
from django import forms
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render, redirect
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from ..models import Season, Candidate
|
||||
|
||||
|
||||
class EnterNameForm(forms.Form):
|
||||
name = forms.CharField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
|
||||
|
||||
class EnterNameView(View):
|
||||
template_name = "quiz/enter_name.html"
|
||||
forms_class = EnterNameForm
|
||||
|
||||
def get(self, request, season: Season, *args, **kwargs):
|
||||
if season.active_quiz == None:
|
||||
raise Http404("No quiz active")
|
||||
|
||||
return render(
|
||||
request,
|
||||
self.template_name,
|
||||
{"form": self.forms_class(), "season": season},
|
||||
)
|
||||
|
||||
def post(self, request, season: Season, *args, **kwargs):
|
||||
name = request.POST.get("name")
|
||||
|
||||
if season.preregister_candidates:
|
||||
try:
|
||||
candidate = Candidate.objects.get(season=season, name=name)
|
||||
except Candidate.DoesNotExist:
|
||||
raise Http404("Candidate not found")
|
||||
else:
|
||||
candidate, created = Candidate.objects.get_or_create(
|
||||
season=season, name=name
|
||||
)
|
||||
|
||||
return redirect(
|
||||
reverse(
|
||||
"question",
|
||||
kwargs={"candidate": candidate},
|
||||
)
|
||||
)
|
||||
44
tvdt/quiz/views/questionview.py
Normal file
44
tvdt/quiz/views/questionview.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
|
||||
from ..models import Candidate, Answer, GivenAnswer
|
||||
from ..models.question import NoActiveTestForSeason
|
||||
|
||||
|
||||
class QuestionView(View):
|
||||
template_name = "quiz/question.html"
|
||||
|
||||
def get(
|
||||
self, request: HttpRequest, candidate: Candidate, *args, **kwargs
|
||||
) -> HttpResponse:
|
||||
try:
|
||||
question = candidate.get_next_question(candidate)
|
||||
except NoActiveTestForSeason:
|
||||
messages.error(request, _("No active Quiz for season"))
|
||||
raise Http404("No active Quiz for seaon")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"quiz/question.html",
|
||||
{"candidate": candidate, "question": question},
|
||||
)
|
||||
|
||||
def post(self, request: HttpRequest, candidate: Candidate, *args, **kwargs):
|
||||
answer_id = request.POST.get("answer")
|
||||
if answer_id == None:
|
||||
raise BadRequest
|
||||
|
||||
try:
|
||||
answer = Answer.objects.get(id=answer_id)
|
||||
except Answer.DoesNotExist:
|
||||
raise BadRequest
|
||||
|
||||
GivenAnswer.objects.create(
|
||||
candidate=candidate, question=answer.question, answer=answer
|
||||
)
|
||||
|
||||
return self.get(request, candidate, args, kwargs)
|
||||
29
tvdt/quiz/views/selectseasonview.py
Normal file
29
tvdt/quiz/views/selectseasonview.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from crispy_forms.helper import FormHelper
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.views.generic.edit import FormView
|
||||
from django import forms
|
||||
|
||||
from ..models import Season
|
||||
|
||||
|
||||
class SelectSeasonForm(forms.Form):
|
||||
code = forms.CharField(max_length=5)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
|
||||
|
||||
class SelectSeasonView(FormView):
|
||||
form_class = SelectSeasonForm
|
||||
template_name = "quiz/select_season.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
season = Season.objects.get(season_code=form.cleaned_data["code"].upper())
|
||||
except Season.DoesNotExist:
|
||||
raise Http404("Season does not exist")
|
||||
|
||||
return redirect(reverse("quiz", kwargs={"season": season}))
|
||||
Reference in New Issue
Block a user