Add flagging functionality for exam questions with a reusable Flag model and toggle feature

This commit is contained in:
Ross
2026-01-05 13:47:45 +00:00
parent e4cfb1f782
commit bf344f20e1
6 changed files with 246 additions and 47 deletions
+96 -2
View File
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from generic.models import CidUser, ExamUserStatus, CidUserExam
from generic.models import CidUser, ExamUserStatus, CidUserExam, Flag
from physics.decorators import user_is_author_or_physics_checker
from physics.filters import QuestionFilter, UserAnswerFilter
from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
@@ -352,6 +352,18 @@ def exam_take(request, pk: int, sk: int, cid: str | None = None, passcode: str |
if answer is not None:
saved_answer = [answer.a, answer.b, answer.c, answer.d, answer.e]
# compute flagged state for this question+actor
try:
ct = ContentType.objects.get_for_model(question)
flags_qs = Flag.objects.filter(content_type=ct, object_id=question.pk)
if cid is not None:
flags_qs = flags_qs.filter(cid_user__cid=cid)
else:
flags_qs = flags_qs.filter(user=request.user)
flagged = flags_qs.exists()
except Exception:
flagged = False
return render(
request,
"physics/exam_take.html",
@@ -367,6 +379,7 @@ def exam_take(request, pk: int, sk: int, cid: str | None = None, passcode: str |
"saved_answer": saved_answer,
"passcode": passcode,
"cid_user_exam": cid_user_exam,
"flagged": flagged,
},
)
@@ -416,6 +429,18 @@ def exam_take_fragment(request, pk: int, sk: int, cid: str | None = None, passco
form = UserAnswerForm()
saved_answer = False
# compute flagged state for this question+actor so fragment renders correctly
try:
ct = ContentType.objects.get_for_model(question)
flags_qs = Flag.objects.filter(content_type=ct, object_id=question.pk)
if cid is not None:
flags_qs = flags_qs.filter(cid_user__cid=cid)
else:
flags_qs = flags_qs.filter(user=request.user)
flagged = flags_qs.exists()
except Exception:
flagged = False
previous = -1
if sk > 0:
previous = sk - 1
@@ -435,13 +460,82 @@ def exam_take_fragment(request, pk: int, sk: int, cid: str | None = None, passco
"previous": previous,
"exam_length": exam_length,
"pos": pos,
"saved_answer": saved_answer,
"saved_answer": saved_answer,
"answer": answer,
"flagged": flagged,
"passcode": passcode,
"cid_user_exam": cid_user_exam,
},
)
@login_required
def exam_toggle_flag(request, pk: int, sk: int, cid: str | None = None, passcode: str | None = None):
"""Toggle the flagged state for the current user's answer to a question.
Returns the small flag-button partial so HTMX clients can swap it in-place.
"""
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
return exam_inactive(request, context={"exam": exam})
exam.check_user_can_take(cid, passcode, request.user)
# canonical questions list
questions = list(exam.get_questions())
try:
index = int(sk)
except Exception:
raise Http404("Invalid question index")
if index < 0 or index >= len(questions):
raise Http404("Question not found in exam")
question = questions[index]
# Determine actor: either a CidUser (for cid flows) or the logged-in user
cid_user_obj = None
if cid is not None:
try:
cid_user_obj = CidUser.objects.filter(cid=cid).first()
except Exception:
cid_user_obj = None
# Find existing flag for this question+actor
ct = ContentType.objects.get_for_model(question)
flags_qs = Flag.objects.filter(content_type=ct, object_id=question.pk)
if cid_user_obj is not None:
flags_qs = flags_qs.filter(cid_user=cid_user_obj)
else:
flags_qs = flags_qs.filter(user=request.user)
flagged = flags_qs.exists()
if request.method == "POST":
# set param explicitly controls state; otherwise toggle
set_val = request.POST.get("set")
if set_val is None:
# toggle
if flagged:
flags_qs.delete()
flagged = False
else:
Flag.objects.create(content_type=ct, object_id=question.pk, user=(None if cid_user_obj else request.user), cid_user=cid_user_obj)
flagged = True
else:
desired = str(set_val).lower() in ("1", "true", "yes")
if desired and not flagged:
Flag.objects.create(content_type=ct, object_id=question.pk, user=(None if cid_user_obj else request.user), cid_user=cid_user_obj)
flagged = True
elif not desired and flagged:
flags_qs.delete()
flagged = False
# Render the partial button for the current state
return render(request, "physics/partials/exam_flag_button.html", {"flagged": flagged, "exam": exam, "pos": index, "cid": cid, "passcode": passcode})
# def loadJsonAnswer(answer):
# # As access is not restricted make sure the data appears valid
# if (not isinstance(answer["cid"], int)) or (not isinstance(answer["eid"], int)):