This commit is contained in:
2024-12-11 23:22:09 +01:00
parent 4b86b33872
commit 9b7944c14d
53 changed files with 2054 additions and 249 deletions

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Answer, Candidate, GivenAnswer, Question, Quiz, Season
from .models import Answer, Candidate, Correction, GivenAnswer, Question, Quiz, Season
class CandidatesAdmin(admin.StackedInline):
@@ -30,6 +30,8 @@ class AnswerInline(admin.TabularInline):
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
list_display = ["question", "quiz__season__name", "quiz__name", "_order"]
ordering = ["quiz__season", "quiz", "_order"]
inlines = [AnswerInline]
@@ -41,3 +43,8 @@ class CandidateAdmin(admin.ModelAdmin):
@admin.register(GivenAnswer)
class GivenAnswerAdmin(admin.ModelAdmin):
pass
@admin.register(Correction)
class CorrextionAdmin(admin.ModelAdmin):
pass

View File

@@ -0,0 +1,2 @@
def get_theme(request) -> dict:
return {"theme": "wie_is_de_mol"}

View File

@@ -16,7 +16,7 @@ class CandidateConverter:
raise ValueError
try:
season = Season.objects.aget(season_code=season_code)
season = Season.objects.get(season_code=season_code)
candidate = Candidate.objects.get(name=name, season=season)
return candidate

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-25 19:18+0100\n"
"POT-Creation-Date: 2024-12-11 23:22+0100\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"
@@ -19,36 +19,36 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: quiz/apps.py:8 quiz/models/correction.py:17 quiz/models/given_answer.py:19
#: quiz/models/question.py:20 quiz/models/quiz.py:24
#: quiz/models/question.py:23 quiz/models/quiz.py:60
msgid "quiz"
msgstr "test"
#: quiz/models/answer.py:7
#: quiz/models/answer.py:10
msgid "text"
msgstr "tekst"
#: quiz/models/answer.py:12 quiz/models/question.py:15
#: quiz/models/question.py:28
#: quiz/models/answer.py:15 quiz/models/question.py:18
#: quiz/models/question.py:58
msgid "question"
msgstr "vraag"
#: quiz/models/answer.py:14
#: quiz/models/answer.py:17
msgid "is right answer"
msgstr "is goede antwoord"
#: quiz/models/answer.py:16 quiz/models/candidate.py:50
#: quiz/models/answer.py:19 quiz/models/candidate.py:50
msgid "candidates"
msgstr "kandidaten"
#: quiz/models/answer.py:20 quiz/models/given_answer.py:25
#: quiz/models/answer.py:23 quiz/models/given_answer.py:25
msgid "answer"
msgstr "antwoord"
#: quiz/models/answer.py:21
#: quiz/models/answer.py:24
msgid "answers"
msgstr "antwoorden"
#: quiz/models/candidate.py:18 quiz/models/quiz.py:7 quiz/models/season.py:12
#: quiz/models/candidate.py:18 quiz/models/quiz.py:12 quiz/models/season.py:12
msgid "name"
msgstr "naam"
@@ -57,11 +57,15 @@ msgstr "naam"
msgid "candidate"
msgstr "kandidaat"
#: quiz/models/correction.py:22
#: quiz/models/correction.py:19
msgid "amount"
msgstr "aantal"
#: quiz/models/correction.py:23
msgid "correction"
msgstr "joker"
#: quiz/models/correction.py:23
#: quiz/models/correction.py:24
msgid "corrections"
msgstr "jokers"
@@ -73,19 +77,35 @@ msgstr "gegeven antwoord"
msgid "given answers"
msgstr "gegeven antwoorden"
#: quiz/models/question.py:22
#: quiz/models/question.py:25
msgid "enabled"
msgstr "actief"
#: quiz/models/question.py:29
#: quiz/models/question.py:42
msgid "Error: Question has no answers"
msgstr "Fout: Raar genoeg heeft deze vraag geen antwoorden..."
#: quiz/models/question.py:47
msgid "Error: This question has no right answer!"
msgstr "Fout: Raar genoeg heeft deze vraag geen antwoorden..."
#: quiz/models/question.py:50
msgid "Warning: This question has multiple correct answers"
msgstr "Waarschuwing: Raar genoeg heeft deze vraag geen antwoorden..."
#: quiz/models/question.py:59
msgid "questions"
msgstr "vraag"
#: quiz/models/quiz.py:12 quiz/models/season.py:38
#: quiz/models/quiz.py:17 quiz/models/season.py:43
msgid "season"
msgstr "seizoen"
#: quiz/models/quiz.py:25
#: quiz/models/quiz.py:21
msgid "dropouts"
msgstr "afvallers"
#: quiz/models/quiz.py:61
msgid "quizzes"
msgstr "tests"
@@ -101,27 +121,31 @@ msgstr "seizoencode"
msgid "preregister candidates"
msgstr "kandidaten voorregistreren"
#: quiz/models/season.py:39
#: quiz/models/season.py:30
msgid "owners"
msgstr "eigenaren"
#: quiz/models/season.py:44
msgid "seasons"
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
#: quiz/templates/quiz/base.html:16
msgid "Tijd voor de test"
msgstr "Tijd voor de test"
#: quiz/views/enternameview.py:14
#: quiz/templates/quiz/question.html:15
msgid "Weirdly enough this question has no answers..."
msgstr "Raar genoeg heeft deze vraag geen antwoorden..."
#: quiz/views/enternameview.py:15
msgid "Name"
msgstr "Naam"
#: quiz/views/enternameview.py:27
#: quiz/views/enternameview.py:28
msgid "This season has no active quiz."
msgstr "Dit seizoen heeft geen actieve test."
#: quiz/views/enternameview.py:39
#: quiz/views/enternameview.py:40
msgid "Candidate does not exist"
msgstr "Kandidaat bestaat niet"

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.1.3 on 2024-11-30 18:21
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="season",
name="owner",
field=models.ManyToManyField(
related_name="seasons",
to=settings.AUTH_USER_MODEL,
verbose_name="owners",
),
),
]

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.1.3 on 2024-12-01 14:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0002_season_owner"),
]
operations = [
migrations.AddField(
model_name="correction",
name="amount",
field=models.FloatField(default=1, verbose_name="amount"),
),
migrations.AlterField(
model_name="correction",
name="candidate",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="corrections",
to="quiz.candidate",
verbose_name="candidate",
),
),
migrations.AlterField(
model_name="correction",
name="quiz",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="corrections",
to="quiz.quiz",
verbose_name="quiz",
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2024-12-01 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0003_correction_amount_alter_correction_candidate_and_more"),
]
operations = [
migrations.AddField(
model_name="quiz",
name="dropouts",
field=models.PositiveSmallIntegerField(default=1, verbose_name="dropouts"),
),
]

View File

@@ -1,8 +1,11 @@
from typing import final
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
@final
class Answer(models.Model):
text = models.CharField(max_length=64, verbose_name=_("text"))
question = models.ForeignKey(

View File

@@ -7,15 +7,16 @@ class Correction(models.Model):
candidate = models.ForeignKey(
"Candidate",
on_delete=models.CASCADE,
related_name="corrections_used",
related_name="corrections",
verbose_name=_("candidate"),
)
quiz = models.ForeignKey(
"Quiz",
on_delete=models.CASCADE,
related_name="corrections_used",
related_name="corrections",
verbose_name=_("quiz"),
)
amount = models.FloatField(verbose_name=_("amount"), default=1)
class Meta(TypedModelMeta):
unique_together = ("candidate", "quiz")

View File

@@ -1,7 +1,10 @@
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
from quiz.models import Answer
class NoActiveTestForSeason(Exception):
pass
@@ -21,6 +24,33 @@ class Question(models.Model):
)
enabled = models.BooleanField(default=True, verbose_name=_("enabled"))
@property
def order(self):
return self._order
@property
def right_answer(self) -> QuerySet[Answer]:
return self.answers.filter(is_right_answer=True)
@property
def has_right_answer(self) -> bool:
return self.answers.filter(is_right_answer=True).count() > 0
@property
def errors(self) -> str | None:
if self.answers.count() == 0:
return _("Error: Question has no answers")
n_correct_answers = self.answers.filter(is_right_answer=True).count()
if n_correct_answers == 0:
return _("Error: This question has no right answer!")
if n_correct_answers > 1:
return _("Warning: This question has multiple correct answers")
return None
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)"

View File

@@ -1,7 +1,12 @@
from django.db import models
from django.db.models import F, OuterRef, Subquery
from django.db.models.aggregates import Count, Max, Min
from django.db.models.functions import Coalesce
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
from quiz.models import Candidate, Correction
class Quiz(models.Model):
name = models.CharField(max_length=64, verbose_name=_("name"))
@@ -12,11 +17,42 @@ class Quiz(models.Model):
verbose_name=_("season"),
)
dropouts = models.PositiveSmallIntegerField(
verbose_name=_("dropouts"),
default=1,
)
def is_valid_quiz(self) -> bool:
return True
# Check > 0 active questions
# Check every question 1 right answer
def get_score(self):
time_query = (
Candidate.objects.filter(id=OuterRef("id"), answers__quiz=self)
.annotate(time=Max("answers__created") - Min("answers__created"))
.values("time")
)
corrections = Correction.objects.filter(
quiz=self, candidate=OuterRef("id")
).values("amount")
scores = (
Candidate.objects.filter(
answers__answer__is_right_answer=True,
answers__quiz=self,
)
.values("id", "name")
.annotate(
correct=Count("answers"),
corrections=Coalesce(Subquery(corrections), 0.0),
score=F("correct") + F("corrections"),
time=Subquery(time_query),
)
.order_by("-score", "time")
)
return scores
def __str__(self) -> str:
return f"{self.season.name} - {self.name}"

View File

@@ -1,12 +1,12 @@
import random
import string
from django.contrib.auth import get_user_model
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
User = get_user_model()
class Season(models.Model):
name = models.CharField(max_length=64, verbose_name=_("name"))
@@ -25,6 +25,11 @@ class Season(models.Model):
preregister_candidates = models.BooleanField(
default=True, verbose_name=_("preregister candidates")
)
owner = models.ManyToManyField(
User,
verbose_name=_("owners"),
related_name="seasons",
)
def renew_season_code(self) -> str:
self.season_code = generate_season_code()

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

View File

@@ -1,18 +1,27 @@
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<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>
<head>
<meta charset="UTF-8">
<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 %}
{% translate "Tijd voor de test" %}
{% endblock title %}
</title>
<style>
html, body {
height: 100%;
background-image: url("{% static "quiz/background.png" %}");
{% with "quiz/"|add:theme|add:"/background.png" as background %}
background-image: url("{% static background %}");
{% endwith %}
background-position: center center;
background-repeat: no-repeat;
background-color: black;
@@ -26,23 +35,17 @@
.asteriskField {
display: none;
}
</style>
</head>
<body>
<main>
<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 %}
</div>
{% block script %}{% endblock %}
</main>
</body>
</style>
</head>
<body>
<main>
<div class="container">
{% include "messages.html" %}
{% block body %}
{% endblock body %}
</div>
{% block script %}
{% endblock script %}
</main>
</body>
</html>

View File

@@ -1,8 +1,5 @@
{% extends "quiz/base.html" %}
{% load crispy_forms_tags %}
{% block body %}
{% crispy form %}
{% endblock %}
{% endblock body %}

View File

@@ -1,14 +1,18 @@
{% extends 'quiz/base.html' %}
{% 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 %}
{% 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 %}
{% endblock body %}

View File

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

View File

@@ -10,7 +10,8 @@ from .views.questionview import QuestionView
register_converter(SeasonCodeConverter, "season")
register_converter(CandidateConverter, "candidate")
urlpatterns = [
path("", SelectSeasonView.as_view(), name="home"),
path("<season:season>/", EnterNameView.as_view(), name="quiz"),
path("", SelectSeasonView.as_view(), name="index"),
path("<season:season>/", EnterNameView.as_view(), name="enter_name"),
path("<candidate:candidate>/", QuestionView.as_view(), name="question"),
# path("<>")
]

View File

@@ -26,7 +26,7 @@ class QuestionView(View, TemplateResponseMixin):
if not kwargs.get("from_post"):
messages.error(request, _("Quiz done"))
return redirect(reverse("quiz", kwargs={"season": candidate.season}))
return redirect(reverse("enter_name", kwargs={"season": candidate.season}))
# TODO: On first question -> record time
if (

View File

@@ -36,4 +36,4 @@ class SelectSeasonView(FormView):
env.read_env()
print(env.dump())
return redirect(reverse("quiz", kwargs={"season": season}))
return redirect(reverse("enter_name", kwargs={"season": season}))