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

View File

@@ -1,17 +0,0 @@
# Conflicts
Please resolve them and commit them using the commands `Git: Commit all changes` followed by `Git: Push`
(This file will automatically be deleted before commit)
[[#Additional Instructions]] available below file list
- Not a file: content/.obsidian/workspace.json
# Additional Instructions
I strongly recommend to use "Source mode" for viewing the conflicted files. For simple conflicts, in each file listed above replace every occurrence of the following text blocks with the desired text.
```diff
<<<<<<< HEAD
File changes in local repository
=======
File changes in remote repository
>>>>>>> origin/main
```

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 file_mtime = models.FloatField(null=True, blank=True) # Track file modification time
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
tags = models.ManyToManyField('Tag', blank=True, related_name='questions')
def __str__(self): def __str__(self):
return self.text[:50] 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): class Option(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='options') question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='options')
letter = models.CharField(max_length=1) 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 - options: list of (letter, text) tuples
- correct_answer: the correct answer letter(s) - correct_answer: the correct answer letter(s)
- has_answer: whether it has an answer (not TODO) - has_answer: whether it has an answer (not TODO)
- tags: list of tag strings
""" """
lines = content.split('\n') lines = content.split('\n')
@@ -104,7 +105,15 @@ def parse_markdown_question(file_path: Path, content: str) -> Tuple[bool, dict]:
question_type = 'textalternativ' question_type = 'textalternativ'
elif 'frågetyp/textfält' in line: elif 'frågetyp/textfält' in line:
question_type = 'textfält' 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: if not is_question:
return False, {} return False, {}
@@ -138,7 +147,9 @@ def parse_markdown_question(file_path: Path, content: str) -> Tuple[bool, dict]:
'options': [], 'options': [],
'correct_answer': '', 'correct_answer': '',
'has_answer': False, '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) # 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, 'options': options_data,
'correct_answer': correct_answer, 'correct_answer': correct_answer,
'has_answer': has_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 stats.created += 1
else: else:
stats.updated += 1 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 # Update options
question.options.all().delete() question.options.all().delete()

View File

@@ -2,23 +2,46 @@ from django.http import HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.http import require_http_methods 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): def index(request):
handle_tag_filter(request)
total_questions = Question.objects.count() total_questions = Question.objects.count()
answered_count = QuizResult.objects.filter(user=request.quiz_user).count() answered_count = QuizResult.objects.filter(user=request.quiz_user).count()
context = { context = {
'total_questions': total_questions, 'total_questions': total_questions,
'answered_count': answered_count, 'answered_count': answered_count,
'tags': Tag.objects.all(),
'current_tag': request.session.get('quiz_tag'),
} }
return render(request, 'index.html', context) return render(request, 'index.html', context)
def get_next_question(request): 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) 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: if not next_question:
return render(request, 'partials/complete.html') return render(request, 'partials/complete.html')

View File

@@ -1,6 +1,42 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% 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> <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">
<div class="progress-bar" style="width: {% if total_questions > 0 %}{{ answered_count|floatformat:0 }}{% else %}0{% endif %}%"></div> <div class="progress-bar" style="width: {% if total_questions > 0 %}{{ answered_count|floatformat:0 }}{% else %}0{% endif %}%"></div>
</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" },
]