feat(dgsi): Add a list of accounts and allow managin the assigned VLAN

This commit is contained in:
Tom Hubrecht 2025-02-02 11:08:15 +01:00
parent e43e42afed
commit b1d80b3837
Signed by: thubrecht
SSH key fingerprint: SHA256:r+nK/SIcWlJ0zFZJGHtlAoRwq1Rm+WcKAm5ADYMoQPc
7 changed files with 127 additions and 14 deletions

View file

@ -1,5 +1,7 @@
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import HttpRequest
from django.views.generic.base import ContextMixin, TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from dgsi.models import User
@ -18,3 +20,24 @@ class StaffRequiredMixin(UserPassesTestMixin):
def get_context_data(self, **kwargs):
# NOTE: We are only allowed to do this if a class is supplied to the right when constructing the view
return super().get_context_data(admin_view=True, **kwargs) # pyright: ignore
class HtmxPostMixin(TemplateResponseMixin, ContextMixin):
http_method_names = ["post"]
def execute_action(self, *args, **kwargs) -> None:
return None
def post(self, request, *args, **kwargs):
# Execute action
self.execute_action()
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
class HtmxPostObjectMixin(SingleObjectMixin, HtmxPostMixin):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)

View file

@ -235,7 +235,7 @@ class User(AbstractUser):
self.vlan_id = min(
set(range(settings.VLAN_ID_MIN, settings.VLAN_ID_MAX))
- set(User.objects.exclude(vlan_id__isnull=True).values_list("vlan_id"))
- set(*User.objects.exclude(vlan_id__isnull=True).values_list("vlan_id"))
)
# Preempt the vlan attribution
@ -270,7 +270,7 @@ class User(AbstractUser):
if group.member != [f"{self.username}@sso.dgnum.eu"]:
# Remove the user from the group
sync_call("group_delete_members", self.username)
sync_call("group_delete_members", group_name, [self.username])
self.vlan_id = None
self.save(update_fields=["vlan_id"])
raise RuntimeError("Duplicate VLAN attribution detected")

View file

@ -0,0 +1,19 @@
{% load i18n %}
<tr id="user-{{ person.pk }}">
<th>{{ person.username }}</th>
<td>{{ person.first_name }}&nbsp;{{ person.last_name }}</td>
<td>{{ person.email }}</td>
<td>{{ person.vlan_id|default:"" }}</td>
<td>
{% if person.vlan_id %}
<a hx-post="{% url "dgsi:dgn-user_deassign_vlan" person.pk %}"
hx-target="#user-{{ person.pk }}"
class="button is-fullwidth is-light is-warning">{% trans "Désallouer" %}</a>
{% else %}
<a hx-post="{% url "dgsi:dgn-user_assign_vlan" person.pk %}"
hx-target="#user-{{ person.pk }}"
class="button is-fullwidth is-light is-primary">{% trans "Allouer" %}</a>
{% endif %}
</td>
</tr>

View file

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% include "_subtitle.html" with subtitle="Comptes DG·SI" %}
<table class="table is-fullwidth is-striped">
<thead>
<tr>
<th>{% trans "Nom d'utilisateur" %}</th>
<th>{% trans "Nom d'usage" %}</th>
<th>{% trans "Adresse e-mail" %}</th>
<th>{% trans "VLAN attribué" %}</th>
<th>{% trans "Gestion du VLAN" %}</th>
</tr>
</thead>
<tbody>
{% for person in user_list %}
{% include "dgsi/partials/user_list-user.html" %}
{% endfor %}
</tbody>
</table>
{% endblock content %}

View file

@ -53,6 +53,21 @@ urlpatterns = [
views.CreateKanidmAccountView.as_view(),
name="dgn-create_kanidm_user",
),
path(
"accounts/list/",
views.UserListView.as_view(),
name="dgn-user_list",
),
path(
"accounts/assign-vlan/<int:pk>",
views.AssignVlanView.as_view(),
name="dgn-user_assign_vlan",
),
path(
"accounts/deassign-vlan/<int:pk>",
views.DeassignVlanView.as_view(),
name="dgn-user_deassign_vlan",
),
path(
"accounts/forbidden/",
views.TemplateView.as_view(template_name="accounts/forbidden_category.html"),

View file

@ -17,7 +17,7 @@ from django.views.generic import FormView, ListView, RedirectView, TemplateView,
from django.views.generic.detail import SingleObjectMixin
from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm
from dgsi.mixins import StaffRequiredMixin
from dgsi.mixins import HtmxPostObjectMixin, StaffRequiredMixin
from dgsi.models import Archive, Bylaws, Service, Statutes, User
from shared.kanidm import klient
@ -51,6 +51,7 @@ ADMIN_LINKS: list[Link] = [
_("Créer un compte Kanidm"),
"user-plus",
),
Link("is-primary", "dgsi:dgn-user_list", _("Liste des comptes"), "users"),
Link(
"is-warning", "admin:index", _("Interface d'administration"), "settings-filled"
),
@ -371,3 +372,25 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView)
).send()
return super().form_valid(form)
class UserListView(StaffRequiredMixin, ListView):
model = User
class AssignVlanView(StaffRequiredMixin, HtmxPostObjectMixin, View):
model = User
template_name = "dgsi/partials/user_list-user.html"
context_object_name = "person"
def execute_action(self, *args, **kwargs) -> None:
self.object.register_unique_vlan()
class DeassignVlanView(StaffRequiredMixin, HtmxPostObjectMixin, View):
model = User
template_name = "dgsi/partials/user_list-user.html"
context_object_name = "person"
def execute_action(self, *args, **kwargs) -> None:
self.object.reclaim_vlan()

View file

@ -1,15 +1,15 @@
document.addEventListener("DOMContentLoaded", () => {
(document.querySelectorAll(".notification .delete") || []).forEach(
($delete) => {
const $notification = $delete.parentNode;
const dismiss = () => $notification.parentNode.removeChild($notification);
const init = ($node) => {
const q = (query, f) => ($node.querySelectorAll(query) || []).forEach(f);
$delete.addEventListener("click", dismiss);
setTimeout(dismiss, 15000);
},
);
q(".notification .delete", ($delete) => {
const $notification = $delete.parentNode;
const dismiss = () => $notification.parentNode.removeChild($notification);
(document.querySelectorAll("[data-toggle]") || []).forEach(($toggle) => {
$delete.addEventListener("click", dismiss);
setTimeout(dismiss, 15000);
});
q("[data-toggle]", ($toggle) => {
const target = document.querySelector($toggle.dataset.target);
$toggle.addEventListener("click", () => {
@ -25,9 +25,17 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
(document.querySelectorAll("[data-select]") || []).forEach(($input) => {
q("[data-select]", ($input) => {
$input.addEventListener("click", () => {
$input.select();
});
});
};
document.addEventListener("DOMContentLoaded", () => {
init(document);
document.body.addEventListener("htmx:load", (e) => {
init(e.detail.elt);
});
});