Add exam review functionality with navigation for questions
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'generic/examcollection_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
<div class="alert alert-info">Review complete for exam: {{ exam.name }}</div>
|
||||||
|
<p><a class="btn btn-secondary" href="{% url app_name|add:':exam_overview' exam.pk %}">Back to exam overview</a></p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'generic/examcollection_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
<h2>Reviewing: {{ exam.name }} (Question {{ q_index|add:1 }} of {{ total }})</h2>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Question</h5>
|
||||||
|
<p class="lead">{{ question }}</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{% url app_name|add:':question_detail' question.pk %}" target="_blank">View full question details</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-3">
|
||||||
|
{% if prev_index is not None %}
|
||||||
|
<a class="btn btn-outline-primary" href="{% url app_name|add:':exam_review_question' exam.pk prev_index %}">← Previous</a>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if next_index is not None %}
|
||||||
|
<a class="btn btn-primary" href="{% url app_name|add:':exam_review_question' exam.pk next_index %}">Next →</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-success" href="{% url app_name|add:':exam_overview' exam.pk %}">Complete review</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -315,6 +315,16 @@ def generic_view_urls(generic_views: GenericViewBase):
|
|||||||
generic_views.question_user_answers,
|
generic_views.question_user_answers,
|
||||||
name="question_user_answers",
|
name="question_user_answers",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"exam/<int:pk>/review/start",
|
||||||
|
generic_views.exam_review_start,
|
||||||
|
name="exam_review_start",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"exam/<int:pk>/review/<int:q_index>",
|
||||||
|
generic_views.exam_review_question,
|
||||||
|
name="exam_review_question",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"question/<int:pk>/user_answers/<str:answer_compare>/str",
|
"question/<int:pk>/user_answers/<str:answer_compare>/str",
|
||||||
generic_views.question_user_answers_by_compare,
|
generic_views.question_user_answers_by_compare,
|
||||||
|
|||||||
@@ -1163,6 +1163,61 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
data = {"status": "error"}
|
data = {"status": "error"}
|
||||||
return JsonResponse(data, status=400)
|
return JsonResponse(data, status=400)
|
||||||
|
|
||||||
|
@method_decorator(login_required)
|
||||||
|
def exam_review_start(self, request, pk):
|
||||||
|
"""Start a per-question review for an exam. Redirects to the first question."""
|
||||||
|
# Ensure the user can edit the exam (authors only)
|
||||||
|
if not self.check_user_edit_access(request.user, exam_id=pk):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
exam = get_object_or_404(self.Exam, pk=pk)
|
||||||
|
|
||||||
|
# Materialise ordered questions
|
||||||
|
questions = list(exam.get_questions())
|
||||||
|
if not questions:
|
||||||
|
return render(request, "generic/exam_review_complete.html", {"exam": exam, "app_name": self.app_name})
|
||||||
|
|
||||||
|
# Redirect (render) the first question
|
||||||
|
return self.exam_review_question(request, pk, q_index=0)
|
||||||
|
|
||||||
|
@method_decorator(login_required)
|
||||||
|
def exam_review_question(self, request, pk, q_index=0):
|
||||||
|
"""Render a single question from an exam for review with navigation."""
|
||||||
|
if not self.check_user_edit_access(request.user, exam_id=pk):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
exam = get_object_or_404(self.Exam, pk=pk)
|
||||||
|
|
||||||
|
questions = list(exam.get_questions())
|
||||||
|
total = len(questions)
|
||||||
|
|
||||||
|
try:
|
||||||
|
q_index = int(q_index)
|
||||||
|
except Exception:
|
||||||
|
q_index = 0
|
||||||
|
|
||||||
|
if q_index < 0 or q_index >= total:
|
||||||
|
# Out of range — render complete
|
||||||
|
return render(request, "generic/exam_review_complete.html", {"exam": exam, "app_name": self.app_name})
|
||||||
|
|
||||||
|
question = questions[q_index]
|
||||||
|
|
||||||
|
prev_index = q_index - 1 if q_index > 0 else None
|
||||||
|
next_index = q_index + 1 if q_index < total - 1 else None
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"exam": exam,
|
||||||
|
"question": question,
|
||||||
|
"q_index": q_index,
|
||||||
|
"total": total,
|
||||||
|
"prev_index": prev_index,
|
||||||
|
"next_index": next_index,
|
||||||
|
"app_name": self.app_name,
|
||||||
|
"can_edit": self.check_user_edit_access(request.user, exam_id=pk),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "generic/exam_review_question.html", context)
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def exam_json_recreate(self, request, pk):
|
def exam_json_recreate(self, request, pk):
|
||||||
exam = get_object_or_404(self.Exam, pk=pk)
|
exam = get_object_or_404(self.Exam, pk=pk)
|
||||||
|
|||||||
Reference in New Issue
Block a user