Implement question review flow with category and status filters; add templates for review start, question display, and completion

This commit is contained in:
Ross
2025-10-27 10:21:07 +00:00
parent ec45464e58
commit 8380ca1dd9
8 changed files with 236 additions and 34 deletions
@@ -4,7 +4,6 @@
hx-swap="innerHTML"
class="w-100">
{% csrf_token %}
<div class="row g-2 align-items-start">
<div class="col-auto">
<label class="form-label mb-1">Status</label>
@@ -22,16 +21,16 @@
{% endif %}
</div>
<div class="col">
<div class="col-md-12 mt-2">
<label for="{{ form.comment.id_for_label }}" class="form-label mb-1">Comment</label>
{{ form.comment }}
{% if form.comment.errors %}
<div class="invalid-feedback d-block">{{ form.comment.errors|join:", " }}</div>
{% endif %}
</div>
<div class="col-auto d-flex align-items-end">
<button class="btn btn-primary" type="submit">Save</button>
</div>
</div>
<div class="col-auto d-flex align-items-end mt-2">
<button class="btn btn-primary" type="submit">Save</button>
</div>
</div>
</form>
@@ -1,9 +0,0 @@
{% extends 'sbas/base.html' %}
{% load partials %}
{% block content %}
Test
{% endblock %}
+6 -1
View File
@@ -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/<int:pk>/thumbnail/fail", generic_views.question_thumbnail_fail, name="series_thumbnail_fail"),
path("question/<int:pk>/thumbnail", generic_views.question_thumbnail, name="series_thumbnail"),
path(
+93 -1
View File
@@ -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))