From 8380ca1dd9dca0a45916af8f5329955509819520 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 27 Oct 2025 10:21:07 +0000 Subject: [PATCH] Implement question review flow with category and status filters; add templates for review start, question display, and completion --- .../partials/question_review_form.html | 11 +-- .../templates/generic/question_review.html | 9 -- generic/urls.py | 7 +- generic/views.py | 94 ++++++++++++++++++- rad/settings.py | 61 ++++++++---- .../sbas/question_review_complete.html | 9 ++ .../sbas/question_review_question.html | 40 ++++++++ .../templates/sbas/question_review_start.html | 39 ++++++++ 8 files changed, 236 insertions(+), 34 deletions(-) delete mode 100644 generic/templates/generic/question_review.html create mode 100644 sbas/templates/sbas/question_review_complete.html create mode 100644 sbas/templates/sbas/question_review_question.html create mode 100644 sbas/templates/sbas/question_review_start.html diff --git a/generic/templates/generic/partials/question_review_form.html b/generic/templates/generic/partials/question_review_form.html index 10bc56e4..a489929c 100644 --- a/generic/templates/generic/partials/question_review_form.html +++ b/generic/templates/generic/partials/question_review_form.html @@ -4,7 +4,6 @@ hx-swap="innerHTML" class="w-100"> {% csrf_token %} -
@@ -22,16 +21,16 @@ {% endif %}
-
+
{{ form.comment }} {% if form.comment.errors %}
{{ form.comment.errors|join:", " }}
{% endif %}
- -
- -
+
+ +
+
\ No newline at end of file diff --git a/generic/templates/generic/question_review.html b/generic/templates/generic/question_review.html deleted file mode 100644 index f38f6051..00000000 --- a/generic/templates/generic/question_review.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'sbas/base.html' %} - -{% load partials %} - -{% block content %} - -Test - -{% endblock %} \ No newline at end of file diff --git a/generic/urls.py b/generic/urls.py index fb73d46a..24c82280 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -287,10 +287,15 @@ def generic_view_urls(generic_views: GenericViewBase): name="question_reviews", ), path( - "question/review/", + "question/review/start", generic_views.question_review_start, name="question_review_start", ), + path( + "question/review/next", + generic_views.question_review_next, + name="question_review_next", + ), path("question//thumbnail/fail", generic_views.question_thumbnail_fail, name="series_thumbnail_fail"), path("question//thumbnail", generic_views.question_thumbnail, name="series_thumbnail"), path( diff --git a/generic/views.py b/generic/views.py index be69b256..5e6657b2 100644 --- a/generic/views.py +++ b/generic/views.py @@ -3033,9 +3033,101 @@ class GenericViewBase: def question_review_start(self, request): + # Prepare category choices if the question model exposes a FK named 'category' + categories = None + try: + field = self.question_object._meta.get_field("category") + # Only handle FK-like fields + if hasattr(field, "related_model") and field.related_model is not None: + CategoryModel = field.related_model + # prefer 'category' or 'name' display field if present + order_by = "category" if hasattr(CategoryModel, "category") else "name" if hasattr(CategoryModel, "name") else "pk" + categories = CategoryModel.objects.all().order_by(order_by) + except Exception: + categories = None - return render(request, "generic/question_review_start.html", {"app_name": self.app_name}) + # Status options map: label -> status code (or special 'UNREVIEWED') + status_options = [ + ("ANY", "Any"), + ("UNREVIEWED", "Unreviewed"), + ("AC", "Accepted"), + ("OD", "Outdated"), + ("ER", "Error"), + ("RJ", "Rejected"), + ("IP", "In Progress"), + ] + return render( + request, + f"{self.app_name}/question_review_start.html", + {"app_name": self.app_name, "categories": categories, "status_options": status_options}, + ) + + + def question_review_next(self, request): + """Find and redirect to the next question matching the supplied filters. + + Accepts POST or GET parameters: + - category: integer primary key of category (optional) + - status: one of the status codes (AC, OD, ER, RJ, IP), or 'UNREVIEWED' or 'ANY' + """ + from django.contrib.contenttypes.models import ContentType + from generic.models import QuestionReview + + # Read filters + category = request.POST.get("category") or request.GET.get("category") + status = request.POST.get("status") or request.GET.get("status") or "ANY" + + # Build base queryset of questions for this app + qs = self.question_object.objects.all().order_by("pk") + + # Restrict by category if provided and the model has such a relation + if category: + try: + qs = qs.filter(category__id=int(category)) + except Exception: + pass + + # Apply permission filter similar to filters elsewhere: non-checkers see only open_access or their own + if not request.user.groups.filter(name=self.checker_group).exists(): + try: + qs = qs.filter(open_access=True) | qs.filter(author__id=request.user.id) + except Exception: + # If model doesn't have those fields, ignore + pass + + # Iterate questions and pick first matching status + for question in qs: + # Skip authors_only questions unless user is author or superuser + try: + if question.authors_only and not ( + request.user.is_superuser or request.user in question.author.all() + ): + continue + except Exception: + pass + + ct = ContentType.objects.get_for_model(question) + latest = ( + QuestionReview.objects.filter(content_type=ct, object_id=question.pk) + .order_by("-created_on").first() + ) + + match = False + if status == "ANY": + match = True + elif status == "UNREVIEWED": + match = latest is None + else: + # status is expected to be a QuestionReview.StatusChoices code + if latest is not None and latest.status == status: + match = True + + if match: + return render(request, f"{self.app_name}/question_review_question.html", {"question": question}) + + # Nothing found - render complete page + return render(request, f"{self.app_name}/question_review_complete.html", {"app_name": self.app_name}) @method_decorator(user_passes_test(lambda u: u.is_superuser)) diff --git a/rad/settings.py b/rad/settings.py index 8e6cb681..23f27eb9 100644 --- a/rad/settings.py +++ b/rad/settings.py @@ -118,24 +118,51 @@ ROOT_URLCONF = "rad.urls" # #cached_loaders = [("django.template.loaders.cached.Loader", default_loaders)] -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "templates")], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.template.context_processors.static", - ], - #"loaders": default_loaders if DEBUG else cached_loaders, +if DEBUG: + # Development: do not use cached loader so templates reload on each request + TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.static", + ], + }, }, - - }, -] + ] +else: + # Production: enable cached loader for performance + TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": False, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.static", + ], + "loaders": [ + ( + "django.template.loaders.cached.Loader", + [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ], + ) + ], + }, + }, + ] WSGI_APPLICATION = "rad.wsgi.application" diff --git a/sbas/templates/sbas/question_review_complete.html b/sbas/templates/sbas/question_review_complete.html new file mode 100644 index 00000000..d429c2d1 --- /dev/null +++ b/sbas/templates/sbas/question_review_complete.html @@ -0,0 +1,9 @@ +{% extends 'sbas/base.html' %} + +{% block content %} + +

Review complete

+ +

There are no more questions matching your filters.

+ +{% endblock %} diff --git a/sbas/templates/sbas/question_review_question.html b/sbas/templates/sbas/question_review_question.html new file mode 100644 index 00000000..a51050a3 --- /dev/null +++ b/sbas/templates/sbas/question_review_question.html @@ -0,0 +1,40 @@ +{# Generic question display used in review UI. Tries to be resilient across apps. #} +
+ {% if question.title %} +
{{ question.title }}
+ {% endif %} + +
+ {% if question.get_stem_stripped %} +
{{ question.get_stem_stripped|safe }}
+ {% elif question.stem %} +
{{ question.stem|safe }}
+ {% endif %} +
+ + {# Multiple-choice answers (SBA style) #} + {% if question.get_answers is defined or question.a_answer is defined %} +
+ {% for code,label in (('a','A'),('b','B'),('c','C'),('d','D'),('e','E')) %} + {% with ans=getattr(question, code|add:'_answer', None) %} + {% if ans %} +
+
+ {{ label }}. + {{ ans|safe }} +
+ {% if question.best_answer and question.best_answer == code %} + Correct + {% endif %} +
+ {% endif %} + {% endwith %} + {% endfor %} +
+ {% endif %} + + {# Generic feedback text if present #} + {% if question.feedback %} +
Feedback: {{ question.feedback|safe }}
+ {% endif %} +
diff --git a/sbas/templates/sbas/question_review_start.html b/sbas/templates/sbas/question_review_start.html new file mode 100644 index 00000000..4bc4ce4c --- /dev/null +++ b/sbas/templates/sbas/question_review_start.html @@ -0,0 +1,39 @@ +{% extends 'sbas/base.html' %} + +{% load partials %} + +{% block content %} + +

Review Questions

+ +

Filter questions to review. Choose a category and/or a review status, then click Start to open the first matching question.

+ +
+ {% csrf_token %} +
+ + {% if categories %} + + {% else %} +

No category choices available for this question type.

+ {% endif %} +
+ +
+ + +
+ + +
+ +{% endblock %} \ No newline at end of file