feat: restrict user answer compaction to admin users and update related tests

This commit is contained in:
Ross
2026-07-06 09:43:01 +01:00
parent daebdc1844
commit e56c1f2d0f
7 changed files with 53 additions and 45 deletions
@@ -66,6 +66,7 @@
</table>
</div>
{% if request.user.is_superuser or request.user.is_staff %}
<!-- Compacting tools panel -->
<div class="card mt-4 border-warning">
<div class="card-header bg-warning-subtle text-warning-emphasis fw-bold">
@@ -76,14 +77,6 @@
Compacting history deletes all older versions and their associated revision logs, retaining only the final answer database state. This is permanent and cannot be undone.
</p>
<div class="d-flex flex-wrap gap-3">
<form method="post" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="action" value="compact_single">
<button type="submit" class="btn btn-warning btn-sm" onclick="return confirm('Are you sure you want to delete all historical edits for this individual answer?');">
Compact Single Answer
</button>
</form>
{% if exam or collection %}
<form method="post" style="display:inline;">
{% csrf_token %}
@@ -96,6 +89,7 @@
</div>
</div>
</div>
{% endif %}
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-secondary" onclick="window.history.back();">Back</button>
+18 -3
View File
@@ -280,11 +280,26 @@ def test_answer_history_and_compacting(cid_manager_client, exam_data, logged_in_
assert b"Version 2" in response.content
assert b"Initial answer" in response.content
# Test Compact Single action
response_compact = cid_manager_client.post(url, {"action": "compact_single"})
# Test that compact_single is forbidden
response_compact_single = cid_manager_client.post(url, {"action": "compact_single"})
assert response_compact_single.status_code == 403 # forbidden/denied
# Test Compact Bulk action is forbidden for non-admin/non-staff
response_compact_bulk_fail = cid_manager_client.post(url, {"action": "compact_bulk"})
assert response_compact_bulk_fail.status_code == 403
# Elevate the user to superuser/admin to test successful bulk compaction
from django.contrib.auth import get_user_model
User = get_user_model()
manager_user = User.objects.get(username="manager1")
manager_user.is_superuser = True
manager_user.save()
# Test Compact Bulk action (exam-wide compaction) succeeds for admin
response_compact = cid_manager_client.post(url, {"action": "compact_bulk"})
assert response_compact.status_code == 302 # redirect
# Verify only 1 version remains
# Verify only 1 version remains for our answer object
from reversion.models import Version
versions = Version.objects.get_for_object(ans)
assert versions.count() == 1
+7 -7
View File
@@ -3784,7 +3784,7 @@ class ExamViews(View, LoginRequiredMixin):
# Load questions; prefetch 'answers' for apps that use them to avoid repeated DB hits
questions_qs = exam.get_questions()
if self.app_name not in ("physics", "sbas"):
if self.app_name == "anatomy":
questions_qs = questions_qs.prefetch_related("answers")
# Materialize questions list for iteration
@@ -5302,13 +5302,11 @@ def view_answer_history(request, app_name, model_name, pk):
raise PermissionDenied("You do not have permission to view this answer's history")
if request.method == "POST":
action = request.POST.get("action")
if action == "compact_single":
compacted = compact_history_for_object(answer)
messages.success(request, f"Compacted single answer history. Removed {compacted} old revisions.")
return HttpResponseRedirect(request.path)
if not (request.user.is_superuser or request.user.is_staff):
raise PermissionDenied("Only admin users can compact history")
elif action == "compact_bulk":
action = request.POST.get("action")
if action == "compact_bulk":
compacted_total = 0
if exam:
answers_qs = Model.objects.filter(exam=exam)
@@ -5325,6 +5323,8 @@ def view_answer_history(request, app_name, model_name, pk):
compacted_total += compact_history_for_object(ans)
messages.success(request, f"Bulk compacted history for all answers in Case Collection '{collection}'. Removed {compacted_total} old revisions.")
return HttpResponseRedirect(request.path)
else:
raise PermissionDenied("Invalid compaction action or action not allowed on an individual answer basis.")
# GET request
versions = Version.objects.get_for_object(answer).select_related("revision__user").order_by("-revision__date_created")