Enhance user group management by adding support for Django auth groups and improving group toggle functionality for superusers and user managers

This commit is contained in:
Ross
2025-11-03 12:25:29 +00:00
parent ff02e3f02f
commit 68dc7c05d8
3 changed files with 151 additions and 55 deletions
@@ -1,48 +1,102 @@
{% load i18n %} {% load i18n %}
<div id="user-groups-{{ user.pk }}"> <div id="user-groups-{{ user.pk }}">
<div class="d-flex flex-wrap gap-2 align-items-center"> <div class="d-flex flex-column gap-2">
{% if user.user_groups.all %} <div>
{% for group in user.user_groups.all %} <small class="text-muted">Project groups:</small>
{% if request.user.is_superuser %} <div class="d-flex flex-wrap gap-2 mt-1">
<button {% if user.user_groups.all %}
class="btn btn-sm btn-outline-secondary" {% for group in user.user_groups.all %}
hx-post="{% url 'generic:user_toggle_group' user.pk group.pk %}" {% if request.user.is_superuser or is_cid_user_manager %}
hx-target="#user-groups-{{ user.pk }}" <button
hx-swap="outerHTML" class="btn btn-sm btn-outline-secondary"
> hx-post="{% url 'generic:user_toggle_group' user.pk group.pk %}"
{{ group.name }} hx-target="#user-groups-{{ user.pk }}"
<span class="ms-1 text-muted small">&times;</span> hx-swap="outerHTML"
</button> >
{{ group.name }}
<span class="ms-1 text-muted small">&times;</span>
</button>
{% else %}
<a class="btn btn-sm btn-outline-secondary" href="{% url 'generic:user_group_detail' group.pk %}">{{ group.name }}</a>
{% endif %}
{% endfor %}
{% else %} {% else %}
<a class="btn btn-sm btn-outline-secondary" href="{% url 'generic:user_group_detail' group.pk %}">{{ group.name }}</a> <div class="text-muted">No groups.</div>
{% endif %} {% endif %}
{% endfor %}
{% else %}
<div class="text-muted">No groups.</div>
{% endif %}
{% if request.user.is_superuser %} {% if request.user.is_superuser or is_cid_user_manager %}
{% if available_groups.count %} {% if available_groups.count %}
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="addGroupDropdown" data-bs-toggle="dropdown" aria-expanded="false"> <button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="addGroupDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Add group Add group
</button> </button>
<ul class="dropdown-menu" aria-labelledby="addGroupDropdown"> <ul class="dropdown-menu" aria-labelledby="addGroupDropdown">
{% for ag in available_groups %} {% for ag in available_groups %}
<li> <li>
<button <button
class="dropdown-item" class="dropdown-item"
hx-post="{% url 'generic:user_toggle_group' user.pk ag.pk %}" hx-post="{% url 'generic:user_toggle_group' user.pk ag.pk %}"
hx-target="#user-groups-{{ user.pk }}" hx-target="#user-groups-{{ user.pk }}"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
{{ ag.name }} {{ ag.name }}
</button> </button>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div>
</div>
<div>
<small class="text-muted">Auth groups:</small>
<div class="d-flex flex-wrap gap-2 mt-1">
{% if user.groups.all %}
{% for group in user.groups.all %}
{% if request.user.is_superuser or is_cid_user_manager %}
<button
class="btn btn-sm btn-outline-secondary"
hx-post="{% url 'generic:user_toggle_group' user.pk group.pk %}?type=auth"
hx-target="#user-groups-{{ user.pk }}"
hx-swap="outerHTML"
>
{{ group.name }}
<span class="ms-1 text-muted small">&times;</span>
</button>
{% else %}
<span class="btn btn-sm btn-outline-secondary">{{ group.name }}</span>
{% endif %}
{% endfor %}
{% else %}
<span class="badge bg-light text-muted">No auth groups</span>
{% endif %}
{% if request.user.is_superuser or is_cid_user_manager %}
{% if available_auth_groups.count %}
<div class="dropdown">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="addAuthGroupDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Add auth group
</button>
<ul class="dropdown-menu" aria-labelledby="addAuthGroupDropdown">
{% for ag in available_auth_groups %}
<li>
<button
class="dropdown-item"
hx-post="{% url 'generic:user_toggle_group' user.pk ag.pk %}?type=auth"
hx-target="#user-groups-{{ user.pk }}"
hx-swap="outerHTML"
>
{{ ag.name }}
</button>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
</div>
</div>
</div> </div>
</div> </div>
+40 -12
View File
@@ -5,7 +5,7 @@ import statistics
import threading import threading
from typing import Any from typing import Any
from dal import autocomplete 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.contrib.contenttypes.models import ContentType
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.db.models.functions import Coalesce, Lower from django.db.models.functions import Coalesce, Lower
@@ -3879,34 +3879,62 @@ def user_group_view_all(request):
@login_required @login_required
def user_toggle_group(request, user_id, group_id): 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 Visible to superusers and users in the `cid_user_manager` group. The
rendered groups fragment for the user so the frontend can swap it in. 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": if request.method != "POST":
return HttpResponse("Method not allowed", status=405) 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) return HttpResponse("Forbidden", status=403)
user = get_object_or_404(User, pk=user_id) user = get_object_or_404(User, pk=user_id)
group = get_object_or_404(UserUserGroup, pk=group_id)
# Toggle membership gtype = request.GET.get("type", "custom")
if group.users.filter(pk=user.pk).exists():
group.users.remove(user) 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: 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( available_groups = UserUserGroup.objects.filter(archive=False).exclude(
pk__in=user.user_groups.values_list("pk", flat=True) 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( return render(
request, request,
"generic/partials/user_groups_fragment.html", "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,
},
) )
+16 -2
View File
@@ -80,8 +80,9 @@ from generic.models import (
Supervisor, Supervisor,
UserGrades, UserGrades,
UserProfile, UserProfile,
USER_EXAM_TYPES USER_EXAM_TYPES,
) )
from django.contrib.auth.models import Group
from generic.models import UserUserGroup from generic.models import UserUserGroup
from django_filters.views import FilterView from django_filters.views import FilterView
@@ -121,7 +122,20 @@ def profile(request):
available_groups = UserUserGroup.objects.filter(archive=False).exclude( available_groups = UserUserGroup.objects.filter(archive=False).exclude(
pk__in=user.user_groups.values_list("pk", flat=True) 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): def index(request):