From b417266e56ed03d6d5b83c3cc236f930fad7aabf Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 22 Oct 2025 21:00:01 +0100 Subject: [PATCH] Add QuestionReview model and implement review functionality with HTMX integration --- generic/migrations/0026_questionreview.py | 29 +++++++++++ .../partials/question_review_block.html | 15 ++++++ .../partials/question_review_form.html | 20 +++++++ generic/urls.py | 5 ++ generic/views.py | 52 +++++++++++++++++++ sbas/templates/sbas/question_detail.html | 4 ++ 6 files changed, 125 insertions(+) create mode 100644 generic/migrations/0026_questionreview.py create mode 100644 generic/templates/generic/partials/question_review_block.html create mode 100644 generic/templates/generic/partials/question_review_form.html diff --git a/generic/migrations/0026_questionreview.py b/generic/migrations/0026_questionreview.py new file mode 100644 index 00000000..4d571f8b --- /dev/null +++ b/generic/migrations/0026_questionreview.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.4 on 2025-10-22 19:19 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('generic', '0025_cimarseriesthumbnail'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='QuestionReview', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('comment', models.TextField(blank=True)), + ('status', models.CharField(choices=[('AC', 'Accepted'), ('OD', 'Outdated'), ('ER', 'Error'), ('RJ', 'Rejected')], default='AC', max_length=2)), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + ), + ] diff --git a/generic/templates/generic/partials/question_review_block.html b/generic/templates/generic/partials/question_review_block.html new file mode 100644 index 00000000..7f713267 --- /dev/null +++ b/generic/templates/generic/partials/question_review_block.html @@ -0,0 +1,15 @@ +
+
+
+
Latest review
+
+ {{ review.get_status_display }} +
{{ review.comment }}
+
by {{ review.get_author_str }} on {{ review.created_on }}
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/generic/templates/generic/partials/question_review_form.html b/generic/templates/generic/partials/question_review_form.html new file mode 100644 index 00000000..a60db9fe --- /dev/null +++ b/generic/templates/generic/partials/question_review_form.html @@ -0,0 +1,20 @@ +
+ {% csrf_token %} +
+ +
+ +
+ + +
+
\ No newline at end of file diff --git a/generic/urls.py b/generic/urls.py index 444b8e92..0b652dd9 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -276,6 +276,11 @@ def generic_view_urls(generic_views: GenericViewBase): path( "question//", generic_views.question_detail, name="question_detail" ), + path( + "question//set_review", + generic_views.question_set_review, + name="question_set_review", + ), 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 cb43c3e8..6d41bc02 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1988,6 +1988,7 @@ class ExamViews(View, LoginRequiredMixin): "view_feedback": view_feedback, "can_edit": can_edit, "remote_url": settings.REMOTE_URL, + "app_name": self.app_name, }, ) @@ -2947,6 +2948,56 @@ class GenericViewBase: question_json = question.get_question_json(based=False, feedback=True) return JsonResponse(question_json) + def question_set_review(self, request, pk): + """HTMX endpoint to get/set the latest QuestionReview for a question. + + GET: renders the current review block or the form (if ?edit=1) + POST: creates a new QuestionReview and returns the rendered block + """ + from django.contrib.contenttypes.models import ContentType + from generic.models import QuestionReview + + logger.debug("question_set_review called") + + question = get_object_or_404(self.question_object, pk=pk) + + ct = ContentType.objects.get_for_model(question) + latest = ( + QuestionReview.objects.filter(content_type=ct, object_id=question.pk) + .order_by("-created_on").first() + ) + + # If GET and edit requested, return the form. If `new` is set, render with no latest so the + # form is blank for creating a fresh review. + if request.method == "GET" and request.GET.get("edit"): + if request.GET.get("new"): + return render(request, "generic/partials/question_review_form.html", {"question": question, "latest": None, "app_name": self.app_name}) + return render(request, "generic/partials/question_review_form.html", {"question": question, "latest": latest, "app_name": self.app_name}) + + if request.method == "POST": + # Require login to post a review + if not request.user.is_authenticated: + return HttpResponse(status=403) + + status = request.POST.get("status") + comment = request.POST.get("comment", "") + + review = QuestionReview.objects.create( + content_type=ct, + object_id=question.pk, + author=request.user, + status=status, + comment=comment, + ) + + return render(request, "generic/partials/question_review_block.html", {"review": review, "question": question, "app_name": self.app_name}) + + # Default: render the block (latest if exists, otherwise form) + if latest: + return render(request, "generic/partials/question_review_block.html", {"review": latest, "question": question, "app_name": self.app_name}) + else: + return render(request, "generic/partials/question_review_form.html", {"question": question, "latest": latest, "app_name": self.app_name}) + @method_decorator(user_passes_test(lambda u: u.is_superuser)) def user_answer_delete_multiple(self, request): print(request.POST["answer_ids"]) @@ -2999,6 +3050,7 @@ class GenericViewBase: "view_feedback": view_feedback, "remote_url": settings.REMOTE_URL, "can_edit": question.can_edit(request.user), + "app_name": self.app_name, }, ) diff --git a/sbas/templates/sbas/question_detail.html b/sbas/templates/sbas/question_detail.html index 38fc7c57..69470105 100644 --- a/sbas/templates/sbas/question_detail.html +++ b/sbas/templates/sbas/question_detail.html @@ -3,6 +3,9 @@ {% block content %}
+
+ {# Placeholder while HTMX fetches review/form #} +

{{ question.title|default:"(No title set)" }}

@@ -14,6 +17,7 @@ Delete Admin +