From 208252a8581474de2bf664dc4abfc6abdb61a705 Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 12 Nov 2025 21:48:50 +0000 Subject: [PATCH] Add grade filter to user search widget and enhance logging for bulk add operations --- generic/forms.py | 159 +++++++++++------- generic/models.py | 37 ++++ .../partials/user_search_widget_results.html | 9 +- generic/views.py | 29 +++- 4 files changed, 169 insertions(+), 65 deletions(-) diff --git a/generic/forms.py b/generic/forms.py index 20d81691..cf56377b 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -593,77 +593,112 @@ class UserUserGroupForm(ModelForm): url = reverse("generic:user_search_widget") + # Build a small grade filter select + grades_options = '' + try: + from generic.models import UserGrades + grades_qs = UserGrades.objects.all() + for g in grades_qs: + grades_options += f'' + except Exception: + grades_options = '' + html = f""" -
- +
+ -
- -
-
+
+
+ + +
+
+
-
- -
    - {selected_html} -
-
+
+ +
    + {selected_html} +
+
- -
-""" + // add to visible list + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center'; + li.id = 'selected_user_' + fieldName + '_' + id; + const span = document.createElement('span'); span.innerHTML = text; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm btn-outline-danger ms-2'; btn.innerText = 'Remove'; + btn.addEventListener('click', function() {{ removeUserFromField(fieldName, id); }}); + li.appendChild(span); li.appendChild(btn); + selectedList.appendChild(li); + }} + + window.removeUserFromField = function(fieldName, id) {{ + const sel = document.getElementById('id_' + fieldName); + if (!sel) return; + for (let i=sel.options.length-1;i>=0;i--) {{ if (sel.options[i].value == id) sel.remove(i); }} + const li = document.getElementById('selected_user_' + fieldName + '_' + id); + if (li && li.parentNode) li.parentNode.removeChild(li); + }} + + window.addAllFromResults = function(fieldName) {{ + const list = document.getElementById('user_search_results_list_' + fieldName); + if (!list) return; + const items = list.querySelectorAll('li[data-user-id]'); + items.forEach(function(it) {{ + const id = it.getAttribute('data-user-id'); + const text = it.getAttribute('data-user-text') || it.textContent.trim(); + addUserToField(fieldName, id, text); + }}); + }} + }})(); + +
+ """ return mark_safe(html) diff --git a/generic/models.py b/generic/models.py index c69d8740..c3a15775 100644 --- a/generic/models.py +++ b/generic/models.py @@ -1967,6 +1967,13 @@ class ExamCollection(models.Model, AuthorMixin): if UserUserGroup is None: raise RuntimeError("UserUserGroup model not available") + # Debug: log what we received + try: + group_pks = [g.pk for g in groups] if groups is not None else [] + except Exception: + group_pks = [] + logger.debug("bulk_add_user_user_groups called on collection=%s with groups=%s exam_types=%s", self.pk, group_pks, exam_types) + # Normalize groups to a list of model instances if groups is None: return 0 @@ -1994,9 +2001,21 @@ class ExamCollection(models.Model, AuthorMixin): for exam in exams: # Each exam model exposes a `user_user_groups` ManyToManyField try: + before_pks = set(exam.user_user_groups.values_list('pk', flat=True)) exam.user_user_groups.add(*groups_qs) # Ensure any downstream signals / caches see the change exam.save() + after_pks = set(exam.user_user_groups.values_list('pk', flat=True)) + # log if expected group pks are not present after add + try: + expected_pks = {g.pk for g in groups_qs} + except Exception: + expected_pks = set() + if not expected_pks.issubset(after_pks): + logger.warning( + "After add, exam %s missing expected user_user_groups; before=%s after=%s expected=%s", + getattr(exam, 'pk', exam), sorted(before_pks), sorted(after_pks), sorted(expected_pks), + ) updated += 1 except Exception as e: logger.exception("Failed to add user_user_groups to exam %s: %s", getattr(exam, 'pk', exam), e) @@ -2011,6 +2030,13 @@ class ExamCollection(models.Model, AuthorMixin): if CidUserGroup is None: raise RuntimeError("CidUserGroup model not available") + # Debug: log what we received + try: + group_pks = [g.pk for g in groups] if groups is not None else [] + except Exception: + group_pks = [] + logger.debug("bulk_add_cid_user_groups called on collection=%s with groups=%s exam_types=%s", self.pk, group_pks, exam_types) + if groups is None: return 0 @@ -2033,8 +2059,19 @@ class ExamCollection(models.Model, AuthorMixin): for exam in exams: # Each exam model exposes a `cid_user_groups` ManyToManyField try: + before_pks = set(exam.cid_user_groups.values_list('pk', flat=True)) exam.cid_user_groups.add(*groups_qs) exam.save() + after_pks = set(exam.cid_user_groups.values_list('pk', flat=True)) + try: + expected_pks = {g.pk for g in groups_qs} + except Exception: + expected_pks = set() + if not expected_pks.issubset(after_pks): + logger.warning( + "After add, exam %s missing expected cid_user_groups; before=%s after=%s expected=%s", + getattr(exam, 'pk', exam), sorted(before_pks), sorted(after_pks), sorted(expected_pks), + ) updated += 1 except Exception as e: logger.exception("Failed to add cid_user_groups to exam %s: %s", getattr(exam, 'pk', exam), e) diff --git a/generic/templates/generic/partials/user_search_widget_results.html b/generic/templates/generic/partials/user_search_widget_results.html index 5ea7b4d3..62ba7527 100644 --- a/generic/templates/generic/partials/user_search_widget_results.html +++ b/generic/templates/generic/partials/user_search_widget_results.html @@ -1,7 +1,7 @@ {% if results %} -