From a1bc96f99aff86d42828b2dc1024e66c1dd4ce9a Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 22 Dec 2025 11:13:29 +0000 Subject: [PATCH] Add CidUserExam management features: forms, views, and templates for exam records --- generic/forms.py | 14 +++++++ .../generic/ciduserexam_record_form.html | 12 ++++++ generic/templates/generic/exam_cids.html | 35 +++++++++++------ .../partials/ciduserexam_detail_partial.html | 22 +++++++++++ generic/urls.py | 12 ++++++ generic/views.py | 38 +++++++++++++++++++ 6 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 generic/templates/generic/ciduserexam_record_form.html create mode 100644 generic/templates/generic/partials/ciduserexam_detail_partial.html diff --git a/generic/forms.py b/generic/forms.py index c2c1cce1..e36aaf5c 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -20,6 +20,7 @@ from django.contrib.auth.models import User from generic.models import ( CidUser, + CidUserExam, CidUserGroup, Examination, ExamCollection, @@ -541,6 +542,19 @@ class CidUserExamForm(ModelForm): fields = [] +class CidUserExamRecordForm(ModelForm): + class Meta: + model = CidUserExam + fields = [ + "start_time", + "end_time", + "completed", + "manual_submission", + "share_with_supervisor", + "results_emailed_status", + ] + + class CidUserGroupModelChoiceField(ModelMultipleChoiceField): def label_from_instance(self, obj): return f"{obj.cid} [{obj.email}]" diff --git a/generic/templates/generic/ciduserexam_record_form.html b/generic/templates/generic/ciduserexam_record_form.html new file mode 100644 index 00000000..1f38767b --- /dev/null +++ b/generic/templates/generic/ciduserexam_record_form.html @@ -0,0 +1,12 @@ +{% extends "generic/base.html" %} + +{% load crispy_forms_tags %} + +{% block content %} +

Edit CidUserExam record

+
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock %} diff --git a/generic/templates/generic/exam_cids.html b/generic/templates/generic/exam_cids.html index b4f6b79c..93156b85 100644 --- a/generic/templates/generic/exam_cids.html +++ b/generic/templates/generic/exam_cids.html @@ -21,17 +21,25 @@
    {% for cid in cid_users %} -
  1. - - {{cid.cid}} [{{cid.passcode}}]
    {{cid.name}}
    Email: {{cid.email}}
    -
  2. +
  3. +
    + + + {{cid.cid}} + [{{cid.passcode}}] + + +
    +
    {{cid.name}}
    +
    Email: {{cid.email}}
    +
  4. {% endfor %}
@@ -132,6 +140,11 @@ }) + {% endblock js %} diff --git a/generic/templates/generic/partials/ciduserexam_detail_partial.html b/generic/templates/generic/partials/ciduserexam_detail_partial.html new file mode 100644 index 00000000..fab9016e --- /dev/null +++ b/generic/templates/generic/partials/ciduserexam_detail_partial.html @@ -0,0 +1,22 @@ +
+

CidUserExam: {% if cid_user_exam.cid_user %}{{ cid_user_exam.cid_user.cid }}{% elif cid_user_exam.user_user %}{{ cid_user_exam.user_user.username }}{% else %}-{% endif %}

+ + + + + + + + + + + + +
CID{% if cid_user_exam.cid_user %}{{ cid_user_exam.cid_user.cid }}{% else %}-{% endif %}
User{% if cid_user_exam.user_user %}{{ cid_user_exam.user_user.username }}{% else %}-{% endif %}
Start time{{ cid_user_exam.start_time }}
End time{{ cid_user_exam.end_time }}
Completed{{ cid_user_exam.completed }}
Manual submission{{ cid_user_exam.manual_submission }}
Share with supervisor{{ cid_user_exam.share_with_supervisor }}
Results emailed status{{ cid_user_exam.results_emailed_status }}
CID User email{% if cid_user_exam.cid_user %}{{ cid_user_exam.cid_user.email }}{% else %}-{% endif %}
+
+ {% if cid_user_exam %} + Edit + {% endif %} + +
+
diff --git a/generic/urls.py b/generic/urls.py index 5a2b8a32..de5f3afb 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -124,6 +124,18 @@ urlpatterns = [ views.CidUserGroupUpdate.as_view(), name="cid_group_update", ), + + # HTMX partial to show a CidUserExam record for an exam (used from exam lists) + path( + "exam///cid//ciduserexam", + views.cid_user_exam_partial, + name="cid_user_exam_partial", + ), + path( + "cids/ciduserexam//update", + views.CidUserExamRecordUpdate.as_view(), + name="update_cid_user_exam", + ), path( "cids/group//delete", views.CidUserGroupDelete.as_view(), diff --git a/generic/views.py b/generic/views.py index 118e0949..d06e3d43 100644 --- a/generic/views.py +++ b/generic/views.py @@ -110,6 +110,26 @@ from .models import ( ) +@user_is_cid_user_manager +def cid_user_exam_partial(request, app_name: str, exam_id: int, cid: int): + """Return an HTMX partial showing details for a CidUserExam record. + + Matches by content_type app_label and object_id (exam id), and cid value. + """ + # Find the matching CidUserExam + try: + cux = CidUserExam.objects.select_related("cid_user", "user_user", "content_type").filter( + content_type__app_label=app_name, object_id=exam_id, cid_user__cid=cid + ).first() + except Exception: + cux = None + + if not cux: + return HttpResponse("No record found", status=404) + + return render(request, "generic/partials/ciduserexam_detail_partial.html", {"cid_user_exam": cux}) + + from rapids.models import ( Rapid as RapidQuestion, ExamQuestionDetail as RapidsExamQuestionDetail, @@ -5295,6 +5315,8 @@ class CidUserUpdate(CidManagerRequiredMixin, UpdateView): class CidUserExamUpdate(CidManagerRequiredMixin, UpdateView): + # This view edits the CidUser (which stores which exams the CID is registered for). + # Keep model as `CidUser` so existing URL `update_cid_exams` continues to work. model = CidUser form_class = CidUserExamForm template_name = "generic/ciduserexam_form.html" @@ -5316,6 +5338,22 @@ class CidUserGroupCreate(RevisionMixin, CidManagerRequiredMixin, CreateView): form_class = CidUserGroupForm +class CidUserExamRecordUpdate(CidManagerRequiredMixin, UpdateView): + """Update a CidUserExam record (individual exam record for a CID/user). + + Separate from `CidUserExamUpdate` which edits a `CidUser`'s exam memberships. + """ + model = CidUserExam + form_class = None + template_name = "generic/ciduserexam_record_form.html" + + def get_form_class(self): + # import locally to avoid circular imports at module import time + from .forms import CidUserExamRecordForm + + return CidUserExamRecordForm + + class CidUserGroupUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView): model = CidUserGroup form_class = CidUserGroupForm