Add QuestionReview model and implement review functionality with HTMX integration
This commit is contained in:
@@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<div id="question-review-block" class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<div class="small text-muted">Latest review</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-info text-white">{{ review.get_status_display }}</span>
|
||||||
|
<div class="small">{{ review.comment }}</div>
|
||||||
|
<div class="text-muted small ms-2">by {{ review.get_author_str }} on {{ review.created_on }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" hx-get="{% url app_name|add:':question_set_review' question.pk %}?edit=1" hx-target="#question-review-block" hx-swap="outerHTML">Edit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<form method="post"
|
||||||
|
hx-post="{% url app_name|add:':question_set_review' question.pk %}?edit=1"
|
||||||
|
hx-target="#question-review-block"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="d-flex gap-2 align-items-start">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-select-sm">
|
||||||
|
<select name="status" class="form-select form-select-sm">
|
||||||
|
<option value="AC" {% if latest and latest.status == 'AC' %}selected{% endif %}>Accepted</option>
|
||||||
|
<option value="OD" {% if latest and latest.status == 'OD' %}selected{% endif %}>Outdated</option>
|
||||||
|
<option value="ER" {% if latest and latest.status == 'ER' %}selected{% endif %}>Error</option>
|
||||||
|
<option value="RJ" {% if latest and latest.status == 'RJ' %}selected{% endif %}>Rejected</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="comment" class="form-control form-control-sm" placeholder="Comment (optional)" value="{% if latest %}{{ latest.comment }}{% endif %}">
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Save</button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" hx-get="{{ request.path }}" hx-target="#question-review-block" hx-swap="outerHTML">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -276,6 +276,11 @@ def generic_view_urls(generic_views: GenericViewBase):
|
|||||||
path(
|
path(
|
||||||
"question/<int:pk>/", generic_views.question_detail, name="question_detail"
|
"question/<int:pk>/", generic_views.question_detail, name="question_detail"
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"question/<int:pk>/set_review",
|
||||||
|
generic_views.question_set_review,
|
||||||
|
name="question_set_review",
|
||||||
|
),
|
||||||
path("question/<int:pk>/thumbnail/fail", generic_views.question_thumbnail_fail, name="series_thumbnail_fail"),
|
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("question/<int:pk>/thumbnail", generic_views.question_thumbnail, name="series_thumbnail"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -1988,6 +1988,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
"view_feedback": view_feedback,
|
"view_feedback": view_feedback,
|
||||||
"can_edit": can_edit,
|
"can_edit": can_edit,
|
||||||
"remote_url": settings.REMOTE_URL,
|
"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)
|
question_json = question.get_question_json(based=False, feedback=True)
|
||||||
return JsonResponse(question_json)
|
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))
|
@method_decorator(user_passes_test(lambda u: u.is_superuser))
|
||||||
def user_answer_delete_multiple(self, request):
|
def user_answer_delete_multiple(self, request):
|
||||||
print(request.POST["answer_ids"])
|
print(request.POST["answer_ids"])
|
||||||
@@ -2999,6 +3050,7 @@ class GenericViewBase:
|
|||||||
"view_feedback": view_feedback,
|
"view_feedback": view_feedback,
|
||||||
"remote_url": settings.REMOTE_URL,
|
"remote_url": settings.REMOTE_URL,
|
||||||
"can_edit": question.can_edit(request.user),
|
"can_edit": question.can_edit(request.user),
|
||||||
|
"app_name": self.app_name,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div id="question-review-block" hx-get="{% url 'sbas:question_set_review' question.pk %}" hx-trigger="load" hx-swap="innerHTML">
|
||||||
|
{# Placeholder while HTMX fetches review/form #}
|
||||||
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3 flex-column flex-md-row">
|
<div class="d-flex justify-content-between align-items-start mb-3 flex-column flex-md-row">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="h4 mb-1">{{ question.title|default:"(No title set)" }}</h2>
|
<h2 class="h4 mb-1">{{ question.title|default:"(No title set)" }}</h2>
|
||||||
@@ -14,6 +17,7 @@
|
|||||||
<a class="btn btn-sm btn-outline-danger" href="{% url 'sbas:question_delete' pk=question.pk %}">Delete</a>
|
<a class="btn btn-sm btn-outline-danger" href="{% url 'sbas:question_delete' pk=question.pk %}">Delete</a>
|
||||||
<a class="btn btn-sm btn-outline-light" href="{% url 'admin:sbas_question_change' question.id %}">Admin</a>
|
<a class="btn btn-sm btn-outline-light" href="{% url 'admin:sbas_question_change' question.id %}">Admin</a>
|
||||||
<button class="btn btn-sm btn-outline-info" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='sbas' pk=question.pk %}')">Add Note</button>
|
<button class="btn btn-sm btn-outline-info" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='sbas' pk=question.pk %}')">Add Note</button>
|
||||||
|
<button class="btn btn-sm btn-outline-success" hx-get="{% url 'sbas:question_set_review' question.pk %}?edit=1&new=1" hx-target="#question-review-block" hx-swap="outerHTML">Add Review</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user