Implement question review flow with category and status filters; add templates for review start, question display, and completion
This commit is contained in:
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user