diff --git a/generic/forms.py b/generic/forms.py
index 613ca8a8..acc489a2 100755
--- a/generic/forms.py
+++ b/generic/forms.py
@@ -2,6 +2,7 @@ from django.forms import (
Form,
ModelForm,
ModelMultipleChoiceField,
+ MultipleChoiceField,
ModelChoiceField,
ChoiceField,
CharField,
@@ -291,6 +292,43 @@ class ExamGroupsFormMixin(ModelForm):
self.fields["user_user_groups"].widget.attrs["size"] = 8
+class ExamCollectionBulkAddGroupsForm(Form):
+ """Form used by the HTMX modal to bulk-add CID/User groups to an ExamCollection.
+
+ Accepts `user` kwarg in __init__ to limit available groups based on permissions
+ (mirrors behaviour of `ExamGroupsFormMixin`).
+ """
+ cid_user_groups = ModelMultipleChoiceField(required=False, queryset=CidUserGroup.objects.none(), label="CID User Groups")
+ user_user_groups = ModelMultipleChoiceField(required=False, queryset=UserUserGroup.objects.none(), label="User Groups")
+ EXAM_TYPE_CHOICES = [
+ ("all", "All"),
+ ("anatomy", "Anatomy"),
+ ("longs", "Longs"),
+ ("rapids", "Rapids"),
+ ("shorts", "Shorts"),
+ ("physics", "Physics"),
+ ("sbas", "SBAs"),
+ ]
+ exam_types = MultipleChoiceField(required=False, choices=EXAM_TYPE_CHOICES, label="Exam types (leave empty for all)")
+
+ def __init__(self, *args, user=None, **kwargs):
+ # keep compatibility with other forms that pass user as kwarg
+ # This is a plain Form (not a ModelForm) so call Form.__init__
+ Form.__init__(self, *args, **kwargs)
+ # choose queryset depending on permissions
+ if user and (user.is_superuser or user.groups.filter(name="cid_user_manager").exists()):
+ cid_qs = CidUserGroup.objects.filter(archive=False)
+ user_qs = UserUserGroup.objects.filter(archive=False)
+ else:
+ cid_qs = CidUserGroup.objects.filter(archive=False, open_access=True)
+ user_qs = UserUserGroup.objects.filter(archive=False, open_access=True)
+
+ self.fields["cid_user_groups"].queryset = cid_qs
+ self.fields["user_user_groups"].queryset = user_qs
+ self.fields["cid_user_groups"].widget.attrs["size"] = 8
+ self.fields["user_user_groups"].widget.attrs["size"] = 8
+
+
class ExaminationForm(ModelForm):
def __init__(self, *args, **kwargs):
# Where is this coming from?
diff --git a/generic/models.py b/generic/models.py
index 2182a030..49b6b20e 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -1946,6 +1946,92 @@ class ExamCollection(models.Model, AuthorMixin):
exam.add_markers(users)
#return True
+ def bulk_add_user_user_groups(self, groups, exam_types: None | list[str] = None) -> int:
+ """Bulk-add one or more UserUserGroup objects (or PKs) to all exams in this
+ collection.
+
+ Args:
+ groups: iterable of UserUserGroup instances or an iterable of PKs (ints),
+ or a Django QuerySet of UserUserGroup.
+ exam_types: optional list of exam type keys to limit the operation. Keys
+ are the same as returned by `get_exams()` (e.g. 'physics', 'anatomy').
+
+ Returns:
+ Number of exams that were updated (int).
+
+ Note: permission checks are not performed here; callers should ensure the
+ current user is allowed to modify these exams.
+ """
+ # import locally to avoid circular import surprises when module is imported
+ UserUserGroup = globals().get("UserUserGroup")
+ if UserUserGroup is None:
+ raise RuntimeError("UserUserGroup model not available")
+
+ # Normalize groups to a list of model instances
+ if groups is None:
+ return 0
+
+ # If groups is a queryset or list of model instances, materialize it
+ try:
+ # QuerySet-ish
+ groups_qs = list(groups)
+ except TypeError:
+ # single value provided
+ groups_qs = [groups]
+
+ # If provided values were ints (PKs), resolve them to model instances
+ if groups_qs and all(isinstance(g, int) for g in groups_qs):
+ groups_qs = list(UserUserGroup.objects.filter(pk__in=groups_qs))
+
+ if not groups_qs:
+ return 0
+
+ updated = 0
+ for exam_type, exams in self.get_exams().items():
+ if exam_types is not None and exam_type not in exam_types:
+ continue
+
+ for exam in exams:
+ # Each exam model exposes a `user_user_groups` ManyToManyField
+ exam.user_user_groups.add(*groups_qs)
+ updated += 1
+
+ return updated
+
+ def bulk_add_cid_user_groups(self, groups, exam_types: None | list[str] = None) -> int:
+ """Bulk-add one or more CidUserGroup objects (or PKs) to all exams in this
+ collection. Returns number of exams updated.
+ """
+ CidUserGroup = globals().get("CidUserGroup")
+ if CidUserGroup is None:
+ raise RuntimeError("CidUserGroup model not available")
+
+ if groups is None:
+ return 0
+
+ try:
+ groups_qs = list(groups)
+ except TypeError:
+ groups_qs = [groups]
+
+ if groups_qs and all(isinstance(g, int) for g in groups_qs):
+ groups_qs = list(CidUserGroup.objects.filter(pk__in=groups_qs))
+
+ if not groups_qs:
+ return 0
+
+ updated = 0
+ for exam_type, exams in self.get_exams().items():
+ if exam_types is not None and exam_type not in exam_types:
+ continue
+
+ for exam in exams:
+ # Each exam model exposes a `cid_user_groups` ManyToManyField
+ exam.cid_user_groups.add(*groups_qs)
+ updated += 1
+
+ return updated
+
#class Presentation(models.Model, AuthorMixin):
#
diff --git a/generic/templates/generic/examcollection_detail.html b/generic/templates/generic/examcollection_detail.html
index 306ab4bb..2f91165e 100755
--- a/generic/templates/generic/examcollection_detail.html
+++ b/generic/templates/generic/examcollection_detail.html
@@ -86,9 +86,13 @@
{% endif %}
-
Added user groups to {{ total_user }} exams.
+Added CID groups to {{ total_cid }} exams.
+ {% endif %} +