Merge pull request #1017 from ae-utbm/subscription-perms

Subscription perms
This commit is contained in:
thomas girod 2025-02-15 12:18:40 +01:00 committed by GitHub
commit 3df33261ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 468 additions and 222 deletions

View File

@ -125,6 +125,11 @@ class Command(BaseCommand):
unix_name=settings.SITH_MAIN_CLUB["unix_name"], unix_name=settings.SITH_MAIN_CLUB["unix_name"],
address=settings.SITH_MAIN_CLUB["address"], address=settings.SITH_MAIN_CLUB["address"],
) )
main_club.board_group.permissions.add(
*Permission.objects.filter(
codename__in=["view_subscription", "add_subscription"]
)
)
bar_club = Club.objects.create( bar_club = Club.objects.create(
id=2, id=2,
name=settings.SITH_BAR_MANAGER["name"], name=settings.SITH_BAR_MANAGER["name"],

View File

@ -417,29 +417,6 @@ class User(AbstractUser):
def is_board_member(self) -> bool: def is_board_member(self) -> bool:
return self.groups.filter(club_board=settings.SITH_MAIN_CLUB_ID).exists() return self.groups.filter(club_board=settings.SITH_MAIN_CLUB_ID).exists()
@cached_property
def can_read_subscription_history(self) -> bool:
if self.is_root or self.is_board_member:
return True
from club.models import Club
for club in Club.objects.filter(
id__in=settings.SITH_CAN_READ_SUBSCRIPTION_HISTORY
):
if club in self.clubs_with_rights:
return True
return False
@cached_property
def can_create_subscription(self) -> bool:
return self.is_root or (
self.memberships.board()
.ongoing()
.filter(club_id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS)
.exists()
)
@cached_property @cached_property
def is_launderette_manager(self): def is_launderette_manager(self):
from club.models import Club from club.models import Club
@ -679,14 +656,6 @@ class AnonymousUser(AuthAnonymousUser):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@property
def can_create_subscription(self):
return False
@property
def can_read_subscription_history(self):
return False
@property @property
def was_subscribed(self): def was_subscribed(self):
return False return False

View File

@ -1,19 +1,40 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{# if the template context has the `object_name` variable,
then this one will be used in the page title,
instead of the result of `str(object)` #}
{% if object and not object_name %}
{% set object_name=object %}
{% endif %}
{% block title %} {% block title %}
{% if object %} {% if object_name %}
{% trans obj=object %}Edit {{ obj }}{% endtrans %} {% trans name=object_name %}Edit {{ name }}{% endtrans %}
{% else %} {% else %}
{% trans %}Save{% endtrans %} {% trans %}Save{% endtrans %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if object %} {% if object_name %}
<h2>{% trans obj=object %}Edit {{ obj }}{% endtrans %}</h2> <h2>{% trans name=object_name %}Edit {{ name }}{% endtrans %}</h2>
{% else %} {% else %}
<h2>{% trans %}Save{% endtrans %}</h2> <h2>{% trans %}Save{% endtrans %}</h2>
{% endif %} {% endif %}
{% if messages %}
<div x-data="{show_alert: true}" class="alert alert-green" x-show="show_alert" x-transition>
<span class="alert-main">
{% for message in messages %}
{% if message.level_tag == "success" %}
{{ message }}
{% endif %}
{% endfor %}
</span>
<span class="clickable" @click="show_alert = false">
<i class="fa fa-close"></i>
</span>
</div>
{% endif %}
<form action="" method="post" enctype="multipart/form-data"> <form action="" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_p() }} {{ form.as_p() }}

View File

@ -166,7 +166,7 @@
</div> </div>
{% endif %} {% endif %}
<br> <br>
{% if profile.was_subscribed and (user == profile or user.can_read_subscription_history)%} {% if profile.was_subscribed and (user == profile or user.has_perm("subscription.view_subscription")) %}
<div class="collapse" :class="{'shadow': collapsed}" x-data="{collapsed: false}" x-cloak> <div class="collapse" :class="{'shadow': collapsed}" x-data="{collapsed: false}" x-cloak>
<div class="collapse-header clickable" @click="collapsed = !collapsed"> <div class="collapse-header clickable" @click="collapsed = !collapsed">
<span class="collapse-header-text"> <span class="collapse-header-text">
@ -197,9 +197,9 @@
</table> </table>
</div> </div>
</div> </div>
<hr>
{% endif %} {% endif %}
<hr>
<div> <div>
{% if user.is_root or user.is_board_member %} {% 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"> <form class="form-gifts" action="{{ url('core:user_gift_create', user_id=profile.id) }}" method="post">

View File

@ -13,7 +13,7 @@
<h3>{% trans %}User Tools{% endtrans %}</h3> <h3>{% trans %}User Tools{% endtrans %}</h3>
<div class="container"> <div class="container">
{% if user.can_create_subscription or user.is_root or user.is_board_member %} {% if user.has_perm("subscription.view_userban") or user.is_root or user.is_board_member %}
<div> <div>
<h4>{% trans %}Sith management{% endtrans %}</h4> <h4>{% trans %}Sith management{% endtrans %}</h4>
<ul> <ul>
@ -21,16 +21,16 @@
<li><a href="{{ url('core:group_list') }}">{% trans %}Groups{% endtrans %}</a></li> <li><a href="{{ url('core:group_list') }}">{% trans %}Groups{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:merge') }}">{% trans %}Merge users{% endtrans %}</a></li> <li><a href="{{ url('rootplace:merge') }}">{% trans %}Merge users{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li> <li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li> <li>
<a href="{{ url('rootplace:delete_forum_messages') }}">
{% trans %}Delete user's forum messages{% endtrans %}
</a>
</li>
{% endif %} {% endif %}
{% if user.has_perm("core.view_userban") %} {% if user.has_perm("core.view_userban") %}
<li><a href="{{ url("rootplace:ban_list") }}">{% trans %}Bans{% endtrans %}</a></li> <li><a href="{{ url("rootplace:ban_list") }}">{% trans %}Bans{% endtrans %}</a></li>
{% endif %} {% endif %}
{% if user.can_create_subscription or user.is_root %}
<li><a href="{{ url('subscription:subscription') }}">{% trans %}Subscriptions{% endtrans %}</a></li>
{% endif %}
{% if user.is_board_member or user.is_root %} {% 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> <li><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></li>
{% endif %} {% endif %}
</ul> </ul>
@ -42,152 +42,202 @@
{% set is_admin_on_a_counter = true %} {% set is_admin_on_a_counter = true %}
{% endfor %} {% endfor %}
{% if {% if is_admin_on_a_counter or user.is_root or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) %}
is_admin_on_a_counter <div>
or user.is_root <h4>{% trans %}Counters{% endtrans %}</h4>
or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) <ul>
%} {% if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) %}
<li>
<a href="{{ url('counter:admin_list') }}">
{% trans %}General counters management{% endtrans %}
</a>
</li>
<li>
<a href="{{ url('counter:product_list') }}">
{% trans %}Products management{% endtrans %}
</a>
</li>
<li>
<a href="{{ url('counter:product_type_list') }}">
{% trans %}Product types management{% endtrans %}
</a>
</li>
<li>
<a href="{{ url('counter:cash_summary_list') }}">
{% trans %}Cash register summaries{% endtrans %}
</a>
</li>
<li>
<a href="{{ url('counter:invoices_call') }}">
{% trans %}Invoices call{% endtrans %}
</a>
</li>
<li>
<a href="{{ url('counter:eticket_list') }}">
{% trans %}Etickets{% endtrans %}
</a>
</li>
{% endif %}
</ul>
<ul>
{% for b in settings.SITH_COUNTER_BARS %}
{% if user.is_in_group(name=b[1]+" admin") %}
{% set c = Counter.objects.filter(id=b[0]).first() %}
<li class="rows counter">
<a href="{{ url('counter:details', counter_id=b[0]) }}">{{ b[1] }}</a>
<span>
<span>
<a class="button" href="{{ url('counter:admin', counter_id=b[0]) }}">
{% trans %}Edit{% endtrans %}
</a>
<a class="button" href="{{ url('counter:stats', counter_id=b[0]) }}">
{% trans %}Stats{% endtrans %}
</a>
</span>
</span>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{% if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<div>
<h4>{% trans %}Accounting{% endtrans %}</h4>
<ul>
{% if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<li><a href="{{ url('accounting:refound_account') }}">{% trans %}Refound Account{% endtrans %}</a></li>
<li><a href="{{ url('accounting:bank_list') }}">{% trans %}General accounting{% endtrans %}</a></li>
<li><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></li>
{% endif %}
{% for m in user.memberships.filter(end_date=None).filter(role__gte=7).all() -%}
{%- for b in m.club.bank_accounts.all() %}
<li class="rows">
<strong>{% trans %}Bank account: {% endtrans %}</strong>
<a href="{{ url('accounting:bank_details', b_account_id=b.id) }}">{{ b }}</a>
</li>
{%- endfor %}
{% if m.club.club_account.exists() -%}
{% for ca in m.club.club_account.all() %}
<li class="rows">
<strong>{% trans %}Club account: {% endtrans %}</strong>
<a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca }}</a>
</li>
{%- endfor %}
{%- endif -%}
{%- endfor %}
</ul>
</div>
{% endif %}
{% if user.is_root or user.is_com_admin or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
<div>
<h4>{% trans %}Communication{% endtrans %}</h4>
<ul>
{% if user.is_com_admin or user.is_root %}
<li><a href="{{ url('com:weekmail_article') }}">{% trans %}Create weekmail article{% endtrans %}</a></li>
<li><a href="{{ url('com:weekmail') }}">{% trans %}Weekmail{% endtrans %}</a></li>
<li><a href="{{ url('com:weekmail_destinations') }}">{% trans %}Weekmail destinations{% endtrans %}</a></li>
<li><a href="{{ url('com:news_new') }}">{% trans %}Create news{% endtrans %}</a></li>
<li><a href="{{ url('com:news_admin_list') }}">{% trans %}Moderate news{% endtrans %}</a></li>
<li><a href="{{ url('com:alert_edit') }}">{% trans %}Edit alert message{% endtrans %}</a></li>
<li><a href="{{ url('com:info_edit') }}">{% trans %}Edit information message{% endtrans %}</a></li>
<li><a href="{{ url('core:file_moderation') }}">{% trans %}Moderate files{% endtrans %}</a></li>
<li><a href="{{ url('com:mailing_admin') }}">{% trans %}Mailing lists administration{% endtrans %}</a></li>
<li><a href="{{ url('com:poster_list') }}">{% trans %}Posters{% endtrans %}</a></li>
<li><a href="{{ url('com:screen_list') }}">{% trans %}Screens{% endtrans %}</a></li>
{% endif %}
{% if user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
<li><a href="{{ url('sas:moderation') }}">{% trans %}Moderate pictures{% endtrans %}</a></li>
{% endif %}
</ul>
</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>
<ul>
{% for m in user.memberships.filter(end_date=None).all() %}
<li><a href="{{ url('club:tools', club_id=m.club.id) }}">{{ m.club }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if user.has_perm("pedagogy.add_uv") or user.has_perm("pedagogy.delete_uvcomment") %}
<div>
<h4>{% trans %}Pedagogy{% endtrans %}</h4>
<ul>
{% if user.has_perm("pedagogy.add_uv") %}
<li>
<a href="{{ url("pedagogy:uv_create") }}">
{% trans %}Create UV{% endtrans %}
</a>
</li>
{% endif %}
{% if user.has_perm("pedagogy.delete_uvcomment") %}
<li>
<a href="{{ url("pedagogy:moderation") }}">
{% trans %}Moderate comments{% endtrans %}
</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
<div> <div>
<h4>{% trans %}Counters{% endtrans %}</h4> <h4>{% trans %}Elections{% endtrans %}</h4>
<ul> <ul>
{% if user.is_root <li><a href="{{ url('election:list') }}">{% trans %}See available elections{% endtrans %}</a></li>
or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) <li><a href="{{ url('election:list_archived') }}">{% trans %}See archived elections{% endtrans %}</a></li>
%} {%- if user.is_subscribed -%}
<li><a href="{{ url('counter:admin_list') }}">{% trans %}General counters management{% endtrans %}</a></li> <li><a href="{{ url('election:create') }}">{% trans %}Create a new election{% endtrans %}</a></li>
<li><a href="{{ url('counter:product_list') }}">{% trans %}Products management{% endtrans %}</a></li> {%- endif -%}
<li><a href="{{ url('counter:product_type_list') }}">{% trans %}Product types management{% endtrans %}</a></li> </ul>
<li><a href="{{ url('counter:cash_summary_list') }}">{% trans %}Cash register summaries{% endtrans %}</a></li> </div>
<li><a href="{{ url('counter:invoices_call') }}">{% trans %}Invoices call{% endtrans %}</a></li>
<li><a href="{{ url('counter:eticket_list') }}">{% trans %}Etickets{% endtrans %}</a></li>
{% endif %}
</ul>
<ul>
{% for b in settings.SITH_COUNTER_BARS %}
{% if user.is_in_group(name=b[1]+" admin") %}
{% set c = Counter.objects.filter(id=b[0]).first() %}
<li class="rows counter"> <div>
<a href="{{ url('counter:details', counter_id=b[0]) }}">{{ b[1] }}</a> <h4>{% trans %}Other tools{% endtrans %}</h4>
<ul>
<span> <li><a href="{{ url('trombi:user_tools') }}">{% trans %}Trombi tools{% endtrans %}</a></li>
<span> </ul>
<a class="button" href="{{ url('counter:admin', counter_id=b[0]) }}">{% trans %}Edit{% endtrans %}</a> </div>
<a class="button" href="{{ url('counter:stats', counter_id=b[0]) }}">{% trans %}Stats{% endtrans %}</a> </div>
</span> </main>
</span>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{% if
user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or user.memberships.ongoing().filter(role__gte=7).count() > 10
%}
<div>
<h4>{% trans %}Accounting{% endtrans %}</h4>
<ul>
{% if user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
%}
<li><a href="{{ url('accounting:refound_account') }}">{% trans %}Refound Account{% endtrans %}</a></li>
<li><a href="{{ url('accounting:bank_list') }}">{% trans %}General accounting{% endtrans %}</a></li>
<li><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></li>
{% endif %}
{% for m in user.memberships.filter(end_date=None).filter(role__gte=7).all() -%}
{%- for b in m.club.bank_accounts.all() %}
<li class="rows">
<strong>{% trans %}Bank account: {% endtrans %}</strong>
<a href="{{ url('accounting:bank_details', b_account_id=b.id) }}">{{ b }}</a>
</li>
{%- endfor %}
{% if m.club.club_account.exists() -%}
{% for ca in m.club.club_account.all() %}
<li class="rows">
<strong>{% trans %}Club account: {% endtrans %}</strong>
<a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca }}</a>
</li>
{%- endfor %}
{%- endif -%}
{%- endfor %}
</ul>
</div>
{% endif %}
{% if
user.is_root
or user.is_com_admin
or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
%}
<div>
<h4>{% trans %}Communication{% endtrans %}</h4>
<ul>
{% if user.is_com_admin or user.is_root %}
<li><a href="{{ url('com:weekmail_article') }}">{% trans %}Create weekmail article{% endtrans %}</a></li>
<li><a href="{{ url('com:weekmail') }}">{% trans %}Weekmail{% endtrans %}</a></li>
<li><a href="{{ url('com:weekmail_destinations') }}">{% trans %}Weekmail destinations{% endtrans %}</a></li>
<li><a href="{{ url('com:news_new') }}">{% trans %}Create news{% endtrans %}</a></li>
<li><a href="{{ url('com:news_admin_list') }}">{% trans %}Moderate news{% endtrans %}</a></li>
<li><a href="{{ url('com:alert_edit') }}">{% trans %}Edit alert message{% endtrans %}</a></li>
<li><a href="{{ url('com:info_edit') }}">{% trans %}Edit information message{% endtrans %}</a></li>
<li><a href="{{ url('core:file_moderation') }}">{% trans %}Moderate files{% endtrans %}</a></li>
<li><a href="{{ url('com:mailing_admin') }}">{% trans %}Mailing lists administration{% endtrans %}</a></li>
<li><a href="{{ url('com:poster_list') }}">{% trans %}Posters{% endtrans %}</a></li>
<li><a href="{{ url('com:screen_list') }}">{% trans %}Screens{% endtrans %}</a></li>
{% endif %}
{% if user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
<li><a href="{{ url('sas:moderation') }}">{% trans %}Moderate pictures{% endtrans %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% if user.memberships.filter(end_date=None).all().count() > 0 %}
<div>
<h4>{% trans %}Club tools{% endtrans %}</h4>
<ul>
{% for m in user.memberships.filter(end_date=None).all() %}
<li><a href="{{ url('club:tools', club_id=m.club.id) }}">{{ m.club }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if
user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
%}
<div>
<h4>{% trans %}Pedagogy{% endtrans %}</h4>
<ul>
<li><a href="{{ url('pedagogy:uv_create') }}">{% trans %}Create UV{% endtrans %}</a></li>
<li><a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a></li>
</ul>
</div>
{% endif %}
<div>
<h4>{% trans %}Elections{% endtrans %}</h4>
<ul>
<li><a href="{{ url('election:list') }}">{% trans %}See available elections{% endtrans %}</a></li>
<li><a href="{{ url('election:list_archived') }}">{% trans %}See archived elections{% endtrans %}</a></li>
{%- if user.is_subscribed -%}
<li><a href="{{ url('election:create') }}">{% trans %}Create a new election{% endtrans %}</a></li>
{%- endif -%}
</ul>
</div>
<div>
<h4>{% trans %}Other tools{% endtrans %}</h4>
<ul>
<li><a href="{{ url('trombi:user_tools') }}">{% trans %}Trombi tools{% endtrans %}</a></li>
</ul>
</div>
</div>
</main>
{% endblock %} {% endblock %}

View File

@ -28,6 +28,7 @@ from captcha.fields import CaptchaField
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth.models import Permission
from django.contrib.staticfiles.management.commands.collectstatic import ( from django.contrib.staticfiles.management.commands.collectstatic import (
staticfiles_storage, staticfiles_storage,
) )
@ -440,3 +441,28 @@ class GiftForm(forms.ModelForm):
id=user_id id=user_id
) )
self.fields["user"].widget = forms.HiddenInput() 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(),
label=_("Groups"),
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,10 @@
from django import forms from django import forms
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Permission
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView from django.views.generic import ListView
@ -25,6 +29,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
from core.auth.mixins import CanEditMixin from core.auth.mixins import CanEditMixin
from core.models import Group, User from core.models import Group, User
from core.views import DetailFormView from core.views import DetailFormView
from core.views.forms import PermissionGroupsForm
from core.views.widgets.select import AutoCompleteSelectMultipleUser from core.views.widgets.select import AutoCompleteSelectMultipleUser
# Forms # Forms
@ -130,3 +135,62 @@ class GroupDeleteView(CanEditMixin, DeleteView):
pk_url_kwarg = "group_id" pk_url_kwarg = "group_id"
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("core:group_list") success_url = reverse_lazy("core:group_list")
class PermissionGroupsUpdateView(
PermissionRequiredMixin, SuccessMessageMixin, 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 SubscriptionPermissionView(PermissionGroupsUpdateView):
permission = "subscription.add_subscription"
```
"""
permission_required = "auth.change_permission"
template_name = "core/edit.jinja"
form_class = PermissionGroupsForm
permission = None
success_message = _("Groups have been successfully updated.")
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 buying alcohol` : les utilisateurs interdits de vente d'alcool (non mineurs)
- `Banned from counters` : les utilisateurs interdits d'utilisation des comptoirs - `Banned from counters` : les utilisateurs interdits d'utilisation des comptoirs
- `Banned to subscribe` : les utilisateurs interdits de cotisation - `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 "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-19 18:12+0100\n" "POT-Creation-Date: 2025-02-12 15:55+0100\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@ -2383,11 +2383,10 @@ msgstr "Confirmation"
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
#: core/templates/core/edit.jinja core/templates/core/file_edit.jinja #: core/templates/core/edit.jinja
#: counter/templates/counter/cash_register_summary.jinja
#, python-format #, python-format
msgid "Edit %(obj)s" msgid "Edit %(name)s"
msgstr "Éditer %(obj)s" msgstr "Éditer %(name)s"
#: core/templates/core/file.jinja core/templates/core/file_list.jinja #: core/templates/core/file.jinja core/templates/core/file_list.jinja
msgid "File list" msgid "File list"
@ -2457,6 +2456,12 @@ msgstr "octets"
msgid "Download" msgid "Download"
msgstr "Télécharger" msgstr "Télécharger"
#: core/templates/core/file_edit.jinja
#: counter/templates/counter/cash_register_summary.jinja
#, python-format
msgid "Edit %(obj)s"
msgstr "Éditer %(obj)s"
#: core/templates/core/file_list.jinja #: core/templates/core/file_list.jinja
msgid "There is no file in this website." msgid "There is no file in this website."
msgstr "Il n'y a pas de fichier sur ce site web." msgstr "Il n'y a pas de fichier sur ce site web."
@ -2914,7 +2919,7 @@ msgstr "Blouse"
msgid "Not subscribed" msgid "Not subscribed"
msgstr "Non cotisant" 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 #: subscription/templates/subscription/subscription.jinja
msgid "New subscription" msgid "New subscription"
msgstr "Nouvelle cotisation" msgstr "Nouvelle cotisation"
@ -3146,15 +3151,6 @@ msgstr "Supprimer les messages forum d'un utilisateur"
msgid "Bans" msgid "Bans"
msgstr "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 #: core/templates/core/user_tools.jinja counter/forms.py
#: counter/views/mixins.py #: counter/views/mixins.py
msgid "Counters" msgid "Counters"
@ -3227,6 +3223,19 @@ msgstr "Modérer les fichiers"
msgid "Moderate pictures" msgid "Moderate pictures"
msgstr "Modérer les photos" 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 #: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja
msgid "Create UV" msgid "Create UV"
msgstr "Créer UV" msgstr "Créer UV"
@ -3355,6 +3364,10 @@ msgstr "Utilisateurs à ajouter au groupe"
msgid "Users to remove from group" msgid "Users to remove from group"
msgstr "Utilisateurs à retirer du groupe" msgstr "Utilisateurs à retirer du groupe"
#: core/views/group.py
msgid "Groups have been successfully updated."
msgstr "Les groupes ont été mis à jour avec succès."
#: core/views/user.py #: core/views/user.py
msgid "We couldn't verify that this email actually exists" msgid "We couldn't verify that this email actually exists"
msgstr "Nous n'avons pas réussi à vérifier que cette adresse mail existe." msgstr "Nous n'avons pas réussi à vérifier que cette adresse mail existe."
@ -5676,6 +5689,10 @@ msgstr "Cotisations par type"
msgid "Existing member" msgid "Existing member"
msgstr "Membre existant" msgstr "Membre existant"
#: subscription/views.py
msgid "the groups that can create subscriptions"
msgstr "les groupes pouvant créer des cotisations"
#: trombi/models.py #: trombi/models.py
msgid "subscription deadline" msgid "subscription deadline"
msgstr "fin des inscriptions" msgstr "fin des inscriptions"

View File

@ -517,14 +517,6 @@ SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER = 1
SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = 2 SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = 2
SITH_PRODUCTTYPE_SUBSCRIPTION = 2 SITH_PRODUCTTYPE_SUBSCRIPTION = 2
# Defines which club lets its member the ability to make subscriptions
# Elements of this list are club's id
SITH_CAN_CREATE_SUBSCRIPTIONS = [1]
# Defines which clubs lets its members the ability to see users subscription history
# Elements of this list are club's id
SITH_CAN_READ_SUBSCRIPTION_HISTORY = []
# Number of weeks before the end of a subscription when the subscriber can resubscribe # Number of weeks before the end of a subscription when the subscriber can resubscribe
SITH_SUBSCRIPTION_END = 10 SITH_SUBSCRIPTION_END = 10

View File

@ -5,6 +5,7 @@ from typing import Callable
import pytest import pytest
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.contrib.auth.models import Permission
from django.test import Client from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import localdate from django.utils.timezone import localdate
@ -108,7 +109,12 @@ def test_page_access(
@pytest.mark.django_db @pytest.mark.django_db
def test_submit_form_existing_user(client: Client, settings: SettingsWrapper): def test_submit_form_existing_user(client: Client, settings: SettingsWrapper):
client.force_login(board_user.make()) client.force_login(
baker.make(
User,
user_permissions=Permission.objects.filter(codename="add_subscription"),
)
)
user = old_subscriber_user.make() user = old_subscriber_user.make()
response = client.post( response = client.post(
reverse("subscription:fragment-existing-user"), reverse("subscription:fragment-existing-user"),
@ -133,7 +139,12 @@ def test_submit_form_existing_user(client: Client, settings: SettingsWrapper):
@pytest.mark.django_db @pytest.mark.django_db
def test_submit_form_new_user(client: Client, settings: SettingsWrapper): def test_submit_form_new_user(client: Client, settings: SettingsWrapper):
client.force_login(board_user.make()) client.force_login(
baker.make(
User,
user_permissions=Permission.objects.filter(codename="add_subscription"),
)
)
response = client.post( response = client.post(
reverse("subscription:fragment-new-user"), reverse("subscription:fragment-new-user"),
{ {

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, CreateSubscriptionNewUserFragment,
NewSubscription, NewSubscription,
SubscriptionCreatedFragment, SubscriptionCreatedFragment,
SubscriptionPermissionView,
SubscriptionsStatsView, SubscriptionsStatsView,
) )
@ -41,5 +42,10 @@ urlpatterns = [
SubscriptionCreatedFragment.as_view(), SubscriptionCreatedFragment.as_view(),
name="creation-success", name="creation-success",
), ),
path(
"perms/",
SubscriptionPermissionView.as_view(),
name="perms",
),
path("stats/", SubscriptionsStatsView.as_view(), name="stats"), path("stats/", SubscriptionsStatsView.as_view(), name="stats"),
] ]

View File

@ -14,13 +14,15 @@
# #
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.timezone import localdate from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, TemplateView from django.views.generic import CreateView, DetailView, TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from core.views.group import PermissionGroupsUpdateView
from counter.apps import PAYMENT_METHOD from counter.apps import PAYMENT_METHOD
from subscription.forms import ( from subscription.forms import (
SelectionDateForm, SelectionDateForm,
@ -30,13 +32,9 @@ from subscription.forms import (
from subscription.models import Subscription from subscription.models import Subscription
class CanCreateSubscriptionMixin(UserPassesTestMixin): class NewSubscription(PermissionRequiredMixin, TemplateView):
def test_func(self):
return self.request.user.can_create_subscription
class NewSubscription(CanCreateSubscriptionMixin, TemplateView):
template_name = "subscription/subscription.jinja" template_name = "subscription/subscription.jinja"
permission_required = "subscription.add_subscription"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
@ -49,8 +47,9 @@ class NewSubscription(CanCreateSubscriptionMixin, TemplateView):
} }
class CreateSubscriptionFragment(CanCreateSubscriptionMixin, CreateView): class CreateSubscriptionFragment(PermissionRequiredMixin, CreateView):
template_name = "subscription/fragments/creation_form.jinja" template_name = "subscription/fragments/creation_form.jinja"
permission_required = "subscription.add_subscription"
def get_success_url(self): def get_success_url(self):
return reverse( return reverse(
@ -72,13 +71,21 @@ class CreateSubscriptionNewUserFragment(CreateSubscriptionFragment):
extra_context = {"post_url": reverse_lazy("subscription:fragment-new-user")} extra_context = {"post_url": reverse_lazy("subscription:fragment-new-user")}
class SubscriptionCreatedFragment(CanCreateSubscriptionMixin, DetailView): class SubscriptionCreatedFragment(PermissionRequiredMixin, DetailView):
template_name = "subscription/fragments/creation_success.jinja" template_name = "subscription/fragments/creation_success.jinja"
permission_required = "subscription.add_subscription"
model = Subscription model = Subscription
pk_url_kwarg = "subscription_id" pk_url_kwarg = "subscription_id"
context_object_name = "subscription" context_object_name = "subscription"
class SubscriptionPermissionView(PermissionGroupsUpdateView):
"""Manage the groups that have access to the subscription creation page."""
permission = "subscription.add_subscription"
extra_context = {"object_name": _("the groups that can create subscriptions")}
class SubscriptionsStatsView(FormView): class SubscriptionsStatsView(FormView):
template_name = "subscription/stats.jinja" template_name = "subscription/stats.jinja"
form_class = SelectionDateForm form_class = SelectionDateForm