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",
|
||||
"type": "tabs",
|
||||
"dimension": 62.97968397291196,
|
||||
"children": [
|
||||
{
|
||||
"id": "baa45c5e57825965",
|
||||
@@ -14,44 +13,13 @@
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Anatomi & Histologi 2/1 Öga anatomi/Provfrågor.md",
|
||||
"file": "Anatomi & Histologi 2/Demokompendium.md",
|
||||
"mode": "source",
|
||||
"source": false,
|
||||
"backlinks": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Provfrågor"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"title": "Demokompendium"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -224,10 +192,26 @@
|
||||
},
|
||||
"active": "baa45c5e57825965",
|
||||
"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/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/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-08-08/10.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-01-15/18.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.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/2022-06-01/!2022-06-01-0101-MGY.pdf",
|
||||
"Anatomi & Histologi 2/Gamla tentor/2022-01-15/!2022-01-15-0032-BWD.pdf",
|
||||
"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.sh",
|
||||
"attachments/image-120.png",
|
||||
"attachments/image-119.png",
|
||||
"attachments/image-118.png",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
```dataviewjs
|
||||
for (const path of dv.pagePaths("#provfråga and #öga and #anatomi")) {
|
||||
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
|
||||
|
||||
```spoiler-block:
|
||||
TODO
|
||||
A
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
tags: [ah2, provfråga, frågetyp/mcq, anatomi, öga]
|
||||
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:**
|
||||
- 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:
|
||||
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(
|
||||
user=request.quiz_user,
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
{% if show_answer %}
|
||||
<div class="answer-feedback {% if is_correct %}correct{% else %}incorrect{% endif %}">
|
||||
{% if is_correct %}
|
||||
✓ Rätt svar!
|
||||
{% else %}
|
||||
✗ Fel svar. Rätt svar är: {{ question.correct_answer }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="current-question-id" value="{{ question.id }}">
|
||||
|
||||
<div class="question-text">{{ question.text }}</div>
|
||||
|
||||
<div class="options-container">
|
||||
{% for option in question.options.all %}
|
||||
<div class="option-item" id="option-{{ option.letter }}"
|
||||
onclick="selectOption('{{ option.letter }}', {{ question.id }}, {{ session.id }})">
|
||||
<div class="option-item" id="option-{{ option.letter }}" onclick="toggleOption('{{ option.letter }}')">
|
||||
<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>{{ option.text }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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">
|
||||
<button class="nav-btn" {% if not has_previous %}disabled{% endif %}
|
||||
onclick="navigateQuestion('previous', {{ session.id }})">
|
||||
@@ -53,55 +23,4 @@
|
||||
onclick="navigateQuestion('next', {{ session.id }})">
|
||||
Nästa →
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
@@ -52,6 +52,8 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
@@ -70,78 +72,7 @@
|
||||
text-align: center;
|
||||
line-height: 2rem;
|
||||
font-weight: 700;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
@@ -169,25 +100,6 @@
|
||||
background: #cbd5e1;
|
||||
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>
|
||||
|
||||
<div class="quiz-mode-container">
|
||||
@@ -202,9 +114,82 @@
|
||||
</div>
|
||||
|
||||
<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 () {
|
||||
htmx.ajax('GET', '{% url 'quiz_question' session.id %}', { target: '#quiz-content' });
|
||||
setTimeout(restoreAnswers, 100);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user