diff --git a/content/conflict-files-obsidian-git.md b/content/conflict-files-obsidian-git.md deleted file mode 100644 index b447a9f..0000000 --- a/content/conflict-files-obsidian-git.md +++ /dev/null @@ -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 -``` \ No newline at end of file diff --git a/quiz/__pycache__/settings.cpython-313.pyc b/quiz/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..3e444d8 Binary files /dev/null and b/quiz/__pycache__/settings.cpython-313.pyc differ diff --git a/quiz/db.sqlite3 b/quiz/db.sqlite3 index b62fac9..b13fe1b 100644 Binary files a/quiz/db.sqlite3 and b/quiz/db.sqlite3 differ diff --git a/quiz/db.sqlite3-shm b/quiz/db.sqlite3-shm deleted file mode 100644 index 9a677cb..0000000 Binary files a/quiz/db.sqlite3-shm and /dev/null differ diff --git a/quiz/db.sqlite3-wal b/quiz/db.sqlite3-wal deleted file mode 100644 index 066729a..0000000 Binary files a/quiz/db.sqlite3-wal and /dev/null differ diff --git a/quiz/quiz/__pycache__/__init__.cpython-313.pyc b/quiz/quiz/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d5e8c77 Binary files /dev/null and b/quiz/quiz/__pycache__/__init__.cpython-313.pyc differ diff --git a/quiz/quiz/__pycache__/admin.cpython-313.pyc b/quiz/quiz/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..036dbac Binary files /dev/null and b/quiz/quiz/__pycache__/admin.cpython-313.pyc differ diff --git a/quiz/quiz/__pycache__/apps.cpython-313.pyc b/quiz/quiz/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..05edb21 Binary files /dev/null and b/quiz/quiz/__pycache__/apps.cpython-313.pyc differ diff --git a/quiz/quiz/__pycache__/middleware.cpython-313.pyc b/quiz/quiz/__pycache__/middleware.cpython-313.pyc new file mode 100644 index 0000000..67bf426 Binary files /dev/null and b/quiz/quiz/__pycache__/middleware.cpython-313.pyc differ diff --git a/quiz/quiz/__pycache__/models.cpython-313.pyc b/quiz/quiz/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..67d6d45 Binary files /dev/null and b/quiz/quiz/__pycache__/models.cpython-313.pyc differ diff --git a/quiz/quiz/__pycache__/urls.cpython-313.pyc b/quiz/quiz/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..40483d3 Binary files /dev/null and b/quiz/quiz/__pycache__/urls.cpython-313.pyc differ diff --git a/quiz/quiz/__pycache__/views.cpython-313.pyc b/quiz/quiz/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..32cf67c Binary files /dev/null and b/quiz/quiz/__pycache__/views.cpython-313.pyc differ diff --git a/quiz/quiz/management/__pycache__/__init__.cpython-313.pyc b/quiz/quiz/management/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..18a0914 Binary files /dev/null and b/quiz/quiz/management/__pycache__/__init__.cpython-313.pyc differ diff --git a/quiz/quiz/migrations/0006_tag_question_tags.py b/quiz/quiz/migrations/0006_tag_question_tags.py new file mode 100644 index 0000000..93057fc --- /dev/null +++ b/quiz/quiz/migrations/0006_tag_question_tags.py @@ -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'), + ), + ] diff --git a/quiz/quiz/migrations/__pycache__/0001_initial.cpython-313.pyc b/quiz/quiz/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..bde8489 Binary files /dev/null and b/quiz/quiz/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/quiz/quiz/migrations/__pycache__/0002_alter_question_correct_answer.cpython-313.pyc b/quiz/quiz/migrations/__pycache__/0002_alter_question_correct_answer.cpython-313.pyc new file mode 100644 index 0000000..cfa142a Binary files /dev/null and b/quiz/quiz/migrations/__pycache__/0002_alter_question_correct_answer.cpython-313.pyc differ diff --git a/quiz/quiz/migrations/__pycache__/0003_question_file_mtime.cpython-313.pyc b/quiz/quiz/migrations/__pycache__/0003_question_file_mtime.cpython-313.pyc new file mode 100644 index 0000000..d6bf846 Binary files /dev/null and b/quiz/quiz/migrations/__pycache__/0003_question_file_mtime.cpython-313.pyc differ diff --git a/quiz/quiz/migrations/__pycache__/0004_rename_user_quizuser_alter_quizuser_options.cpython-313.pyc b/quiz/quiz/migrations/__pycache__/0004_rename_user_quizuser_alter_quizuser_options.cpython-313.pyc new file mode 100644 index 0000000..355b56d Binary files /dev/null and b/quiz/quiz/migrations/__pycache__/0004_rename_user_quizuser_alter_quizuser_options.cpython-313.pyc differ diff --git a/quiz/quiz/migrations/__pycache__/0005_course_exam_question_exam.cpython-313.pyc b/quiz/quiz/migrations/__pycache__/0005_course_exam_question_exam.cpython-313.pyc new file mode 100644 index 0000000..dfb19d7 Binary files /dev/null and b/quiz/quiz/migrations/__pycache__/0005_course_exam_question_exam.cpython-313.pyc differ diff --git a/quiz/quiz/migrations/__pycache__/0006_tag_question_tags.cpython-313.pyc b/quiz/quiz/migrations/__pycache__/0006_tag_question_tags.cpython-313.pyc new file mode 100644 index 0000000..a4ab766 Binary files /dev/null and b/quiz/quiz/migrations/__pycache__/0006_tag_question_tags.cpython-313.pyc differ diff --git a/quiz/quiz/migrations/__pycache__/__init__.cpython-313.pyc b/quiz/quiz/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..be60711 Binary files /dev/null and b/quiz/quiz/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/quiz/quiz/models.py b/quiz/quiz/models.py index 6030d88..e2dcc88 100644 --- a/quiz/quiz/models.py +++ b/quiz/quiz/models.py @@ -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) diff --git a/quiz/quiz/utils/importer.py b/quiz/quiz/utils/importer.py index 8e22103..97a110a 100644 --- a/quiz/quiz/utils/importer.py +++ b/quiz/quiz/utils/importer.py @@ -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() diff --git a/quiz/quiz/views.py b/quiz/quiz/views.py index 1d66f56..de0efa0 100644 --- a/quiz/quiz/views.py +++ b/quiz/quiz/views.py @@ -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') diff --git a/quiz/templates/index.html b/quiz/templates/index.html index 5f820d5..370ac9d 100644 --- a/quiz/templates/index.html +++ b/quiz/templates/index.html @@ -1,6 +1,42 @@ {% extends "base.html" %} + {% block content %} + +

Quiz Application

+ +
+ All + {% for tag in tags %} + + {{ tag.name }} + + {% endfor %} +
+
diff --git a/quiz/uv.lock b/quiz/uv.lock new file mode 100644 index 0000000..3d6704a --- /dev/null +++ b/quiz/uv.lock @@ -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" }, +]