vault backup: 2025-12-22 13:19:11
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m27s
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m27s
This commit is contained in:
72
content/.obsidian/workspace.json
vendored
72
content/.obsidian/workspace.json
vendored
@@ -6,7 +6,6 @@
|
|||||||
{
|
{
|
||||||
"id": "8dd584e60438200b",
|
"id": "8dd584e60438200b",
|
||||||
"type": "tabs",
|
"type": "tabs",
|
||||||
"dimension": 62.97968397291196,
|
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "baa45c5e57825965",
|
"id": "baa45c5e57825965",
|
||||||
@@ -14,44 +13,13 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "Anatomi & Histologi 2/1 Öga anatomi/Provfrågor.md",
|
"file": "Anatomi & Histologi 2/Demokompendium.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": false,
|
"source": false,
|
||||||
"backlinks": false
|
"backlinks": false
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "Provfrågor"
|
"title": "Demokompendium"
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7e72057acf1e42f0",
|
|
||||||
"type": "tabs",
|
|
||||||
"dimension": 37.020316027088036,
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "9f9fa624da392231",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "pdf",
|
|
||||||
"state": {
|
|
||||||
"file": "Anatomi & Histologi 2/Gamla tentor/2024-05-29/!2024-05-29-0125-GZX.pdf"
|
|
||||||
},
|
|
||||||
"icon": "lucide-file-text",
|
|
||||||
"title": "!2024-05-29-0125-GZX"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "c1c7815735aa906e",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "pdf",
|
|
||||||
"state": {
|
|
||||||
"file": "Anatomi & Histologi 2/Gamla tentor/2023-05-31/!2023-05-31-0100-DKS.pdf"
|
|
||||||
},
|
|
||||||
"icon": "lucide-file-text",
|
|
||||||
"title": "!2023-05-31-0100-DKS"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -224,10 +192,26 @@
|
|||||||
},
|
},
|
||||||
"active": "baa45c5e57825965",
|
"active": "baa45c5e57825965",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2022-01-15/20.md",
|
"Anatomi & Histologi 2/Gamla tentor/2024-01-10/20.md",
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Provfrågor.md",
|
"Anatomi & Histologi 2/1 Öga anatomi/Provfrågor.md",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2022-06-01/21.md",
|
"Anatomi & Histologi 2/Gamla tentor/2024-01-10/19.md",
|
||||||
|
"Anatomi & Histologi 2/Gamla tentor/2024-01-10/!2024-01-10-0009-RYY.pdf",
|
||||||
|
"Anatomi & Histologi 2/Gamla tentor/2023-05-31/!2023-05-31-0100-DKS.pdf",
|
||||||
|
"Anatomi & Histologi 2/Gamla tentor/2024-05-29/!2024-05-29-0125-GZX.pdf",
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Organa sensum.pdf",
|
"Anatomi & Histologi 2/1 Öga anatomi/Organa sensum.pdf",
|
||||||
|
"Anatomi & Histologi 2/1 Öga anatomi/Oculus.md.md",
|
||||||
|
"Anatomi & Histologi 2/1 Öga anatomi/Målbeskrivning.md",
|
||||||
|
"Anatomi & Histologi 2/1 Öga anatomi/Instuderingsfrågor.md",
|
||||||
|
"Anatomi & Histologi 2/1 Öga anatomi/Slides.md",
|
||||||
|
"Anatomi & Histologi 2/1 Öga anatomi/Video 2.md",
|
||||||
|
"Anatomi & Histologi 2/1 Öga anatomi/Video 1.md",
|
||||||
|
"Anatomi & Histologi 2/2 Öra anatomi/Video.md",
|
||||||
|
"Anatomi & Histologi 2/2 Öra anatomi/Slides.pdf.pdf",
|
||||||
|
"Anatomi & Histologi 2/2 Öra anatomi/Instuderingsfrågor.md",
|
||||||
|
"Anatomi & Histologi 2/Schema.md",
|
||||||
|
"Anatomi & Histologi 2/Statistik.md",
|
||||||
|
"Anatomi & Histologi 2/Gamla tentor/2022-01-15/20.md",
|
||||||
|
"Anatomi & Histologi 2/Gamla tentor/2022-06-01/21.md",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2025-06-03/26.md",
|
"Anatomi & Histologi 2/Gamla tentor/2025-06-03/26.md",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2025-08-08/10.md",
|
"Anatomi & Histologi 2/Gamla tentor/2025-08-08/10.md",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2025-08-08/8.md",
|
"Anatomi & Histologi 2/Gamla tentor/2025-08-08/8.md",
|
||||||
@@ -239,28 +223,12 @@
|
|||||||
"Anatomi & Histologi 2/Gamla tentor/2025-02-08/21.md",
|
"Anatomi & Histologi 2/Gamla tentor/2025-02-08/21.md",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2025-01-15/18.md",
|
"Anatomi & Histologi 2/Gamla tentor/2025-01-15/18.md",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2025-01-15/1.md",
|
"Anatomi & Histologi 2/Gamla tentor/2025-01-15/1.md",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2024-05-29/12.md",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2024-05-29/11.md",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2023-05-31/26.md",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2023-05-31/20.md",
|
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Video.md",
|
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Slides.pdf.pdf",
|
"Anatomi & Histologi 2/1 Öga anatomi/Slides.pdf.pdf",
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Slides.md",
|
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Oculus.md.md",
|
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Målbeskrivning.md",
|
|
||||||
"Anatomi & Histologi 2/1 Öga anatomi/Instuderingsfrågor.md",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2024-05-29/22.md",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2024-05-29/!2024-05-29-0125-GZX.pdf",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2025-06-03/10.md",
|
|
||||||
"Biokemi/Metabolism/🍋 Citronsyracykeln/Provfrågor.md",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2023-05-31/!2023-05-31-0100-DKS.pdf",
|
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2023-01-11/!2023-01-11-0044-PRX.pdf",
|
"Anatomi & Histologi 2/Gamla tentor/2023-01-11/!2023-01-11-0044-PRX.pdf",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2022-06-01/!2022-06-01-0101-MGY.pdf",
|
"Anatomi & Histologi 2/Gamla tentor/2022-06-01/!2022-06-01-0101-MGY.pdf",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2022-01-15/!2022-01-15-0032-BWD.pdf",
|
"Anatomi & Histologi 2/Gamla tentor/2022-01-15/!2022-01-15-0032-BWD.pdf",
|
||||||
"attachments/image-121.png",
|
"attachments/image-121.png",
|
||||||
"Anatomi & Histologi 2/Gamla tentor/2024-01-10/!2024-01-10-0009-RYY.pdf",
|
|
||||||
"z-Tech/tag_exam_questions_v2.sh",
|
"z-Tech/tag_exam_questions_v2.sh",
|
||||||
"z-Tech/tag_exam_questions.sh",
|
|
||||||
"attachments/image-120.png",
|
"attachments/image-120.png",
|
||||||
"attachments/image-119.png",
|
"attachments/image-119.png",
|
||||||
"attachments/image-118.png",
|
"attachments/image-118.png",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
```dataviewjs
|
```dataviewjs
|
||||||
for (const path of dv.pagePaths("#provfråga and #öga and #anatomi")) {
|
for (const path of dv.pagePaths("#provfråga and #öga and #anatomi")) {
|
||||||
dv.span(" \n[[" + path + "]]\n")
|
dv.span(" \n[[" + path + "]]\n")
|
||||||
|
|||||||
4
content/Anatomi & Histologi 2/1 Öga anatomi/Video 2.md
Normal file
4
content/Anatomi & Histologi 2/1 Öga anatomi/Video 2.md
Normal file
File diff suppressed because one or more lines are too long
@@ -11,5 +11,5 @@ Vilken av följande strukturer är transparent? (1p)
|
|||||||
- D: Iris
|
- D: Iris
|
||||||
|
|
||||||
```spoiler-block:
|
```spoiler-block:
|
||||||
TODO
|
A
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
tags: [ah2, provfråga, frågetyp/mcq, anatomi, öga]
|
tags: [ah2, provfråga, frågetyp/mcq, anatomi, öga]
|
||||||
date: 2024-01-10
|
date: 2024-01-10
|
||||||
---
|
---
|
||||||
I vilken del av retinae återfinner du vare sig tappar eller stavar? (1p)
|
I vilken del av [retinae](https://en.wikipedia.org/wiki/Retina) återfinner du vare sig tappar eller stavar? (1p)
|
||||||
|
|
||||||
**Välj ett alternativ:**
|
**Välj ett alternativ:**
|
||||||
- A: Macula lutea
|
- A: Macula lutea
|
||||||
|
|||||||
BIN
quiz/db.sqlite3
BIN
quiz/db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
300
quiz/quiz/tests.py
Normal file
300
quiz/quiz/tests.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
from django.test import TestCase, Client
|
||||||
|
from django.urls import reverse
|
||||||
|
from quiz.models import Question, QuizUser, QuizSession, QuizResult, Course, Exam, Tag, Option
|
||||||
|
|
||||||
|
|
||||||
|
class QuizViewsTestCase(TestCase):
|
||||||
|
"""Comprehensive tests for all quiz endpoints"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test data"""
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
# Create test user (QuizUser uses session_key, not username)
|
||||||
|
self.user = QuizUser.objects.create(session_key="test_session_key_123")
|
||||||
|
|
||||||
|
# Create test course
|
||||||
|
self.course = Course.objects.create(name="Test Course")
|
||||||
|
|
||||||
|
# Create test exam
|
||||||
|
self.exam = Exam.objects.create(
|
||||||
|
name="Test Exam",
|
||||||
|
course=self.course,
|
||||||
|
date="2025-01-01" # Required field
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create test tags
|
||||||
|
self.tag1 = Tag.objects.create(name="Tag 1")
|
||||||
|
self.tag2 = Tag.objects.create(name="Tag 2")
|
||||||
|
|
||||||
|
# Create test questions
|
||||||
|
self.question1 = Question.objects.create(
|
||||||
|
text="Test question 1?",
|
||||||
|
correct_answer="A",
|
||||||
|
exam=self.exam
|
||||||
|
)
|
||||||
|
self.question1.tags.add(self.tag1)
|
||||||
|
|
||||||
|
Option.objects.create(question=self.question1, letter="A", text="Answer A")
|
||||||
|
Option.objects.create(question=self.question1, letter="B", text="Answer B")
|
||||||
|
|
||||||
|
self.question2 = Question.objects.create(
|
||||||
|
text="Test question 2 (multi)?",
|
||||||
|
correct_answer="A,B",
|
||||||
|
exam=self.exam
|
||||||
|
)
|
||||||
|
self.question2.tags.add(self.tag1, self.tag2)
|
||||||
|
|
||||||
|
Option.objects.create(question=self.question2, letter="A", text="Answer A")
|
||||||
|
Option.objects.create(question=self.question2, letter="B", text="Answer B")
|
||||||
|
Option.objects.create(question=self.question2, letter="C", text="Answer C")
|
||||||
|
|
||||||
|
# Set user in session
|
||||||
|
session = self.client.session
|
||||||
|
session['quiz_user_id'] = self.user.id
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
def test_index_view(self):
|
||||||
|
"""Test dashboard index view"""
|
||||||
|
response = self.client.get(reverse('index'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'Välkommen')
|
||||||
|
self.assertIn('active_sessions', response.context)
|
||||||
|
self.assertIn('form', response.context)
|
||||||
|
|
||||||
|
def test_create_quiz(self):
|
||||||
|
"""Test quiz creation"""
|
||||||
|
response = self.client.post(reverse('create_quiz'), {
|
||||||
|
'course': self.course.id,
|
||||||
|
'tags': [self.tag1.id],
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirect
|
||||||
|
|
||||||
|
# Verify session was created
|
||||||
|
session = QuizSession.objects.filter(user=self.user).first()
|
||||||
|
self.assertIsNotNone(session)
|
||||||
|
self.assertEqual(session.course, self.course)
|
||||||
|
self.assertTrue(session.is_active)
|
||||||
|
|
||||||
|
def test_quiz_mode_view(self):
|
||||||
|
"""Test dedicated quiz mode view"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('quiz_mode', args=[session.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'Test Course')
|
||||||
|
self.assertIn('session', response.context)
|
||||||
|
|
||||||
|
def test_quiz_question_view(self):
|
||||||
|
"""Test quiz question endpoint"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('quiz_question', args=[session.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('question', response.context)
|
||||||
|
self.assertIn('session', response.context)
|
||||||
|
|
||||||
|
def test_navigate_question(self):
|
||||||
|
"""Test question navigation"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test next navigation
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('navigate_question', args=[session.id, 'next']),
|
||||||
|
{'q': self.question1.id}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Test previous navigation
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('navigate_question', args=[session.id, 'previous']),
|
||||||
|
{'q': self.question2.id}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_submit_single_answer(self):
|
||||||
|
"""Test submitting a single-choice answer"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('submit_answer', args=[session.id]),
|
||||||
|
{
|
||||||
|
'question_id': self.question1.id,
|
||||||
|
'answer': 'A'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Verify result was created
|
||||||
|
result = QuizResult.objects.get(
|
||||||
|
user=self.user,
|
||||||
|
question=self.question1,
|
||||||
|
quiz_session=session
|
||||||
|
)
|
||||||
|
self.assertEqual(result.selected_answer, 'A')
|
||||||
|
self.assertTrue(result.is_correct)
|
||||||
|
|
||||||
|
def test_submit_multi_answer(self):
|
||||||
|
"""Test submitting a multi-choice answer"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test correct multi-answer (order shouldn't matter)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('submit_answer', args=[session.id]),
|
||||||
|
{
|
||||||
|
'question_id': self.question2.id,
|
||||||
|
'answer': 'B,A' # Reversed order
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
result = QuizResult.objects.get(
|
||||||
|
user=self.user,
|
||||||
|
question=self.question2,
|
||||||
|
quiz_session=session
|
||||||
|
)
|
||||||
|
self.assertTrue(result.is_correct) # Should be correct despite order
|
||||||
|
|
||||||
|
def test_submit_difficulty(self):
|
||||||
|
"""Test submitting FSRS difficulty rating"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
# First submit an answer
|
||||||
|
QuizResult.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
question=self.question1,
|
||||||
|
quiz_session=session,
|
||||||
|
selected_answer='A',
|
||||||
|
is_correct=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then submit difficulty
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('submit_difficulty', args=[session.id]),
|
||||||
|
{
|
||||||
|
'question_id': self.question1.id,
|
||||||
|
'difficulty': 'easy'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Verify difficulty was saved
|
||||||
|
result = QuizResult.objects.get(
|
||||||
|
user=self.user,
|
||||||
|
question=self.question1,
|
||||||
|
quiz_session=session
|
||||||
|
)
|
||||||
|
self.assertEqual(result.difficulty, 'easy')
|
||||||
|
|
||||||
|
def test_close_quiz(self):
|
||||||
|
"""Test closing a quiz session"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(reverse('close_quiz', args=[session.id]))
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirect to index
|
||||||
|
|
||||||
|
# Verify session was deactivated
|
||||||
|
session.refresh_from_db()
|
||||||
|
self.assertFalse(session.is_active)
|
||||||
|
|
||||||
|
def test_invalid_session_access(self):
|
||||||
|
"""Test accessing another user's session"""
|
||||||
|
other_user = QuizUser.objects.create(session_key="other_session_key")
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to access it
|
||||||
|
response = self.client.get(reverse('quiz_mode', args=[session.id]))
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_answer_without_question_id(self):
|
||||||
|
"""Test error handling for missing question_id"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('submit_answer', args=[session.id]),
|
||||||
|
{'answer': 'A'} # Missing question_id
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_difficulty_without_result(self):
|
||||||
|
"""Test error handling for difficulty without existing result"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('submit_difficulty', args=[session.id]),
|
||||||
|
{
|
||||||
|
'question_id': self.question1.id,
|
||||||
|
'difficulty': 'easy'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_answer_normalization(self):
|
||||||
|
"""Test that multi-choice answers are normalized correctly"""
|
||||||
|
session = QuizSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
course=self.course
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit answers in different orders
|
||||||
|
test_cases = [
|
||||||
|
('A,B', True), # Correct order
|
||||||
|
('B,A', True), # Reversed order
|
||||||
|
('A,C', False), # Wrong answer
|
||||||
|
('A', False), # Incomplete answer
|
||||||
|
]
|
||||||
|
|
||||||
|
for answer, expected_correct in test_cases:
|
||||||
|
result = QuizResult.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
question=self.question2,
|
||||||
|
quiz_session=session
|
||||||
|
).delete() # Clean up
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('submit_answer', args=[session.id]),
|
||||||
|
{
|
||||||
|
'question_id': self.question2.id,
|
||||||
|
'answer': answer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
result = QuizResult.objects.get(
|
||||||
|
user=self.user,
|
||||||
|
question=self.question2,
|
||||||
|
quiz_session=session
|
||||||
|
)
|
||||||
|
self.assertEqual(result.is_correct, expected_correct,
|
||||||
|
f"Answer '{answer}' should be {'correct' if expected_correct else 'incorrect'}")
|
||||||
@@ -215,7 +215,13 @@ def submit_answer(request, session_id):
|
|||||||
except Question.DoesNotExist:
|
except Question.DoesNotExist:
|
||||||
return HttpResponse("Question not found", status=404)
|
return HttpResponse("Question not found", status=404)
|
||||||
|
|
||||||
is_correct = selected_answer == question.correct_answer
|
# Normalize answers for comparison (sort comma-separated values)
|
||||||
|
def normalize_answer(ans):
|
||||||
|
if ',' in ans:
|
||||||
|
return ','.join(sorted(ans.split(',')))
|
||||||
|
return ans
|
||||||
|
|
||||||
|
is_correct = normalize_answer(selected_answer) == normalize_answer(question.correct_answer)
|
||||||
|
|
||||||
QuizResult.objects.update_or_create(
|
QuizResult.objects.update_or_create(
|
||||||
user=request.quiz_user,
|
user=request.quiz_user,
|
||||||
|
|||||||
@@ -1,49 +1,19 @@
|
|||||||
{% if show_answer %}
|
{% csrf_token %}
|
||||||
<div class="answer-feedback {% if is_correct %}correct{% else %}incorrect{% endif %}">
|
<input type="hidden" id="current-question-id" value="{{ question.id }}">
|
||||||
{% if is_correct %}
|
|
||||||
✓ Rätt svar!
|
|
||||||
{% else %}
|
|
||||||
✗ Fel svar. Rätt svar är: {{ question.correct_answer }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="question-text">{{ question.text }}</div>
|
<div class="question-text">{{ question.text }}</div>
|
||||||
|
|
||||||
<div class="options-container">
|
<div class="options-container">
|
||||||
{% for option in question.options.all %}
|
{% for option in question.options.all %}
|
||||||
<div class="option-item" id="option-{{ option.letter }}"
|
<div class="option-item" id="option-{{ option.letter }}" onclick="toggleOption('{{ option.letter }}')">
|
||||||
onclick="selectOption('{{ option.letter }}', {{ question.id }}, {{ session.id }})">
|
<input type="checkbox" id="checkbox-{{ option.letter }}\" value="{{ option.letter }}"
|
||||||
|
style="margin-right: 0.5rem; width: 1.2rem; height: 1.2rem; cursor: pointer;">
|
||||||
<span class="option-letter">{{ option.letter }}</span>
|
<span class="option-letter">{{ option.letter }}</span>
|
||||||
<span>{{ option.text }}</span>
|
<span>{{ option.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if show_answer %}
|
|
||||||
<div class="difficulty-section">
|
|
||||||
<div class="difficulty-label">Hur svårt var detta?</div>
|
|
||||||
<div class="difficulty-buttons">
|
|
||||||
<button class="difficulty-btn again" onclick="submitDifficulty('again', {{ question.id }}, {{ session.id }})">
|
|
||||||
<div>Igen</div>
|
|
||||||
<small style="font-size: 0.75rem; font-weight: 400;"><1m</small>
|
|
||||||
</button>
|
|
||||||
<button class="difficulty-btn hard" onclick="submitDifficulty('hard', {{ question.id }}, {{ session.id }})">
|
|
||||||
<div>Svårt</div>
|
|
||||||
<small style="font-size: 0.75rem; font-weight: 400;"><6m</small>
|
|
||||||
</button>
|
|
||||||
<button class="difficulty-btn good" onclick="submitDifficulty('good', {{ question.id }}, {{ session.id }})">
|
|
||||||
<div>Bra</div>
|
|
||||||
<small style="font-size: 0.75rem; font-weight: 400;"><10m</small>
|
|
||||||
</button>
|
|
||||||
<button class="difficulty-btn easy" onclick="submitDifficulty('easy', {{ question.id }}, {{ session.id }})">
|
|
||||||
<div>Lätt</div>
|
|
||||||
<small style="font-size: 0.75rem; font-weight: 400;">4d</small>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<button class="nav-btn" {% if not has_previous %}disabled{% endif %}
|
<button class="nav-btn" {% if not has_previous %}disabled{% endif %}
|
||||||
onclick="navigateQuestion('previous', {{ session.id }})">
|
onclick="navigateQuestion('previous', {{ session.id }})">
|
||||||
@@ -54,54 +24,3 @@
|
|||||||
Nästa →
|
Nästa →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
let selectedAnswer = null;
|
|
||||||
|
|
||||||
function selectOption(letter, questionId, sessionId) {
|
|
||||||
{% if show_answer %}
|
|
||||||
return; // Don't allow changing answer after submission
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
selectedAnswer = letter;
|
|
||||||
|
|
||||||
// Visual feedback
|
|
||||||
document.querySelectorAll('.option-item').forEach(opt => {
|
|
||||||
opt.style.borderColor = 'var(--border)';
|
|
||||||
opt.style.background = 'white';
|
|
||||||
});
|
|
||||||
|
|
||||||
const selected = document.getElementById('option-' + letter);
|
|
||||||
selected.style.borderColor = 'var(--primary)';
|
|
||||||
selected.style.background = '#f0f4ff';
|
|
||||||
|
|
||||||
// Submit answer
|
|
||||||
htmx.ajax('POST', `/submit/${sessionId}/`, {
|
|
||||||
target: '#quiz-content',
|
|
||||||
values: {
|
|
||||||
question_id: questionId,
|
|
||||||
answer: letter
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitDifficulty(difficulty, questionId, sessionId) {
|
|
||||||
htmx.ajax('POST', `/difficulty/${sessionId}/`, {
|
|
||||||
values: {
|
|
||||||
question_id: questionId,
|
|
||||||
difficulty: difficulty
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move to next question after a brief delay
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateQuestion('next', sessionId);
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateQuestion(direction, sessionId) {
|
|
||||||
htmx.ajax('GET', `/quiz/${sessionId}/${direction}/`, {
|
|
||||||
target: '#quiz-content'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -52,6 +52,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
background: white;
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-item:hover {
|
.option-item:hover {
|
||||||
@@ -70,78 +72,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-right: 1rem;
|
margin-left: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-section {
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding-top: 2rem;
|
|
||||||
border-top: 2px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-label {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn {
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
background: white;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.again {
|
|
||||||
border-color: #ef4444;
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.again:hover {
|
|
||||||
background: #ef4444;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.hard {
|
|
||||||
border-color: #f59e0b;
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.hard:hover {
|
|
||||||
background: #f59e0b;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.good {
|
|
||||||
border-color: #10b981;
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.good:hover {
|
|
||||||
background: #10b981;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.easy {
|
|
||||||
border-color: #6366f1;
|
|
||||||
color: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-btn.easy:hover {
|
|
||||||
background: #6366f1;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-buttons {
|
.nav-buttons {
|
||||||
@@ -169,25 +100,6 @@
|
|||||||
background: #cbd5e1;
|
background: #cbd5e1;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.answer-feedback {
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.answer-feedback.correct {
|
|
||||||
background: #d1fae5;
|
|
||||||
color: #065f46;
|
|
||||||
border: 2px solid #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.answer-feedback.incorrect {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
border: 2px solid #ef4444;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="quiz-mode-container">
|
<div class="quiz-mode-container">
|
||||||
@@ -202,9 +114,82 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Load first question on page load
|
const SESSION_ID = parseInt("{{ session.id }}");
|
||||||
|
|
||||||
|
function saveAnswer(questionId, selectedLetters) {
|
||||||
|
const key = 'quiz_' + SESSION_ID + '_answers';
|
||||||
|
const answers = JSON.parse(localStorage.getItem(key) || '{}');
|
||||||
|
answers[questionId] = selectedLetters;
|
||||||
|
localStorage.setItem(key, JSON.stringify(answers));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAnswer(questionId) {
|
||||||
|
const key = 'quiz_' + SESSION_ID + '_answers';
|
||||||
|
const answers = JSON.parse(localStorage.getItem(key) || '{}');
|
||||||
|
return answers[questionId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOption(letter) {
|
||||||
|
const checkbox = document.getElementById('checkbox-' + letter);
|
||||||
|
const optionDiv = document.getElementById('option-' + letter);
|
||||||
|
|
||||||
|
if (!checkbox || !optionDiv) return;
|
||||||
|
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
optionDiv.style.borderColor = 'var(--primary)';
|
||||||
|
optionDiv.style.background = '#f0f4ff';
|
||||||
|
} else {
|
||||||
|
optionDiv.style.borderColor = 'var(--border)';
|
||||||
|
optionDiv.style.background = 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionId = document.getElementById('current-question-id').value;
|
||||||
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
|
||||||
|
const selectedLetters = Array.from(checkboxes).map(cb => cb.value).sort();
|
||||||
|
saveAnswer(questionId, selectedLetters);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreAnswers() {
|
||||||
|
const questionId = document.getElementById('current-question-id')?.value;
|
||||||
|
if (!questionId) return;
|
||||||
|
|
||||||
|
const saved = loadAnswer(questionId);
|
||||||
|
saved.forEach(letter => {
|
||||||
|
const checkbox = document.getElementById('checkbox-' + letter);
|
||||||
|
const optionDiv = document.getElementById('option-' + letter);
|
||||||
|
if (checkbox && optionDiv) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
optionDiv.style.borderColor = 'var(--primary)';
|
||||||
|
optionDiv.style.background = '#f0f4ff';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateQuestion(direction, sessionId) {
|
||||||
|
const currentQInput = document.getElementById('current-question-id');
|
||||||
|
const currentQ = currentQInput ? currentQInput.value : null;
|
||||||
|
const url = currentQ
|
||||||
|
? '/quiz/' + sessionId + '/' + direction + '/?q=' + currentQ
|
||||||
|
: '/quiz/' + sessionId + '/' + direction + '/';
|
||||||
|
|
||||||
|
htmx.ajax('GET', url, {
|
||||||
|
target: '#quiz-content'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
restoreAnswers();
|
||||||
|
});
|
||||||
|
observer.observe(document.getElementById('quiz-content'), {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
htmx.ajax('GET', '{% url 'quiz_question' session.id %}', { target: '#quiz-content' });
|
htmx.ajax('GET', '{% url 'quiz_question' session.id %}', { target: '#quiz-content' });
|
||||||
|
setTimeout(restoreAnswers, 100);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user