Add a page to manage the groups that can create permissions

This commit is contained in:
imperosol 2025-02-12 15:32:04 +01:00
parent 2123e83010
commit 05d4a09f8c
9 changed files with 220 additions and 16 deletions

View File

@ -197,9 +197,9 @@
</table>
</div>
</div>
<hr>
{% endif %}
<hr>
<div>
{% if user.is_root or user.is_board_member %}
<form class="form-gifts" action="{{ url('core:user_gift_create', user_id=profile.id) }}" method="post">

View File

@ -30,11 +30,7 @@
{% if user.has_perm("core.view_userban") %}
<li><a href="{{ url("rootplace:ban_list") }}">{% trans %}Bans{% endtrans %}</a></li>
{% endif %}
{% if user.has_perm("subscription.add_subscription") %}
<li><a href="{{ url('subscription:subscription') }}">{% trans %}Subscriptions{% endtrans %}</a></li>
{% endif %}
{% if user.is_board_member or user.is_root %}
<li><a href="{{ url('subscription:stats') }}">{% trans %}Subscription stats{% endtrans %}</a></li>
<li><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></li>
{% endif %}
</ul>
@ -163,6 +159,35 @@
</div>
{% endif %}
{% if user.has_perm("subscription.add_subscription") or user.has_perm("auth.change_perm") or user.is_root or user.is_board_member %}
<div>
<h4>{% trans %}Subscriptions{% endtrans %}</h4>
<ul>
{% if user.has_perm("subscription.add_subscription") %}
<li>
<a href="{{ url("subscription:subscription") }}">
{% trans %}New subscription{% endtrans %}
</a>
</li>
{% endif %}
{% if user.has_perm("auth.change_permission") %}
<li>
<a href="{{ url("subscription:perms") }}">
{% trans %}Manage permissions{% endtrans %}
</a>
</li>
{% endif %}
{% if user.is_root or user.is_board_member %}
<li>
<a href="{{ url("subscription:stats") }}">
{% trans %}Subscription stats{% endtrans %}
</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{% if user.memberships.filter(end_date=None).all().count() > 0 %}
<div>
<h4>{% trans %}Club tools{% endtrans %}</h4>

View File

@ -28,6 +28,7 @@ from captcha.fields import CaptchaField
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth.models import Permission
from django.contrib.staticfiles.management.commands.collectstatic import (
staticfiles_storage,
)
@ -440,3 +441,25 @@ class GiftForm(forms.ModelForm):
id=user_id
)
self.fields["user"].widget = forms.HiddenInput()
class PermissionGroupsForm(forms.ModelForm):
"""Manage the groups that have a specific permission."""
class Meta:
model = Permission
fields = []
groups = forms.ModelMultipleChoiceField(
Group.objects.all(), widget=AutoCompleteSelectMultipleGroup, required=False
)
def __init__(self, instance: Permission, **kwargs):
super().__init__(instance=instance, **kwargs)
self.fields["groups"].initial = instance.group_set.all()
def save(self, commit: bool = True): # noqa FTB001
instance = super().save(commit=False)
if commit:
instance.group_set.set(self.cleaned_data["groups"])
return instance

View File

@ -17,6 +17,9 @@
from django import forms
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Permission
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView
@ -25,6 +28,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
from core.auth.mixins import CanEditMixin
from core.models import Group, User
from core.views import DetailFormView
from core.views.forms import PermissionGroupsForm
from core.views.widgets.select import AutoCompleteSelectMultipleUser
# Forms
@ -130,3 +134,60 @@ class GroupDeleteView(CanEditMixin, DeleteView):
pk_url_kwarg = "group_id"
template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("core:group_list")
class PermissionGroupsUpdateView(PermissionRequiredMixin, UpdateView):
"""Manage the groups that have a specific permission.
Notes:
This is an `UpdateView`, but unlike typical `UpdateView`,
it doesn't accept url arguments to retrieve the object
to update.
As such, a `PermissionGroupsUpdateView` can only deal with
a single hardcoded permission.
This is not a limitation, but an on-purpose design,
mainly for security matters.
Example:
```python
class AddSubscriptionGroupsView(PermissionGroupsUpdateView):
permission = "subscription.add_subscription"
success_url = reverse_lazy("foo:bar")
```
"""
permission_required = "auth.change_permission"
template_name = "core/edit.jinja"
form_class = PermissionGroupsForm
permission = None
def get_object(self, *args, **kwargs):
if not self.permission:
raise ImproperlyConfigured(
f"{self.__class__.__name__} is missing the permission attribute. "
"Please fill it with either a permission string "
"or a Permission object."
)
if isinstance(self.permission, Permission):
return self.permission
if isinstance(self.permission, str):
try:
app_label, codename = self.permission.split(".")
except ValueError as e:
raise ValueError(
"Permission name should be in the form "
"app_label.permission_codename."
) from e
return get_object_or_404(
Permission, codename=codename, content_type__app_label=app_label
)
raise TypeError(
f"{self.__class__.__name__}.permission "
f"must be a string or a permission instance."
)
def get_success_url(self):
# if children classes define a success url, return it,
# else stay on the same page
return self.success_url or self.request.path

View File

@ -228,3 +228,38 @@ Les groupes de ban existants sont les suivants :
- `Banned from buying alcohol` : les utilisateurs interdits de vente d'alcool (non mineurs)
- `Banned from counters` : les utilisateurs interdits d'utilisation des comptoirs
- `Banned to subscribe` : les utilisateurs interdits de cotisation
## Groupes liés à une permission
Certaines actions sur le site demandent une permission en particulier,
que l'on veut donner ou retirer n'importe quand.
Prenons par exemple les cotisations : lors de l'intégration,
on veut permettre aux membres du bureau de l'Integ
de créer des cotisations, et pareil pour les membres du bureau
de la Welcome Week pendant cette dernière.
Dans ces cas-là, il est pertinent de mettre à disposition
des administrateurs du site une page leur permettant
de gérer quels groupes ont une permission donnée.
Pour ce faire, il existe
[PermissionGroupsUpdateView][core.views.PermissionGroupsUpdateView].
Pour l'utiliser, il suffit de créer une vue qui en hérite
et de lui dire quelle est la permission dont on veut gérer
les groupes :
```python
from core.views.group import PermissionGroupsUpdateView
class SubscriptionPermissionView(PermissionGroupsUpdateView):
permission = "subscription.add_subscription"
```
Configurez l'url de la vue, et c'est tout !
La page ainsi générée contiendra un formulaire
avec un unique champ permettant de sélectionner des groupes.
Par défaut, seuls les utilisateurs avec la permission
`auth.change_permission` auront accès à ce formulaire
(donc, normalement, uniquement les utilisateurs Root).

View File

@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-19 18:12+0100\n"
"POT-Creation-Date: 2025-02-12 15:32+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@ -2914,7 +2914,7 @@ msgstr "Blouse"
msgid "Not subscribed"
msgstr "Non cotisant"
#: core/templates/core/user_detail.jinja
#: core/templates/core/user_detail.jinja core/templates/core/user_tools.jinja
#: subscription/templates/subscription/subscription.jinja
msgid "New subscription"
msgstr "Nouvelle cotisation"
@ -3146,15 +3146,6 @@ msgstr "Supprimer les messages forum d'un utilisateur"
msgid "Bans"
msgstr "Bans"
#: core/templates/core/user_tools.jinja
msgid "Subscriptions"
msgstr "Cotisations"
#: core/templates/core/user_tools.jinja
#: subscription/templates/subscription/stats.jinja
msgid "Subscription stats"
msgstr "Statistiques de cotisation"
#: core/templates/core/user_tools.jinja counter/forms.py
#: counter/views/mixins.py
msgid "Counters"
@ -3227,6 +3218,19 @@ msgstr "Modérer les fichiers"
msgid "Moderate pictures"
msgstr "Modérer les photos"
#: core/templates/core/user_tools.jinja
msgid "Subscriptions"
msgstr "Cotisations"
#: core/templates/core/user_tools.jinja
msgid "Manage permissions"
msgstr "Gérer les permissions"
#: core/templates/core/user_tools.jinja
#: subscription/templates/subscription/stats.jinja
msgid "Subscription stats"
msgstr "Statistiques de cotisation"
#: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja
msgid "Create UV"
msgstr "Créer UV"

View File

@ -0,0 +1,43 @@
from django.contrib.auth.models import Permission
from django.test import TestCase
from django.urls import reverse
from model_bakery import baker
from pytest_django.asserts import assertRedirects
from club.models import Club, Membership
from core.baker_recipes import subscriber_user
from core.models import User
class TestSubscriptionPermission(TestCase):
@classmethod
def setUpTestData(cls):
cls.user: User = subscriber_user.make()
cls.admin = baker.make(User, is_superuser=True)
cls.club = baker.make(Club)
baker.make(Membership, user=cls.user, club=cls.club, role=7)
def test_give_permission(self):
self.client.force_login(self.admin)
response = self.client.post(
reverse("subscription:perms"), {"groups": [self.club.board_group_id]}
)
assertRedirects(response, reverse("subscription:perms"))
assert self.user.has_perm("subscription.add_subscription")
def test_remove_permission(self):
self.client.force_login(self.admin)
response = self.client.post(reverse("subscription:perms"), {"groups": []})
assertRedirects(response, reverse("subscription:perms"))
assert not self.user.has_perm("subscription.add_subscription")
def test_subscription_page_access(self):
self.client.force_login(self.user)
response = self.client.get(reverse("subscription:subscription"))
assert response.status_code == 403
self.club.board_group.permissions.add(
Permission.objects.get(codename="add_subscription")
)
response = self.client.get(reverse("subscription:subscription"))
assert response.status_code == 200

View File

@ -20,6 +20,7 @@ from subscription.views import (
CreateSubscriptionNewUserFragment,
NewSubscription,
SubscriptionCreatedFragment,
SubscriptionPermissionView,
SubscriptionsStatsView,
)
@ -41,5 +42,10 @@ urlpatterns = [
SubscriptionCreatedFragment.as_view(),
name="creation-success",
),
path(
"perms/",
SubscriptionPermissionView.as_view(),
name="perms",
),
path("stats/", SubscriptionsStatsView.as_view(), name="stats"),
]

View File

@ -21,6 +21,7 @@ from django.utils.timezone import localdate
from django.views.generic import CreateView, DetailView, TemplateView
from django.views.generic.edit import FormView
from core.views.group import PermissionGroupsUpdateView
from counter.apps import PAYMENT_METHOD
from subscription.forms import (
SelectionDateForm,
@ -77,6 +78,12 @@ class SubscriptionCreatedFragment(PermissionRequiredMixin, DetailView):
context_object_name = "subscription"
class SubscriptionPermissionView(PermissionGroupsUpdateView):
"""Manage the groups that have access to the subscription creation page."""
permission = "subscription.add_subscription"
class SubscriptionsStatsView(FormView):
template_name = "subscription/stats.jinja"
form_class = SelectionDateForm