vault backup: 2025-12-22 12:22:20
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m12s
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m12s
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,26 +1,35 @@
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
from .models import Course, Tag
|
||||
from .models import Course, Tag, Exam
|
||||
|
||||
class TagModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
return f"{obj.name} ({obj.question_count})"
|
||||
|
||||
class ExamModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
return f"{obj.course.name} - {obj.date}"
|
||||
|
||||
class CreateQuizForm(forms.Form):
|
||||
course = forms.ModelChoiceField(
|
||||
queryset=Course.objects.all(),
|
||||
required=False,
|
||||
empty_label="All Courses",
|
||||
empty_label="Alla kurser",
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
)
|
||||
exams = ExamModelMultipleChoiceField(
|
||||
queryset=Exam.objects.all(),
|
||||
required=False,
|
||||
widget=forms.SelectMultiple(attrs={'class': 'form-control'})
|
||||
)
|
||||
tags = TagModelMultipleChoiceField(
|
||||
queryset=Tag.objects.annotate(question_count=Count('questions')).order_by('name'),
|
||||
required=False,
|
||||
widget=forms.SelectMultiple(attrs={'class': 'form-control'})
|
||||
)
|
||||
QUESTION_TYPES = [
|
||||
('single', 'Single Choice'),
|
||||
('multi', 'Multiple Choice'),
|
||||
('single', 'Envalsfrågor'),
|
||||
('multi', 'Flervalsfrågor'),
|
||||
]
|
||||
question_type = forms.MultipleChoiceField(
|
||||
choices=QUESTION_TYPES,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0 on 2025-12-22 11:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('quiz', '0006_tag_question_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QuizSession',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='quiz.course')),
|
||||
('exams', models.ManyToManyField(blank=True, to='quiz.exam')),
|
||||
('tags', models.ManyToManyField(blank=True, to='quiz.tag')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz_sessions', to='quiz.quizuser')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quizresult',
|
||||
name='quiz_session',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='results', to='quiz.quizsession'),
|
||||
),
|
||||
]
|
||||
18
quiz/quiz/migrations/0008_quizsession_question_types.py
Normal file
18
quiz/quiz/migrations/0008_quizsession_question_types.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2025-12-22 11:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('quiz', '0007_quizsession_quizresult_quiz_session'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizsession',
|
||||
name='question_types',
|
||||
field=models.JSONField(blank=True, default=list),
|
||||
),
|
||||
]
|
||||
18
quiz/quiz/migrations/0009_quizresult_difficulty.py
Normal file
18
quiz/quiz/migrations/0009_quizresult_difficulty.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2025-12-22 11:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('quiz', '0008_quizsession_question_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizresult',
|
||||
name='difficulty',
|
||||
field=models.CharField(blank=True, choices=[('again', 'Again'), ('hard', 'Hard'), ('good', 'Good'), ('easy', 'Easy')], max_length=10, null=True),
|
||||
),
|
||||
]
|
||||
@@ -72,11 +72,34 @@ class Option(models.Model):
|
||||
return f"{self.letter}. {self.text[:30]}"
|
||||
|
||||
|
||||
class QuizSession(models.Model):
|
||||
user = models.ForeignKey(QuizUser, on_delete=models.CASCADE, related_name='quiz_sessions')
|
||||
course = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
exams = models.ManyToManyField(Exam, blank=True)
|
||||
tags = models.ManyToManyField(Tag, blank=True)
|
||||
question_types = models.JSONField(default=list, blank=True) # Store as list of strings
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"Session {self.id} for {self.user}"
|
||||
|
||||
|
||||
class QuizResult(models.Model):
|
||||
user = models.ForeignKey(QuizUser, on_delete=models.CASCADE, related_name='results')
|
||||
quiz_session = models.ForeignKey(QuizSession, on_delete=models.CASCADE, related_name='results', null=True, blank=True)
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE)
|
||||
selected_answer = models.CharField(max_length=1)
|
||||
is_correct = models.BooleanField()
|
||||
difficulty = models.CharField(max_length=10, blank=True, null=True, choices=[
|
||||
('again', 'Again'),
|
||||
('hard', 'Hard'),
|
||||
('good', 'Good'),
|
||||
('easy', 'Easy'),
|
||||
])
|
||||
answered_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from .views import index, get_next_question, submit_answer, stats, create_quiz
|
||||
from .views import (
|
||||
index, get_next_question, submit_answer, stats, create_quiz, close_quiz,
|
||||
quiz_mode, quiz_question, navigate_question, submit_difficulty
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', index, name='index'),
|
||||
path('next/', get_next_question, name='next_question'),
|
||||
path('submit/', submit_answer, name='submit_answer'),
|
||||
path('quiz/<int:session_id>/', quiz_mode, name='quiz_mode'),
|
||||
path('quiz/<int:session_id>/question/', quiz_question, name='quiz_question'),
|
||||
path('quiz/<int:session_id>/<str:direction>/', navigate_question, name='navigate_question'),
|
||||
path('next/<int:session_id>/', get_next_question, name='next_question'),
|
||||
path('submit/<int:session_id>/', submit_answer, name='submit_answer'),
|
||||
path('difficulty/<int:session_id>/', submit_difficulty, name='submit_difficulty'),
|
||||
path('close/<int:session_id>/', close_quiz, name='close_quiz'),
|
||||
path('stats/', stats, name='stats'),
|
||||
path('create/', create_quiz, name='create_quiz'),
|
||||
]
|
||||
|
||||
@@ -4,10 +4,18 @@ from django.urls import reverse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import Question, QuizResult, Tag
|
||||
from .models import Question, QuizResult, Tag, Course, Exam, QuizSession
|
||||
from .forms import CreateQuizForm
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def close_quiz(request, session_id):
|
||||
session = get_object_or_404(QuizSession, id=session_id, user=request.quiz_user)
|
||||
session.is_active = False
|
||||
session.save()
|
||||
return redirect('index')
|
||||
|
||||
|
||||
def handle_tag_filter(request):
|
||||
tag_slug = request.GET.get('tag')
|
||||
if tag_slug is not None:
|
||||
@@ -21,95 +29,181 @@ def create_quiz(request):
|
||||
if request.method == 'POST':
|
||||
form = CreateQuizForm(request.POST)
|
||||
if form.is_valid():
|
||||
# clear existing session data
|
||||
keys_to_clear = ['quiz_filter_course_id', 'quiz_filter_tag_ids', 'quiz_filter_types', 'quiz_tag']
|
||||
for key in keys_to_clear:
|
||||
if key in request.session:
|
||||
del request.session[key]
|
||||
|
||||
course = form.cleaned_data.get('course')
|
||||
exams = form.cleaned_data.get('exams')
|
||||
tags = form.cleaned_data.get('tags')
|
||||
question_types = form.cleaned_data.get('question_type')
|
||||
|
||||
if course:
|
||||
request.session['quiz_filter_course_id'] = course.id
|
||||
q_types = form.cleaned_data.get('question_type')
|
||||
|
||||
session = QuizSession.objects.create(
|
||||
user=request.quiz_user,
|
||||
course=course,
|
||||
question_types=q_types if q_types else []
|
||||
)
|
||||
if tags:
|
||||
request.session['quiz_filter_tag_ids'] = list(tags.values_list('id', flat=True))
|
||||
session.tags.set(tags)
|
||||
if exams:
|
||||
session.exams.set(exams)
|
||||
|
||||
if question_types:
|
||||
request.session['quiz_filter_types'] = question_types
|
||||
|
||||
return redirect('next_question')
|
||||
return redirect('index')
|
||||
else:
|
||||
form = CreateQuizForm()
|
||||
|
||||
return render(request, 'quiz_create.html', {'form': form})
|
||||
|
||||
def index(request):
|
||||
handle_tag_filter(request)
|
||||
active_sessions = QuizSession.objects.filter(user=request.quiz_user, is_active=True)
|
||||
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'),
|
||||
'active_sessions': active_sessions,
|
||||
'form': CreateQuizForm(), # Include form on landing page
|
||||
}
|
||||
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')
|
||||
|
||||
# New filters
|
||||
filter_course_id = request.session.get('quiz_filter_course_id')
|
||||
filter_tag_ids = request.session.get('quiz_filter_tag_ids')
|
||||
filter_types = request.session.get('quiz_filter_types')
|
||||
def quiz_mode(request, session_id):
|
||||
"""Dedicated quiz mode view"""
|
||||
session = get_object_or_404(QuizSession, id=session_id, user=request.quiz_user, is_active=True)
|
||||
return render(request, 'quiz_mode.html', {'session': session})
|
||||
|
||||
answered_ids = QuizResult.objects.filter(user=request.quiz_user).values_list('question_id', flat=True)
|
||||
|
||||
questions = Question.objects.exclude(id__in=answered_ids)
|
||||
|
||||
# Apply filters
|
||||
if current_tag:
|
||||
questions = questions.filter(tags__slug=current_tag)
|
||||
|
||||
if filter_course_id:
|
||||
questions = questions.filter(exam__course_id=filter_course_id)
|
||||
|
||||
if filter_tag_ids:
|
||||
questions = questions.filter(tags__id__in=filter_tag_ids)
|
||||
|
||||
if filter_types:
|
||||
# "single" -> no comma
|
||||
# "multi" -> comma
|
||||
def get_session_questions(session):
|
||||
"""Helper to get filtered questions for a session"""
|
||||
questions = Question.objects.all()
|
||||
|
||||
if session.course:
|
||||
questions = questions.filter(exam__course=session.course)
|
||||
|
||||
if session.tags.exists():
|
||||
questions = questions.filter(tags__in=session.tags.all())
|
||||
|
||||
if session.exams.exists():
|
||||
questions = questions.filter(exam__in=session.exams.all())
|
||||
|
||||
if session.question_types:
|
||||
q_objs = Q()
|
||||
if 'single' in filter_types:
|
||||
if 'single' in session.question_types:
|
||||
q_objs |= ~Q(correct_answer__contains=',')
|
||||
if 'multi' in filter_types:
|
||||
if 'multi' in session.question_types:
|
||||
q_objs |= Q(correct_answer__contains=',')
|
||||
|
||||
if q_objs:
|
||||
questions = questions.filter(q_objs)
|
||||
|
||||
# Distinguish questions based on filters to ensure we don't get duplicates if filtering by many-to-many
|
||||
questions = questions.distinct()
|
||||
|
||||
next_question = questions.first()
|
||||
return questions.distinct()
|
||||
|
||||
if not next_question:
|
||||
return render(request, 'partials/complete.html')
|
||||
|
||||
return render(request, 'partials/question.html', {'question': next_question})
|
||||
def quiz_question(request, session_id):
|
||||
"""Get current question in quiz mode"""
|
||||
session = get_object_or_404(QuizSession, id=session_id, user=request.quiz_user)
|
||||
|
||||
# Get all questions for this session
|
||||
all_questions = get_session_questions(session)
|
||||
|
||||
# Get answered questions
|
||||
answered_ids = QuizResult.objects.filter(
|
||||
user=request.quiz_user,
|
||||
quiz_session=session
|
||||
).values_list('question_id', flat=True)
|
||||
|
||||
# Get unanswered questions
|
||||
unanswered = all_questions.exclude(id__in=answered_ids)
|
||||
|
||||
# Default to first unanswered question, or first question if all answered
|
||||
if unanswered.exists():
|
||||
question = unanswered.first()
|
||||
show_answer = False
|
||||
else:
|
||||
# All answered, show first question
|
||||
question = all_questions.first()
|
||||
if question:
|
||||
result = QuizResult.objects.filter(
|
||||
user=request.quiz_user,
|
||||
quiz_session=session,
|
||||
question=question
|
||||
).first()
|
||||
show_answer = result is not None
|
||||
else:
|
||||
return render(request, 'partials/complete.html', {'session': session})
|
||||
|
||||
# Calculate navigation
|
||||
all_q_ids = list(all_questions.values_list('id', flat=True))
|
||||
current_index = all_q_ids.index(question.id) if question.id in all_q_ids else 0
|
||||
|
||||
context = {
|
||||
'question': question,
|
||||
'session': session,
|
||||
'show_answer': show_answer,
|
||||
'has_previous': current_index > 0,
|
||||
'has_next': current_index < len(all_q_ids) - 1,
|
||||
}
|
||||
|
||||
if show_answer:
|
||||
result = QuizResult.objects.get(
|
||||
user=request.quiz_user,
|
||||
quiz_session=session,
|
||||
question=question
|
||||
)
|
||||
context['is_correct'] = result.is_correct
|
||||
|
||||
return render(request, 'partials/quiz_question.html', context)
|
||||
|
||||
|
||||
def navigate_question(request, session_id, direction):
|
||||
"""Navigate to previous/next question"""
|
||||
session = get_object_or_404(QuizSession, id=session_id, user=request.quiz_user)
|
||||
|
||||
# Get current question from session or query params
|
||||
current_q_id = request.GET.get('q')
|
||||
|
||||
all_questions = get_session_questions(session)
|
||||
all_q_ids = list(all_questions.values_list('id', flat=True))
|
||||
|
||||
if current_q_id:
|
||||
try:
|
||||
current_index = all_q_ids.index(int(current_q_id))
|
||||
except (ValueError, IndexError):
|
||||
current_index = 0
|
||||
else:
|
||||
current_index = 0
|
||||
|
||||
# Navigate
|
||||
if direction == 'previous' and current_index > 0:
|
||||
new_index = current_index - 1
|
||||
elif direction == 'next' and current_index < len(all_q_ids) - 1:
|
||||
new_index = current_index + 1
|
||||
else:
|
||||
new_index = current_index
|
||||
|
||||
question = all_questions.filter(id=all_q_ids[new_index]).first()
|
||||
|
||||
# Check if answered
|
||||
result = QuizResult.objects.filter(
|
||||
user=request.quiz_user,
|
||||
quiz_session=session,
|
||||
question=question
|
||||
).first()
|
||||
|
||||
context = {
|
||||
'question': question,
|
||||
'session': session,
|
||||
'show_answer': result is not None,
|
||||
'has_previous': new_index > 0,
|
||||
'has_next': new_index < len(all_q_ids) - 1,
|
||||
}
|
||||
|
||||
if result:
|
||||
context['is_correct'] = result.is_correct
|
||||
|
||||
return render(request, 'partials/quiz_question.html', context)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def submit_answer(request):
|
||||
def submit_answer(request, session_id):
|
||||
session = get_object_or_404(QuizSession, id=session_id, user=request.quiz_user)
|
||||
question_id = request.POST.get('question_id')
|
||||
selected_answer = request.POST.get('answer')
|
||||
|
||||
@@ -126,13 +220,93 @@ def submit_answer(request):
|
||||
QuizResult.objects.update_or_create(
|
||||
user=request.quiz_user,
|
||||
question=question,
|
||||
quiz_session=session,
|
||||
defaults={
|
||||
'selected_answer': selected_answer,
|
||||
'is_correct': is_correct,
|
||||
}
|
||||
)
|
||||
|
||||
return get_next_question(request)
|
||||
# Return the same question but with answer shown
|
||||
all_questions = get_session_questions(session)
|
||||
all_q_ids = list(all_questions.values_list('id', flat=True))
|
||||
current_index = all_q_ids.index(question.id) if question.id in all_q_ids else 0
|
||||
|
||||
context = {
|
||||
'question': question,
|
||||
'session': session,
|
||||
'show_answer': True,
|
||||
'is_correct': is_correct,
|
||||
'has_previous': current_index > 0,
|
||||
'has_next': current_index < len(all_q_ids) - 1,
|
||||
}
|
||||
|
||||
return render(request, 'partials/quiz_question.html', context)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def submit_difficulty(request, session_id):
|
||||
"""Record difficulty rating for FSRS"""
|
||||
session = get_object_or_404(QuizSession, id=session_id, user=request.quiz_user)
|
||||
question_id = request.POST.get('question_id')
|
||||
difficulty = request.POST.get('difficulty')
|
||||
|
||||
if not question_id or not difficulty:
|
||||
return HttpResponse("Invalid submission", status=400)
|
||||
|
||||
try:
|
||||
result = QuizResult.objects.get(
|
||||
user=request.quiz_user,
|
||||
quiz_session=session,
|
||||
question_id=question_id
|
||||
)
|
||||
result.difficulty = difficulty
|
||||
result.save()
|
||||
return HttpResponse("OK")
|
||||
except QuizResult.DoesNotExist:
|
||||
return HttpResponse("Result not found", status=404)
|
||||
|
||||
|
||||
def get_next_question(request, session_id):
|
||||
session = get_object_or_404(QuizSession, id=session_id, user=request.quiz_user)
|
||||
|
||||
answered_ids = QuizResult.objects.filter(
|
||||
user=request.quiz_user,
|
||||
quiz_session=session
|
||||
).values_list('question_id', flat=True)
|
||||
|
||||
questions = Question.objects.exclude(id__in=answered_ids)
|
||||
|
||||
# Apply filters from session
|
||||
if session.course:
|
||||
questions = questions.filter(exam__course=session.course)
|
||||
|
||||
if session.tags.exists():
|
||||
questions = questions.filter(tags__in=session.tags.all())
|
||||
|
||||
if session.exams.exists():
|
||||
questions = questions.filter(exam__in=session.exams.all())
|
||||
|
||||
if session.question_types:
|
||||
q_objs = Q()
|
||||
if 'single' in session.question_types:
|
||||
q_objs |= ~Q(correct_answer__contains=',')
|
||||
if 'multi' in session.question_types:
|
||||
q_objs |= Q(correct_answer__contains=',')
|
||||
|
||||
if q_objs:
|
||||
questions = questions.filter(q_objs)
|
||||
|
||||
questions = questions.distinct()
|
||||
next_question = questions.first()
|
||||
|
||||
if not next_question:
|
||||
return render(request, 'partials/complete.html', {'session': session})
|
||||
|
||||
return render(request, 'partials/question.html', {
|
||||
'question': next_question,
|
||||
'session': session
|
||||
})
|
||||
|
||||
|
||||
def stats(request):
|
||||
@@ -146,4 +320,3 @@ def stats(request):
|
||||
'percentage': round((correct / total * 100) if total > 0 else 0, 1),
|
||||
}
|
||||
return render(request, 'stats.html', context)
|
||||
|
||||
|
||||
@@ -3,19 +3,170 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quiz</title>
|
||||
<title>Medical Quiz</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||
.question { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||
.option { padding: 10px; margin: 5px 0; cursor: pointer; border: 2px solid #ddd; border-radius: 4px; }
|
||||
.option:hover { background: #e9e9e9; }
|
||||
.progress { background: #ddd; height: 20px; border-radius: 10px; margin: 20px 0; }
|
||||
.progress-bar { background: #4CAF50; height: 100%; border-radius: 10px; transition: width 0.3s; }
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--bg: #f8fafc;
|
||||
--card-bg: rgba(255, 255, 255, 0.8);
|
||||
--text-main: #1e293b;
|
||||
--text-muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--glass-bg: rgba(255, 255, 255, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text-main);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
background: var(--border);
|
||||
height: 0.5rem;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: var(--primary);
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
select, input, textarea {
|
||||
width: 100%;
|
||||
padding: 0.625rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
background-color: white;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.session-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.session-meta {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,53 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
<div class="header" style="margin-bottom: 3rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<h1 style="font-size: 2.5rem; margin-bottom: 0.5rem;">Välkommen</h1>
|
||||
<p style="color: var(--text-muted);">Här kan du hantera dina medicinska quiz.</p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-weight: 600; font-size: 1.25rem;">{{ answered_count }} / {{ total_questions }}</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-muted);">Frågor besvarade</div>
|
||||
<div class="progress-container" style="width: 150px; margin-top: 0.5rem;">
|
||||
<div class="progress-bar"
|
||||
style="width: {% if total_questions > 0 %}{{ answered_count|add:0|floatformat:2 }}{% else %}0{% endif %}%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
.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;
|
||||
}
|
||||
<h2 style="margin-bottom: 1.5rem;">Aktiva Quiz</h2>
|
||||
{% if active_sessions %}
|
||||
<div class="grid">
|
||||
{% for session in active_sessions %}
|
||||
<div class="glass-card session-card" id="session-{{ session.id }}">
|
||||
<div class="session-header">
|
||||
<div>
|
||||
<div class="session-title">
|
||||
{% if session.course %}{{ session.course.name }}{% else %}Blandat Quiz{% endif %}
|
||||
</div>
|
||||
<div class="session-meta">
|
||||
Startat {{ session.created_at|date:"Y-m-d H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<form action="{% url 'close_quiz' session.id %}" method="post" hx-post="{% url 'close_quiz' session.id %}"
|
||||
hx-target="#session-{{ session.id }}" hx-swap="outerHTML">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-secondary"
|
||||
style="padding: 0.25rem 0.5rem; color: #ef4444;">Stäng</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
.tag-chip.active {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
{% if session.tags.exists %}
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
{% for tag in session.tags.all %}
|
||||
<span
|
||||
style="font-size: 0.75rem; background: #e0e7ff; color: #4338ca; padding: 0.25rem 0.5rem; border-radius: 1rem;">{{
|
||||
tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
.tag-chip:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
|
||||
.tag-chip.active:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>Quiz Application</h1>
|
||||
|
||||
<div class="filter-section">
|
||||
<a href="{% url 'create_quiz' %}" class="tag-chip" style="background: #2196F3; color: white;">+ New Quiz</a>
|
||||
<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>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{% url 'quiz_mode' session.id %}" class="btn btn-primary" style="width: 100%; text-align: center;">
|
||||
Fortsätt Quiz
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar"
|
||||
style="width: {% if total_questions > 0 %}{{ answered_count|floatformat:0 }}{% else %}0{% endif %}%"></div>
|
||||
{% else %}
|
||||
<div class="glass-card" style="text-align: center; color: var(--text-muted); padding: 3rem;">
|
||||
Inga aktiva quiz. Starta ett nytt nedan!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-top: 4rem;">
|
||||
<h2 style="margin-bottom: 1.5rem;">Starta Nytt Quiz</h2>
|
||||
<div class="glass-card">
|
||||
<form method="post" action="{% url 'create_quiz' %}">
|
||||
{% csrf_token %}
|
||||
<div class="grid">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.course.id_for_label }}">Kurs</label>
|
||||
{{ form.course }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tags.id_for_label }}">Taggar</label>
|
||||
{{ form.tags }}
|
||||
<small style="color: var(--text-muted); margin-top: 0.25rem; display: block;">Håll ner Ctrl/Cmd för
|
||||
att välja flera.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
<button type="submit" class="btn btn-primary" style="padding-left: 2rem; padding-right: 2rem;">Skapa
|
||||
Quiz</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p>Besvarade frågor: {{ answered_count }} / {{ total_questions }}</p>
|
||||
<div id="quiz-container" hx-get="{% url 'next_question' %}" hx-trigger="load"></div>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,13 @@
|
||||
<div class="question">
|
||||
<h2>Quiz Completed!</h2>
|
||||
<p>Du har besvarat alla frågor.</p>
|
||||
<a href="{% url 'stats' %}">Se dina resultat</a>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding: 2rem;">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🎉</div>
|
||||
<h2 style="margin-bottom: 1rem;">Quiz Slutfört!</h2>
|
||||
<p style="color: var(--text-muted); margin-bottom: 2rem;">Bra jobbat! Du har besvarat alla tillgängliga frågor i
|
||||
detta urval.</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center;">
|
||||
<a href="{% url 'stats' %}" class="btn btn-secondary">Se Statistik</a>
|
||||
<form action="{% url 'close_quiz' session.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Avsluta & Gå Tillbaka</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,24 @@
|
||||
<div class="question">
|
||||
<h2>{{ question.text }}</h2>
|
||||
<form hx-post="{% url 'submit_answer' %}" hx-target="#quiz-container">
|
||||
<div class="quiz-content">
|
||||
<h3 style="margin-bottom: 1.5rem;">{{ question.text }}</h3>
|
||||
<form hx-post="{% url 'submit_answer' session.id %}" hx-target="#quiz-container-{{ session.id }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="question_id" value="{{ question.id }}">
|
||||
{% for option in question.options.all %}
|
||||
<div class="option" onclick="this.querySelector('input').checked = true; this.closest('form').requestSubmit();">
|
||||
<input type="radio" name="answer" value="{{ option.letter }}" id="opt_{{ option.letter }}" style="display:none;">
|
||||
<label for="opt_{{ option.letter }}">{{ option.letter }}. {{ option.text }}</label>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
{% for option in question.options.all %}
|
||||
<div class="option"
|
||||
style="padding: 1rem; border: 1px solid var(--border); border-radius: 0.5rem; cursor: pointer; transition: all 0.2s;"
|
||||
onmouseover="this.style.backgroundColor='#f1f5f9'" onmouseout="this.style.backgroundColor='transparent'"
|
||||
onclick="this.querySelector('input').checked = true; this.closest('form').requestSubmit();">
|
||||
<input type="radio" name="answer" value="{{ option.letter }}"
|
||||
id="opt_{{ session.id }}_{{ question.id }}_{{ option.letter }}" style="display:none;">
|
||||
<label style="cursor: pointer; font-weight: 500; color: var(--text-main);"
|
||||
for="opt_{{ session.id }}_{{ question.id }}_{{ option.letter }}">
|
||||
<span style="display: inline-block; width: 1.5rem; color: var(--primary); font-weight: 700;">{{
|
||||
option.letter }}</span>
|
||||
{{ option.text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
106
quiz/templates/partials/quiz_question.html
Normal file
106
quiz/templates/partials/quiz_question.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% if show_answer %}
|
||||
<div class="answer-feedback {{ 'correct' if is_correct else 'incorrect' }}">
|
||||
{% 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="options-container">
|
||||
{% for option in question.options.all %}
|
||||
<div class="option-item" id="option-{{ option.letter }}"
|
||||
onclick="selectOption('{{ option.letter }}', {{ question.id }}, {{ session.id }})">
|
||||
<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 }})">
|
||||
← Föregående
|
||||
</button>
|
||||
<button class="nav-btn" {% if not has_next %}disabled{% endif %}
|
||||
onclick="navigateQuestion('next', {{ session.id }})">
|
||||
Nästa →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedAnswer = null;
|
||||
|
||||
function selectOption(letter, questionId, sessionId) {
|
||||
if ({{ 'true' if show_answer else 'false' }
|
||||
}) return; // Don't allow changing answer after submission
|
||||
|
||||
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>
|
||||
210
quiz/templates/quiz_mode.html
Normal file
210
quiz/templates/quiz_mode.html
Normal file
@@ -0,0 +1,210 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.quiz-mode-container {
|
||||
max-width: 900px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.quiz-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.quiz-card {
|
||||
background: white;
|
||||
border-radius: 1.5rem;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
padding: 1.25rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
border-color: var(--primary);
|
||||
background: #f8f9fb;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.option-letter {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
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;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.nav-btn:disabled {
|
||||
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">
|
||||
<div class="quiz-header">
|
||||
<h1 style="margin: 0;">{{ session.course.name|default:"Quiz" }}</h1>
|
||||
<a href="{% url 'index' %}" class="btn btn-secondary">← Tillbaka till Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="quiz-card" id="quiz-content">
|
||||
<!-- Content loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load first question on page load
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
htmx.ajax('GET', '{% url 'quiz_question' session.id %}', { target: '#quiz-content' });
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user