1
0

vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s

This commit is contained in:
2025-12-26 02:09:22 +01:00
parent 3fddadfe50
commit 50366b9b9c
288 changed files with 58893 additions and 750 deletions

View File

@@ -0,0 +1,30 @@
from .create_quiz_view import create_quiz
from .index_view import index
from .get_session_questions_view import get_session_questions
from .quiz_mode_view import quiz_mode
from .quiz_question_view import quiz_question
from .navigate_question_view import navigate_question
from .submit_answer_view import submit_answer
from .submit_difficulty_view import submit_difficulty
from .get_next_question_view import get_next_question
from .close_quiz_view import close_quiz
from .handle_tag_filter_view import handle_tag_filter
from .stats_view import stats
from .tag_count_api_view import tag_count_api
__all__ = [
'create_quiz',
'index',
'get_session_questions',
'quiz_mode',
'quiz_question',
'navigate_question',
'submit_answer',
'submit_difficulty',
'get_next_question',
'close_quiz',
'handle_tag_filter',
'stats',
'tag_count_api',
]

View File

@@ -0,0 +1,18 @@
from django.shortcuts import redirect, get_object_or_404
from django.http import HttpResponse
from django.views.decorators.http import require_http_methods
from quiz.models import QuizSession
@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()
# If it's an HTMX request, return empty response (card will be removed)
if request.headers.get('HX-Request'):
return HttpResponse('')
return redirect('quiz:index')

View File

@@ -0,0 +1,48 @@
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect
from quiz.models import QuizSession, Course, Tag
from quiz.forms import CreateQuizForm
def create_quiz(request: HttpRequest) -> HttpResponse:
if request.method == 'POST':
# Handle quick-start tag-based quiz
tag_slug = request.POST.get('tag_slug')
if tag_slug:
try:
tag = Tag.objects.get(slug=tag_slug)
course = Course.objects.first() # Get first course
session = QuizSession.objects.create(
user=request.quiz_user,
course=course,
question_types=[]
)
session.tags.set([tag])
return redirect('quiz:quiz_mode', session_id=session.id)
except Tag.DoesNotExist:
pass
# Handle custom form-based quiz
form = CreateQuizForm(request.POST)
if form.is_valid():
course = form.cleaned_data.get('course')
exams = form.cleaned_data.get('exams')
tags = form.cleaned_data.get('tags')
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:
session.tags.set(tags)
if exams:
session.exams.set(exams)
return redirect('quiz:quiz_mode', session_id=session.id)
else:
form = CreateQuizForm()
return render(request, 'quiz_create.html', {'form': form})

View File

@@ -0,0 +1,46 @@
from django.shortcuts import render, get_object_or_404
from django.db.models import Q
from quiz.models import QuizSession, Question, QuizResult
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
})

View File

@@ -0,0 +1,29 @@
from django.db.models import Q
from quiz.models import Question
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 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)
return questions.distinct()

View File

@@ -0,0 +1,9 @@
def handle_tag_filter(request):
tag_slug = request.GET.get('tag')
if tag_slug is not None:
if tag_slug == "":
if 'quiz_tag' in request.session:
del request.session['quiz_tag']
else:
request.session['quiz_tag'] = tag_slug

View File

@@ -0,0 +1,18 @@
from django.shortcuts import render
from quiz.models import QuizSession, QuizResult, Question
from quiz.forms import CreateQuizForm
def index(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,
'active_sessions': active_sessions,
'form': CreateQuizForm(), # Include form on landing page
}
return render(request, 'index.html', context)

View File

@@ -0,0 +1,57 @@
from django.shortcuts import render, get_object_or_404
from quiz.models import QuizSession, QuizResult
from .get_session_questions_view import get_session_questions
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()
current_number = new_index + 1 # 1-based numbering
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,
'current_number': current_number,
'total_questions': len(all_q_ids),
}
if result:
context['is_correct'] = result.is_correct
return render(request, 'partials/quiz_question.html', context)

View File

@@ -0,0 +1,14 @@
from django.shortcuts import render, get_object_or_404
from quiz.models import QuizSession
from .get_session_questions_view import get_session_questions
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)
total_questions = get_session_questions(session).count()
return render(request, 'quiz_mode.html', {
'session': session,
'total_questions': total_questions
})

View File

@@ -0,0 +1,63 @@
from django.shortcuts import render, get_object_or_404
from quiz.models import QuizSession, QuizResult
from .get_session_questions_view import get_session_questions
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
current_number = current_index + 1 # 1-based numbering
context = {
'question': question,
'session': session,
'show_answer': show_answer,
'has_previous': current_index > 0,
'has_next': current_index < len(all_q_ids) - 1,
'current_number': current_number,
'total_questions': len(all_q_ids),
}
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)

View File

@@ -0,0 +1,16 @@
from django.shortcuts import render
from quiz.models import QuizResult
def stats(request):
results = QuizResult.objects.filter(user=request.quiz_user)
total = results.count()
correct = results.filter(is_correct=True).count()
context = {
'total': total,
'correct': correct,
'percentage': round((correct / total * 100) if total > 0 else 0, 1),
}
return render(request, 'stats.html', context)

View File

@@ -0,0 +1,58 @@
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.views.decorators.http import require_http_methods
from quiz.models import QuizSession, Question, QuizResult
from .get_session_questions_view import get_session_questions
@require_http_methods(["POST"])
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')
if not question_id or not selected_answer:
return HttpResponse("Invalid submission", status=400)
try:
question = Question.objects.get(id=question_id)
except Question.DoesNotExist:
return HttpResponse("Question not found", status=404)
# 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,
question=question,
quiz_session=session,
defaults={
'selected_answer': selected_answer,
'is_correct': is_correct,
}
)
# 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
current_number = current_index + 1 # 1-based numbering
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,
'current_number': current_number,
'total_questions': len(all_q_ids),
}
return render(request, 'partials/quiz_question.html', context)

View File

@@ -0,0 +1,28 @@
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.views.decorators.http import require_http_methods
from quiz.models import QuizSession, QuizResult
@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)

View File

@@ -0,0 +1,13 @@
from django.http import JsonResponse
from quiz.models import Tag, Question
def tag_count_api(request, tag_slug):
"""API endpoint to get question count for a tag"""
try:
tag = Tag.objects.get(slug=tag_slug)
count = Question.objects.filter(tags=tag).count()
return JsonResponse({'count': count, 'tag': tag.name})
except Tag.DoesNotExist:
return JsonResponse({'count': 0, 'error': 'Tag not found'}, status=404)