diff --git a/generic/templates/generic/exam_review_complete.html b/generic/templates/generic/exam_review_complete.html new file mode 100644 index 00000000..9ab21c77 --- /dev/null +++ b/generic/templates/generic/exam_review_complete.html @@ -0,0 +1,8 @@ +{% extends 'generic/examcollection_base.html' %} + +{% block content %} +
+
Review complete for exam: {{ exam.name }}
+

Back to exam overview

+
+{% endblock %} diff --git a/generic/templates/generic/exam_review_question.html b/generic/templates/generic/exam_review_question.html new file mode 100644 index 00000000..d59afc4c --- /dev/null +++ b/generic/templates/generic/exam_review_question.html @@ -0,0 +1,32 @@ +{% extends 'generic/examcollection_base.html' %} + +{% block content %} +
+

Reviewing: {{ exam.name }} (Question {{ q_index|add:1 }} of {{ total }})

+ +
+
+
Question
+

{{ question }}

+ +

+ View full question details +

+ +
+ {% if prev_index is not None %} + ← Previous + {% else %} + + {% endif %} + + {% if next_index is not None %} + Next → + {% else %} + Complete review + {% endif %} +
+
+
+
+{% endblock %} diff --git a/generic/urls.py b/generic/urls.py index 1b2ac1dd..7739ef80 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -315,6 +315,16 @@ def generic_view_urls(generic_views: GenericViewBase): generic_views.question_user_answers, name="question_user_answers", ), + path( + "exam//review/start", + generic_views.exam_review_start, + name="exam_review_start", + ), + path( + "exam//review/", + generic_views.exam_review_question, + name="exam_review_question", + ), path( "question//user_answers//str", generic_views.question_user_answers_by_compare, diff --git a/generic/views.py b/generic/views.py index 92c8fc2d..c96acb18 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1163,6 +1163,61 @@ class ExamViews(View, LoginRequiredMixin): data = {"status": "error"} 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) def exam_json_recreate(self, request, pk): exam = get_object_or_404(self.Exam, pk=pk)