From 772e8527637dd56e650ff8e59c356313c4945891 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 3 Nov 2025 12:10:58 +0000 Subject: [PATCH] Implement user group management with toggle functionality for superusers --- .../partials/user_groups_fragment.html | 48 +++++++++++++++++++ generic/urls.py | 5 ++ generic/views.py | 34 +++++++++++++ rad/views.py | 11 ++++- templates/profile.html | 12 +---- 5 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 generic/templates/generic/partials/user_groups_fragment.html diff --git a/generic/templates/generic/partials/user_groups_fragment.html b/generic/templates/generic/partials/user_groups_fragment.html new file mode 100644 index 00000000..7ea395d1 --- /dev/null +++ b/generic/templates/generic/partials/user_groups_fragment.html @@ -0,0 +1,48 @@ +{% load i18n %} +
+
+ {% if user.user_groups.all %} + {% for group in user.user_groups.all %} + {% if request.user.is_superuser %} + + {% else %} + {{ group.name }} + {% endif %} + {% endfor %} + {% else %} +
No groups.
+ {% endif %} + + {% if request.user.is_superuser %} + {% if available_groups.count %} + + {% endif %} + {% endif %} +
+
diff --git a/generic/urls.py b/generic/urls.py index af3aa893..d3258535 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -182,6 +182,11 @@ urlpatterns = [ views.UserUserGroupCreate.as_view(), name="user_group_create", ), + path( + "user//toggle_group//", + views.user_toggle_group, + name="user_toggle_group", + ), path("cids/create", views.manage_cid_users, name="manage_cid_users"), path("cids//details", views.cid_details, name="cid_details"), path("cids/create/email", views.create_cid_email, name="create_cid_email"), diff --git a/generic/views.py b/generic/views.py index 8efb4121..bb3e8849 100644 --- a/generic/views.py +++ b/generic/views.py @@ -49,6 +49,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.tables import ( CidUserExamTable, @@ -3876,6 +3877,39 @@ 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. + + This endpoint is superuser-only. It toggles membership and returns the + rendered groups fragment for the user so the frontend can swap it in. + """ + if request.method != "POST": + return HttpResponse("Method not allowed", status=405) + + if not request.user.is_superuser: + 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) + else: + group.users.add(user) + + # Return the updated groups fragment (outerHTML replacement) + available_groups = UserUserGroup.objects.filter(archive=False).exclude( + pk__in=user.user_groups.values_list("pk", flat=True) + ) + return render( + request, + "generic/partials/user_groups_fragment.html", + {"user": user, "available_groups": available_groups}, + ) + + @login_required @user_is_cid_user_manager def group_email_resend(request, pk): diff --git a/rad/views.py b/rad/views.py index 3004fe8c..c138e0ae 100644 --- a/rad/views.py +++ b/rad/views.py @@ -82,6 +82,7 @@ from generic.models import ( UserProfile, USER_EXAM_TYPES ) +from generic.models import UserUserGroup from django_filters.views import FilterView @@ -117,7 +118,10 @@ def people(request): @login_required def profile(request): user = request.user - return render(request, "profile.html", {"user": user}) + 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}) def index(request): @@ -141,7 +145,10 @@ def index(request): @user_is_cid_user_manager def account_profile(request, slug): user = get_object_or_404(User, username=slug) - return render(request, "profile.html", {"user": user}) + 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}) def cid_selector(request): diff --git a/templates/profile.html b/templates/profile.html index 2cabcc56..b62780ad 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -57,17 +57,7 @@
Groups
- {% if user.user_groups.all %} -
- {% for group in user.user_groups.all %} - {{ group }} - {% empty %} -
No groups.
- {% endfor %} -
- {% else %} -
No groups.
- {% endif %} + {% include 'generic/partials/user_groups_fragment.html' with user=user %}