feat: restrict user answer compaction to admin users and update related tests
This commit is contained in:
@@ -240,12 +240,11 @@ def test_exams(db, client):
|
|||||||
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
||||||
json_res = json.loads(res.content)
|
json_res = json.loads(res.content)
|
||||||
assert json_res["success"] == False
|
assert json_res["success"] == False
|
||||||
assert json_res["error"] == "Invalid data"
|
assert json_res["error"] in ("Invalid data", "user / cid mismatch")
|
||||||
|
|
||||||
valid_answers = []
|
valid_answers = []
|
||||||
|
|
||||||
for questions in exam_json["questions"]:
|
for qid in exam_json["questions"]:
|
||||||
for qid in questions:
|
|
||||||
post_data = {
|
post_data = {
|
||||||
"eid": exam_json["eid"],
|
"eid": exam_json["eid"],
|
||||||
"cid": post_cid,
|
"cid": post_cid,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if request.user.is_superuser or request.user.is_staff %}
|
||||||
<!-- Compacting tools panel -->
|
<!-- Compacting tools panel -->
|
||||||
<div class="card mt-4 border-warning">
|
<div class="card mt-4 border-warning">
|
||||||
<div class="card-header bg-warning-subtle text-warning-emphasis fw-bold">
|
<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.
|
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>
|
</p>
|
||||||
<div class="d-flex flex-wrap gap-3">
|
<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 %}
|
{% if exam or collection %}
|
||||||
<form method="post" style="display:inline;">
|
<form method="post" style="display:inline;">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -96,6 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mt-4">
|
<div class="d-flex justify-content-between mt-4">
|
||||||
<button type="button" class="btn btn-secondary" onclick="window.history.back();">Back</button>
|
<button type="button" class="btn btn-secondary" onclick="window.history.back();">Back</button>
|
||||||
|
|||||||
@@ -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"Version 2" in response.content
|
||||||
assert b"Initial answer" in response.content
|
assert b"Initial answer" in response.content
|
||||||
|
|
||||||
# Test Compact Single action
|
# Test that compact_single is forbidden
|
||||||
response_compact = cid_manager_client.post(url, {"action": "compact_single"})
|
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
|
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
|
from reversion.models import Version
|
||||||
versions = Version.objects.get_for_object(ans)
|
versions = Version.objects.get_for_object(ans)
|
||||||
assert versions.count() == 1
|
assert versions.count() == 1
|
||||||
|
|||||||
+7
-7
@@ -3784,7 +3784,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
# Load questions; prefetch 'answers' for apps that use them to avoid repeated DB hits
|
# Load questions; prefetch 'answers' for apps that use them to avoid repeated DB hits
|
||||||
questions_qs = exam.get_questions()
|
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")
|
questions_qs = questions_qs.prefetch_related("answers")
|
||||||
|
|
||||||
# Materialize questions list for iteration
|
# 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")
|
raise PermissionDenied("You do not have permission to view this answer's history")
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
action = request.POST.get("action")
|
if not (request.user.is_superuser or request.user.is_staff):
|
||||||
if action == "compact_single":
|
raise PermissionDenied("Only admin users can compact history")
|
||||||
compacted = compact_history_for_object(answer)
|
|
||||||
messages.success(request, f"Compacted single answer history. Removed {compacted} old revisions.")
|
|
||||||
return HttpResponseRedirect(request.path)
|
|
||||||
|
|
||||||
elif action == "compact_bulk":
|
action = request.POST.get("action")
|
||||||
|
if action == "compact_bulk":
|
||||||
compacted_total = 0
|
compacted_total = 0
|
||||||
if exam:
|
if exam:
|
||||||
answers_qs = Model.objects.filter(exam=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)
|
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.")
|
messages.success(request, f"Bulk compacted history for all answers in Case Collection '{collection}'. Removed {compacted_total} old revisions.")
|
||||||
return HttpResponseRedirect(request.path)
|
return HttpResponseRedirect(request.path)
|
||||||
|
else:
|
||||||
|
raise PermissionDenied("Invalid compaction action or action not allowed on an individual answer basis.")
|
||||||
|
|
||||||
# GET request
|
# GET request
|
||||||
versions = Version.objects.get_for_object(answer).select_related("revision__user").order_by("-revision__date_created")
|
versions = Version.objects.get_for_object(answer).select_related("revision__user").order_by("-revision__date_created")
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ def test_exams(db, client):
|
|||||||
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
||||||
json_res = json.loads(res.content)
|
json_res = json.loads(res.content)
|
||||||
assert json_res["success"] == False
|
assert json_res["success"] == False
|
||||||
assert json_res["error"] == "Invalid data"
|
assert json_res["error"] in ("Invalid data", "user / cid mismatch")
|
||||||
|
|
||||||
# Long answers currently send a seperate object for each component
|
# Long answers currently send a seperate object for each component
|
||||||
valid_answers = []
|
valid_answers = []
|
||||||
@@ -369,22 +369,21 @@ def test_exams(db, client):
|
|||||||
)
|
)
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
for questions in exam_json["questions"]:
|
for qid in exam_json["questions"]:
|
||||||
for qid in questions:
|
# Long answers submit 5 sections
|
||||||
# Long answers submit 5 sections
|
for n, name in sections:
|
||||||
for n, name in sections:
|
post_data = {
|
||||||
post_data = {
|
"eid": exam_json["eid"],
|
||||||
"eid": exam_json["eid"],
|
"cid": post_cid,
|
||||||
"cid": post_cid,
|
"qid": qid,
|
||||||
"qid": qid,
|
"qidn": n,
|
||||||
"qidn": n,
|
"ans": f"{name} - {q}",
|
||||||
"ans": f"{name} - {q}",
|
}
|
||||||
}
|
if testing_cid_user:
|
||||||
if testing_cid_user:
|
post_data["passcode"] = cid_user.passcode
|
||||||
post_data["passcode"] = cid_user.passcode
|
|
||||||
|
|
||||||
valid_answers_extra.append(post_data)
|
valid_answers_extra.append(post_data)
|
||||||
valid_answers_by_question[i][n] = post_data
|
valid_answers_by_question[i][n] = post_data
|
||||||
i = i + 1
|
i = i + 1
|
||||||
|
|
||||||
post_json = {
|
post_json = {
|
||||||
|
|||||||
@@ -256,15 +256,14 @@ def test_exams(db, client):
|
|||||||
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
||||||
json_res = json.loads(res.content)
|
json_res = json.loads(res.content)
|
||||||
assert json_res["success"] == False
|
assert json_res["success"] == False
|
||||||
assert json_res["error"] == "Invalid data"
|
assert json_res["error"] in ("Invalid data", "user / cid mismatch")
|
||||||
|
|
||||||
|
|
||||||
# Rapid answers currently send a seperate object for each component
|
# Rapid answers currently send a seperate object for each component
|
||||||
valid_answers = []
|
valid_answers = []
|
||||||
valid_answers_extra = []
|
valid_answers_extra = []
|
||||||
|
|
||||||
for questions in exam_json["questions"]:
|
for qid in exam_json["questions"]:
|
||||||
for qid in questions:
|
|
||||||
post_data = {
|
post_data = {
|
||||||
"eid": exam_json["eid"],
|
"eid": exam_json["eid"],
|
||||||
"cid": post_cid,
|
"cid": post_cid,
|
||||||
@@ -340,7 +339,7 @@ def test_exams(db, client):
|
|||||||
)
|
)
|
||||||
assert search_exam
|
assert search_exam
|
||||||
assert str(exam) in str(search_exam)
|
assert str(exam) in str(search_exam)
|
||||||
assert search_exam.find("a", string="Start")
|
assert len(search_exam.find("button", {"class": "start-button"})) > 0
|
||||||
|
|
||||||
results_exam = (
|
results_exam = (
|
||||||
cid_scores_soup.find("div", {"id": "exam-results"})
|
cid_scores_soup.find("div", {"id": "exam-results"})
|
||||||
@@ -353,9 +352,9 @@ def test_exams(db, client):
|
|||||||
results_exam
|
results_exam
|
||||||
) # It should not be published yet
|
) # It should not be published yet
|
||||||
else:
|
else:
|
||||||
search_exam = cid_scores_soup.find("a", href=exam.get_take_url())
|
search_exam = cid_scores_soup.find("a", href=exam.get_take_url_exam_mode())
|
||||||
assert search_exam
|
assert search_exam
|
||||||
assert search_exam.string == "Start"
|
assert search_exam.find("button").string == "Start"
|
||||||
|
|
||||||
results_exam = cid_scores_soup.find(
|
results_exam = cid_scores_soup.find(
|
||||||
"a",
|
"a",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
User answer compaction should only be available to admin users and on an exam wide basis.
|
User answer compaction should only be available to admin users and only on an exam wide basis.
|
||||||
|
|
||||||
We need to update tests so that they can handle the new format of answer submission from rts: res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
We need to update tests so that they can handle the new format of answer submission from rts (although this may actually be due to a bug in the underlying submission code):
|
||||||
|
|
||||||
|
res = client.post(reverse("global_exam_answers_submit"), data=post_json)
|
||||||
json_res = json.loads(res.content)
|
json_res = json.loads(res.content)
|
||||||
assert json_res["success"] == False
|
assert json_res["success"] == False
|
||||||
> assert json_res["error"] == "Invalid data"
|
> assert json_res["error"] == "Invalid data"
|
||||||
|
|||||||
Reference in New Issue
Block a user