1
0

vault backup: 2025-12-22 02:56:57
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m13s

This commit is contained in:
2025-12-22 02:56:57 +01:00
parent 3ae6c10557
commit 5a2b99c99a
26 changed files with 208 additions and 22 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,26 @@
# Generated by Django 6.0 on 2025-12-22 01:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('quiz', '0005_course_exam_question_exam'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
],
),
migrations.AddField(
model_name='question',
name='tags',
field=models.ManyToManyField(blank=True, related_name='questions', to='quiz.tag'),
),
]

View File

@@ -46,11 +46,20 @@ class Question(models.Model):
file_mtime = models.FloatField(null=True, blank=True) # Track file modification time
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
tags = models.ManyToManyField('Tag', blank=True, related_name='questions')
def __str__(self):
return self.text[:50]
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True)
def __str__(self):
return self.name
class Option(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='options')
letter = models.CharField(max_length=1)

View File

@@ -80,6 +80,7 @@ def parse_markdown_question(file_path: Path, content: str) -> Tuple[bool, dict]:
- options: list of (letter, text) tuples
- correct_answer: the correct answer letter(s)
- has_answer: whether it has an answer (not TODO)
- tags: list of tag strings
"""
lines = content.split('\n')
@@ -104,7 +105,15 @@ def parse_markdown_question(file_path: Path, content: str) -> Tuple[bool, dict]:
question_type = 'textalternativ'
elif 'frågetyp/textfält' in line:
question_type = 'textfält'
break
elif in_frontmatter and line.strip().lower().startswith('tags:'):
# Extract tags
# Handle: tags: [tag1, tag2] or tags: tag1, tag2
tag_content = line.split(':', 1)[1].strip()
# Remove brackets if present
tag_content = tag_content.strip('[]')
# Split by comma
tags = [t.strip() for t in tag_content.split(',') if t.strip()]
if not is_question:
return False, {}
@@ -138,7 +147,9 @@ def parse_markdown_question(file_path: Path, content: str) -> Tuple[bool, dict]:
'options': [],
'correct_answer': '',
'has_answer': False,
'question_type': question_type
'question_type': question_type,
'tags': tags if 'tags' in locals() else []
}
}
# Extract options (pattern: "- A:" or "- A" for MCQ, or text for textalternativ)
@@ -244,7 +255,9 @@ def parse_markdown_question(file_path: Path, content: str) -> Tuple[bool, dict]:
'options': options_data,
'correct_answer': correct_answer,
'has_answer': has_answer,
'question_type': question_type
'question_type': question_type,
'tags': tags if 'tags' in locals() else []
}
}
@@ -368,6 +381,19 @@ def import_question_file(file_path: Path, base_path: Path, stats: ImportStats, f
stats.created += 1
else:
stats.updated += 1
# Update tags
from django.utils.text import slugify
from quiz.models import Tag
question.tags.clear()
for tag_name in question_data.get('tags', []):
tag_slug = slugify(tag_name)
tag, _ = Tag.objects.get_or_create(
slug=tag_slug,
defaults={'name': tag_name}
)
question.tags.add(tag)
# Update options
question.options.all().delete()

View File

@@ -2,23 +2,46 @@ from django.http import HttpResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from .models import Question, QuizResult
from .models import Question, QuizResult, Tag
def handle_tag_filter(request):
tag_slug = request.GET.get('tag')
if tag_slug is not None:
if tag_slug == "":
if 'quiz_tag' in request.session:
del request.session['quiz_tag']
else:
request.session['quiz_tag'] = tag_slug
def index(request):
handle_tag_filter(request)
total_questions = Question.objects.count()
answered_count = QuizResult.objects.filter(user=request.quiz_user).count()
context = {
'total_questions': total_questions,
'answered_count': answered_count,
'tags': Tag.objects.all(),
'current_tag': request.session.get('quiz_tag'),
}
return render(request, 'index.html', context)
def get_next_question(request):
# Handle tag filtering
handle_tag_filter(request)
current_tag = request.session.get('quiz_tag')
answered_ids = QuizResult.objects.filter(user=request.quiz_user).values_list('question_id', flat=True)
next_question = Question.objects.exclude(id__in=answered_ids).first()
questions = Question.objects.exclude(id__in=answered_ids)
if current_tag:
questions = questions.filter(tags__slug=current_tag)
next_question = questions.first()
if not next_question:
return render(request, 'partials/complete.html')

View File

@@ -1,6 +1,42 @@
{% extends "base.html" %}
{% block content %}
<style>
.filter-section { margin-bottom: 20px; }
.tag-chip {
display: inline-block;
padding: 5px 12px;
margin: 4px;
border-radius: 16px;
background: #e0e0e0;
text-decoration: none;
color: #333;
font-size: 14px;
transition: background 0.2s;
}
.tag-chip.active {
background: #4CAF50;
color: white;
}
.tag-chip:hover {
background: #d5d5d5;
}
.tag-chip.active:hover {
background: #45a049;
}
</style>
<h1>Quiz Application</h1>
<div class="filter-section">
<a href="?tag=" class="tag-chip {% if not current_tag %}active{% endif %}">All</a>
{% for tag in tags %}
<a href="?tag={{ tag.slug }}" class="tag-chip {% if current_tag == tag.slug %}active{% endif %}">
{{ tag.name }}
</a>
{% endfor %}
</div>
<div class="progress">
<div class="progress-bar" style="width: {% if total_questions > 0 %}{{ answered_count|floatformat:0 }}{% else %}0{% endif %}%"></div>
</div>

83
quiz/uv.lock generated Normal file
View File

@@ -0,0 +1,83 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "asgiref"
version = "3.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
]
[[package]]
name = "django"
version = "6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/15/75/19762bfc4ea556c303d9af8e36f0cd910ab17dff6c8774644314427a2120/django-6.0.tar.gz", hash = "sha256:7b0c1f50c0759bbe6331c6a39c89ae022a84672674aeda908784617ef47d8e26", size = 10932418, upload-time = "2025-12-03T16:26:21.878Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/ae/f19e24789a5ad852670d6885f5480f5e5895576945fcc01817dfd9bc002a/django-6.0-py3-none-any.whl", hash = "sha256:1cc2c7344303bbfb7ba5070487c17f7fc0b7174bbb0a38cebf03c675f5f19b6d", size = 8339181, upload-time = "2025-12-03T16:26:16.231Z" },
]
[[package]]
name = "quiz"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "django" },
{ name = "watchdog" },
]
[package.metadata]
requires-dist = [
{ name = "django", specifier = ">=6.0.0" },
{ name = "watchdog", specifier = ">=6.0.0" },
]
[package.metadata.requires-dev]
dev = []
[[package]]
name = "sqlparse"
version = "0.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
]
[[package]]
name = "tzdata"
version = "2025.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]