From 96e88c05a4891b859296ac97f798613c494f36da Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 5 Jan 2026 11:25:10 +0000 Subject: [PATCH] Enhance submission tracking by adding first and last name data attributes for candidates, improving badge tooltip information in the submitted candidates list. --- generic/templates/generic/exam_cids.html | 24 +++++++++++++++---- .../partials/exam_cids_submitted_list.html | 2 +- generic/views.py | 20 +++++++++++++--- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/generic/templates/generic/exam_cids.html b/generic/templates/generic/exam_cids.html index f33f29d9..5703fd34 100644 --- a/generic/templates/generic/exam_cids.html +++ b/generic/templates/generic/exam_cids.html @@ -159,14 +159,18 @@ // data attributes for cid/user. Then insert a small badge on matching list items. // Collect submitted identifiers first to avoid duplicate processing const submittedEls = document.querySelectorAll('#submitted-candidates [data-cid], #submitted-candidates [data-user]'); - const submittedCids = new Set(); - const submittedUsers = new Set(); + // Build maps of submitted identifiers -> their timestamps so we + // can attach tooltip text to the badges we create below. + const submittedCids = new Map(); + const submittedUsers = new Map(); submittedEls.forEach(function(el){ try{ const cid = el.dataset && el.dataset.cid ? el.dataset.cid.toString() : null; const user = el.dataset && el.dataset.user ? el.dataset.user.toString() : null; - if(cid && cid !== 'None') submittedCids.add(cid); - if(user && user !== 'None') submittedUsers.add(user); + const first = el.dataset && el.dataset.first ? el.dataset.first.toString() : ''; + const last = el.dataset && el.dataset.last ? el.dataset.last.toString() : ''; + if(cid && cid !== 'None') submittedCids.set(cid, {first:first, last:last}); + if(user && user !== 'None') submittedUsers.set(user, {first:first, last:last}); }catch(e){ console.error('highlightSubmitted collect error', e); } }); @@ -177,9 +181,15 @@ const link = li.querySelector && li.querySelector('.cid-link'); const text = (link && link.textContent) ? link.textContent.trim() : (li.textContent||'').trim(); if(submittedCids.has(text) && !li.querySelector('.submitted-badge')){ + const info = submittedCids.get(text) || {first:'', last:''}; const badge = document.createElement('span'); badge.className = 'submitted-badge badge bg-success ms-2'; badge.textContent = 'Submitted'; + if(info.first || info.last){ + const title = `Submitted\nFirst: ${info.first}\nLast: ${info.last}`; + badge.setAttribute('title', title); + badge.setAttribute('data-bs-toggle', 'tooltip'); + } if(link && link.parentNode){ link.parentNode.appendChild(badge); } else { @@ -198,9 +208,15 @@ for(const user of submittedUsers){ if(txt.indexOf(user) !== -1){ if(!li.querySelector('.submitted-badge')){ + const info = submittedUsers.get(user) || {first:'', last:''}; const badge = document.createElement('span'); badge.className = 'submitted-badge badge bg-success ms-2'; badge.textContent = 'Submitted'; + if(info.first || info.last){ + const title = `Submitted\nFirst: ${info.first}\nLast: ${info.last}`; + badge.setAttribute('title', title); + badge.setAttribute('data-bs-toggle', 'tooltip'); + } const nameEl = li.querySelector('.fw-bold') || li.querySelector('a') || li; nameEl.appendChild(badge); } diff --git a/generic/templates/generic/partials/exam_cids_submitted_list.html b/generic/templates/generic/partials/exam_cids_submitted_list.html index 07d50b95..5f4914f0 100644 --- a/generic/templates/generic/partials/exam_cids_submitted_list.html +++ b/generic/templates/generic/partials/exam_cids_submitted_list.html @@ -23,7 +23,7 @@ {% for user_data in user_exam_data %} - + {{ user_data.cid_user }} {{ user_data.user_user }} {{ user_data.start_time }} diff --git a/generic/views.py b/generic/views.py index cdceb9ea..ca0a0d21 100644 --- a/generic/views.py +++ b/generic/views.py @@ -56,7 +56,7 @@ from reversion.views import RevisionMixin from atlas.models import CaseCollection, CaseDetail from generic.decorators import user_is_cid_user_manager from generic.filters import CidUserFilter, ExaminationFilter, SupervisorFilter -from generic.models import UserUserGroup +from generic.models import UserUserGroup, CidUserExam from generic.tables import ( CidUserExamTable, @@ -2335,9 +2335,23 @@ class ExamViews(View, LoginRequiredMixin): if request.user not in exam.author.all() and not request.user.is_superuser: raise PermissionDenied - user_exam_data = exam.cid_users.all().prefetch_related("cid_user", "user_user") + # Explicitly query the CidUserExam entries for this exam to avoid any + # ambiguity from GenericRelation managers and to ensure we only return + # submissions that belong to this exam instance. + from django.contrib.contenttypes.models import ContentType - return render(request, "generic/partials/exam_cids_submitted_list.html", {"user_exam_data": user_exam_data, "exam": exam}) + ct = ContentType.objects.get_for_model(self.Exam) + user_exam_data = ( + CidUserExam.objects.filter(content_type=ct, object_id=exam.pk) + .select_related("cid_user", "user_user") + .order_by("start_time") + ) + + return render( + request, + "generic/partials/exam_cids_submitted_list.html", + {"user_exam_data": user_exam_data, "exam": exam}, + ) # def exam_groups_edit(self, request, exam_id): # exam = get_object_or_404(self.Exam, pk=exam_id)