From 68dc7c05d8e5f0ee4a305b59b5f1b9a2d74fcf6a Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 3 Nov 2025 12:25:29 +0000 Subject: [PATCH] Enhance user group management by adding support for Django auth groups and improving group toggle functionality for superusers and user managers --- .../partials/user_groups_fragment.html | 136 ++++++++++++------ generic/views.py | 52 +++++-- rad/views.py | 18 ++- 3 files changed, 151 insertions(+), 55 deletions(-) diff --git a/generic/templates/generic/partials/user_groups_fragment.html b/generic/templates/generic/partials/user_groups_fragment.html index 7ea395d1..318f2692 100644 --- a/generic/templates/generic/partials/user_groups_fragment.html +++ b/generic/templates/generic/partials/user_groups_fragment.html @@ -1,48 +1,102 @@ {% load i18n %}
-
- {% if user.user_groups.all %} - {% for group in user.user_groups.all %} - {% if request.user.is_superuser %} - +
+
+ Project groups: +
+ {% if user.user_groups.all %} + {% for group in user.user_groups.all %} + {% if request.user.is_superuser or is_cid_user_manager %} + + {% else %} + {{ group.name }} + {% endif %} + {% endfor %} {% else %} - {{ group.name }} +
No groups.
{% endif %} - {% endfor %} - {% else %} -
No groups.
- {% endif %} - {% if request.user.is_superuser %} - {% if available_groups.count %} - - {% endif %} - {% endif %} + {% if request.user.is_superuser or is_cid_user_manager %} + {% if available_groups.count %} + + {% endif %} + {% endif %} +
+
+ +
+ Auth groups: +
+ {% if user.groups.all %} + {% for group in user.groups.all %} + {% if request.user.is_superuser or is_cid_user_manager %} + + {% else %} + {{ group.name }} + {% endif %} + {% endfor %} + {% else %} + No auth groups + {% endif %} + + {% if request.user.is_superuser or is_cid_user_manager %} + {% if available_auth_groups.count %} + + {% endif %} + {% endif %} +
+
diff --git a/generic/views.py b/generic/views.py index bb3e8849..e3f553f5 100644 --- a/generic/views.py +++ b/generic/views.py @@ -5,7 +5,7 @@ import statistics import threading from typing import Any from dal import autocomplete -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.contrib.contenttypes.models import ContentType from django.forms.models import model_to_dict from django.db.models.functions import Coalesce, Lower @@ -3879,34 +3879,62 @@ def user_group_view_all(request): @login_required def user_toggle_group(request, user_id, group_id): - """Toggle membership of a user in a UserUserGroup. + """Toggle membership of a user in either a UserUserGroup or an auth Group. - This endpoint is superuser-only. It toggles membership and returns the - rendered groups fragment for the user so the frontend can swap it in. + Visible to superusers and users in the `cid_user_manager` group. The + caller may pass a querystring `?type=auth` to toggle a Django auth Group; by + default it toggles the project's UserUserGroup. + + Returns the rendered groups fragment for HTMX swaps. """ if request.method != "POST": return HttpResponse("Method not allowed", status=405) - if not request.user.is_superuser: + # Allow superusers or members of the cid_user_manager group + if not ( + request.user.is_superuser + or request.user.groups.filter(name="cid_user_manager").exists() + ): return HttpResponse("Forbidden", status=403) user = get_object_or_404(User, pk=user_id) - group = get_object_or_404(UserUserGroup, pk=group_id) - # Toggle membership - if group.users.filter(pk=user.pk).exists(): - group.users.remove(user) + gtype = request.GET.get("type", "custom") + + if gtype == "auth": + # Toggle Django auth Group membership + auth_group = get_object_or_404(Group, pk=group_id) + if auth_group.user_set.filter(pk=user.pk).exists(): + auth_group.user_set.remove(user) + else: + auth_group.user_set.add(user) else: - group.users.add(user) + # Toggle project UserUserGroup membership + group = get_object_or_404(UserUserGroup, pk=group_id) + if group.users.filter(pk=user.pk).exists(): + group.users.remove(user) + else: + group.users.add(user) - # Return the updated groups fragment (outerHTML replacement) + # Prepare available lists for the fragment available_groups = UserUserGroup.objects.filter(archive=False).exclude( pk__in=user.user_groups.values_list("pk", flat=True) ) + available_auth_groups = Group.objects.exclude( + pk__in=user.groups.values_list("pk", flat=True) + ) + + is_cid_user_manager = request.user.groups.filter(name="cid_user_manager").exists() + return render( request, "generic/partials/user_groups_fragment.html", - {"user": user, "available_groups": available_groups}, + { + "user": user, + "available_groups": available_groups, + "available_auth_groups": available_auth_groups, + "is_cid_user_manager": is_cid_user_manager, + }, ) diff --git a/rad/views.py b/rad/views.py index c138e0ae..c43b44f5 100644 --- a/rad/views.py +++ b/rad/views.py @@ -80,8 +80,9 @@ from generic.models import ( Supervisor, UserGrades, UserProfile, - USER_EXAM_TYPES + USER_EXAM_TYPES, ) +from django.contrib.auth.models import Group from generic.models import UserUserGroup from django_filters.views import FilterView @@ -121,7 +122,20 @@ def profile(request): available_groups = UserUserGroup.objects.filter(archive=False).exclude( pk__in=user.user_groups.values_list("pk", flat=True) ) - return render(request, "profile.html", {"user": user, "available_groups": available_groups}) + available_auth_groups = Group.objects.exclude( + pk__in=user.groups.values_list("pk", flat=True) + ) + is_cid_user_manager = request.user.groups.filter(name="cid_user_manager").exists() + return render( + request, + "profile.html", + { + "user": user, + "available_groups": available_groups, + "available_auth_groups": available_auth_groups, + "is_cid_user_manager": is_cid_user_manager, + }, + ) def index(request):