vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
This commit is contained in:
30
stroma/quiz/views/__init__.py
Normal file
30
stroma/quiz/views/__init__.py
Normal 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',
|
||||
]
|
||||
|
||||
18
stroma/quiz/views/close_quiz_view.py
Normal file
18
stroma/quiz/views/close_quiz_view.py
Normal 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')
|
||||
|
||||
48
stroma/quiz/views/create_quiz_view.py
Normal file
48
stroma/quiz/views/create_quiz_view.py
Normal 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})
|
||||
|
||||
46
stroma/quiz/views/get_next_question_view.py
Normal file
46
stroma/quiz/views/get_next_question_view.py
Normal 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
|
||||
})
|
||||
|
||||
29
stroma/quiz/views/get_session_questions_view.py
Normal file
29
stroma/quiz/views/get_session_questions_view.py
Normal 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()
|
||||
|
||||
9
stroma/quiz/views/handle_tag_filter_view.py
Normal file
9
stroma/quiz/views/handle_tag_filter_view.py
Normal 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
|
||||
|
||||
18
stroma/quiz/views/index_view.py
Normal file
18
stroma/quiz/views/index_view.py
Normal 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)
|
||||
|
||||
57
stroma/quiz/views/navigate_question_view.py
Normal file
57
stroma/quiz/views/navigate_question_view.py
Normal 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)
|
||||
|
||||
14
stroma/quiz/views/quiz_mode_view.py
Normal file
14
stroma/quiz/views/quiz_mode_view.py
Normal 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
|
||||
})
|
||||
|
||||
63
stroma/quiz/views/quiz_question_view.py
Normal file
63
stroma/quiz/views/quiz_question_view.py
Normal 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)
|
||||
|
||||
16
stroma/quiz/views/stats_view.py
Normal file
16
stroma/quiz/views/stats_view.py
Normal 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)
|
||||
|
||||
58
stroma/quiz/views/submit_answer_view.py
Normal file
58
stroma/quiz/views/submit_answer_view.py
Normal 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)
|
||||
|
||||
28
stroma/quiz/views/submit_difficulty_view.py
Normal file
28
stroma/quiz/views/submit_difficulty_view.py
Normal 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)
|
||||
|
||||
13
stroma/quiz/views/tag_count_api_view.py
Normal file
13
stroma/quiz/views/tag_count_api_view.py
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user