Add flagging functionality for exam questions with a reusable Flag model and toggle feature
This commit is contained in:
+96
-2
@@ -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)):
|
||||
|
||||
Reference in New Issue
Block a user