diff --git a/club/forms.py b/club/forms.py index b9b4baaa..abc72df1 100644 --- a/club/forms.py +++ b/club/forms.py @@ -208,15 +208,14 @@ class ClubOldMemberForm(forms.Form): class ClubMemberForm(forms.ModelForm): - """Form handling the members of a club.""" + """Form to add a member to the club, as a board member.""" error_css_class = "error" required_css_class = "required" class Meta: model = Membership - fields = ["user", "role", "description"] - widgets = {"user": AutoCompleteSelectUser} + fields = ["role", "description"] def __init__(self, *args, club: Club, request_user: User, **kwargs): self.club = club @@ -231,22 +230,36 @@ class ClubMemberForm(forms.ModelForm): ] self.instance.club = club + @property + def max_available_role(self): # pragma: no cover + """The greatest role that will be obtainable with this form.""" + # this is unreachable, because it will be overridden by subclasses + return -1 + + +class ClubAddMemberForm(ClubMemberForm): + """Form to add a member to the club, as a board member.""" + + class Meta(ClubMemberForm.Meta): + fields = ["user", *ClubMemberForm.Meta.fields] + widgets = {"user": AutoCompleteSelectUser} + @cached_property def max_available_role(self): """The greatest role that will be obtainable with this form. Admins and the club president can attribute any role. Board members can attribute roles lower than their own. - Other users can attribute curious and member roles. + Other users cannot attribute roles with this form """ if self.request_user.has_perm("club.add_subscription"): return settings.SITH_CLUB_ROLES_ID["President"] membership = self.request_user_membership - if membership is not None and membership.role > settings.SITH_MAXIMUM_FREE_ROLE: - if membership.role == settings.SITH_CLUB_ROLES_ID["President"]: - return membership.role - return membership.role - 1 - return settings.SITH_MAXIMUM_FREE_ROLE + if membership is None or membership.role <= settings.SITH_MAXIMUM_FREE_ROLE: + return -1 + if membership.role == settings.SITH_CLUB_ROLES_ID["President"]: + return membership.role + return membership.role - 1 def clean_user(self): """Check that the user is not trying to add a user already in the club. @@ -264,18 +277,26 @@ class ClubMemberForm(forms.ModelForm): ) return user + +class JoinClubForm(ClubMemberForm): + """Form to join a club.""" + + def __init__(self, *args, club: Club, request_user: User, **kwargs): + super().__init__(*args, club=club, request_user=request_user, **kwargs) + self.instance.user = self.request_user + + @cached_property + def max_available_role(self): + return settings.SITH_MAXIMUM_FREE_ROLE + def clean(self): - """Check user rights for adding a user.""" - cleaned_data = super().clean() - if ( - self.request_user_membership is None - or self.request_user_membership.role <= settings.SITH_MAXIMUM_FREE_ROLE - ) and not self.request_user.has_perm("club.add_membership"): + """Check that the user is subscribed and isn't already in the club.""" + if not self.request_user.is_subscribed: raise forms.ValidationError( - _( - "You cannot add other users to a club " - "if you are not in the club board." - ), - code="invalid", + _("You must be subscribed to join a club"), code="invalid" ) - return cleaned_data + if self.club.get_membership_for(self.request_user): + raise forms.ValidationError( + _("You are already a member of this club"), code="invalid" + ) + return super().clean() diff --git a/club/templates/club/club_members.jinja b/club/templates/club/club_members.jinja index 5e5d02c1..3aa43d56 100644 --- a/club/templates/club/club_members.jinja +++ b/club/templates/club/club_members.jinja @@ -6,23 +6,22 @@ {% endblock %} {% block additional_css %} - {% endblock %} {% block content %} {% block notifications %} - {# Notifications are moved inside the billing info fragment #} + {# Notifications are moved a little bit below #} {% endblock %}

{% trans %}Club members{% endtrans %}

{% if add_member_fragment %}
-

{% trans %}Add a new member{% endtrans %}

{{ add_member_fragment }}
{% endif %} + {% include "core/base/notifications.jinja" %} {% if members %}
{% csrf_token %} diff --git a/club/templates/club/fragments/add_member.jinja b/club/templates/club/fragments/add_member.jinja index b19b451c..8efd878d 100644 --- a/club/templates/club/fragments/add_member.jinja +++ b/club/templates/club/fragments/add_member.jinja @@ -1,32 +1,46 @@ +
+ {% if form.user %} +

{% trans %}Add a new member{% endtrans %}

+ {% else %} +

{% trans %}Join club{% endtrans %}

+ {% endif %} -{% include "core/base/notifications.jinja" %} - - - {% csrf_token %} - {{ form.non_field_errors() }} -
-
- {{ form.user.label_tag()}} - {{ form.user.help_text }} - {{ form.user }} - {{ form.user.errors }} -
-
- {{ form.role.label_tag()}} - {{ form.role }} - {{ form.role.errors }} -
-
- {{ form.description.label_tag()}} - {{ form.description }} - {{ form.description.errors }} -
-
- - +
+ {% csrf_token %} + {{ form.non_field_errors() }} +
+ {% if form.user %} +
+ {{ form.user.label_tag() }} + {{ form.user.help_text }} + {{ form.user }} + {{ form.user.errors }} +
+ {% endif %} +
+ {{ form.role.label_tag() }} + {{ form.role }} + {{ form.role.errors }} +
+
+ {{ form.description.label_tag() }} + {{ form.description }} + {{ form.description.errors }} +
+
+ +
+
diff --git a/club/tests/test_membership.py b/club/tests/test_membership.py index f8ef2b6d..a3c0be50 100644 --- a/club/tests/test_membership.py +++ b/club/tests/test_membership.py @@ -1,5 +1,7 @@ +from collections.abc import Callable from datetime import timedelta +import pytest from bs4 import BeautifulSoup from django.conf import settings from django.contrib.auth.models import Permission @@ -11,7 +13,7 @@ from django.utils.timezone import localdate, localtime, now from model_bakery import baker from pytest_django.asserts import assertRedirects -from club.forms import ClubMemberForm +from club.forms import ClubAddMemberForm, JoinClubForm from club.models import Club, Membership from club.tests.base import TestClub from core.baker_recipes import subscriber_user @@ -268,7 +270,7 @@ class TestMembership(TestClub): cannot be members of clubs. """ for user in self.public, self.old_subscriber: - form = ClubMemberForm( + form = ClubAddMemberForm( data={"user": user.id, "role": 1}, request_user=self.root, club=self.club, @@ -308,7 +310,7 @@ class TestMembership(TestClub): nb_memberships = self.club.members.count() max_id = User.objects.aggregate(id=Max("id"))["id"] for members in [max_id + 1], [max_id + 1, self.subscriber.id]: - form = ClubMemberForm( + form = ClubAddMemberForm( data={"user": members, "role": 1}, request_user=self.root, club=self.club, @@ -346,7 +348,7 @@ class TestMembership(TestClub): """Test that a member of the club member cannot create a membership with a greater role than its own. """ - form = ClubMemberForm( + form = ClubAddMemberForm( data={"user": self.subscriber.id, "role": 10}, request_user=self.simple_board_member, club=self.club, @@ -363,7 +365,7 @@ class TestMembership(TestClub): def test_add_member_without_role(self): """Test that trying to add members without specifying their role fails.""" - form = ClubMemberForm( + form = ClubAddMemberForm( data={"user": self.subscriber.id}, request_user=self.root, club=self.club ) @@ -371,7 +373,7 @@ class TestMembership(TestClub): assert form.errors == {"role": ["Ce champ est obligatoire."]} def test_add_member_already_there(self): - form = ClubMemberForm( + form = ClubAddMemberForm( data={"user": self.simple_board_member, "role": 3}, request_user=self.root, club=self.club, @@ -385,17 +387,14 @@ class TestMembership(TestClub): non_member = subscriber_user.make() simple_member = baker.make(Membership, club=self.club, role=1).user for user in non_member, simple_member: - form = ClubMemberForm( + form = ClubAddMemberForm( data={"user": subscriber_user.make(), "role": 1}, request_user=user, club=self.club, ) assert not form.is_valid() assert form.errors == { - "__all__": [ - "Vous ne pouvez pas ajouter d'autres utilisateurs " - "dans un club si vous ne faites pas partie de son bureau." - ] + "role": ["Sélectionnez un choix valide. 1 n\u2019en fait pas partie."] } def test_simple_members_dont_see_form_anymore(self): @@ -533,6 +532,57 @@ class TestMembership(TestClub): assert new_board == initial_board +@pytest.mark.django_db +class TestJoinClub: + @pytest.fixture(autouse=True) + def clear_cache(self): + cache.clear() + + @pytest.mark.parametrize( + ("user_factory", "role", "errors"), + [ + ( + subscriber_user.make, + 2, + { + "role": [ + "Sélectionnez un choix valide. 2 n\u2019en fait pas partie." + ] + }, + ), + ( + lambda: baker.make(User), + 1, + {"__all__": ["Vous devez être cotisant pour faire partie d'un club"]}, + ), + ], + ) + def test_join_club_errors( + self, user_factory: Callable[[], User], role: int, errors: dict + ): + club = baker.make(Club) + user = user_factory() + form = JoinClubForm(club=club, request_user=user, data={"role": role}) + assert not form.is_valid() + assert form.errors == errors + + def test_user_already_in_club(self): + club = baker.make(Club) + user = subscriber_user.make() + baker.make(Membership, user=user, club=club) + form = JoinClubForm(club=club, request_user=user, data={"role": 1}) + assert not form.is_valid() + assert form.errors == {"__all__": ["Vous êtes déjà membre de ce club."]} + + def test_ok(self): + club = baker.make(Club) + user = subscriber_user.make() + form = JoinClubForm(club=club, request_user=user, data={"role": 1}) + assert form.is_valid() + form.save() + assert Membership.objects.ongoing().filter(user=user, club=club).exists() + + class TestOldMembersView(TestCase): @classmethod def setUpTestData(cls): diff --git a/club/views.py b/club/views.py index c0e496d4..0af2db9d 100644 --- a/club/views.py +++ b/club/views.py @@ -47,10 +47,11 @@ from django.views.generic import DetailView, ListView, View from django.views.generic.edit import CreateView, DeleteView, UpdateView from club.forms import ( + ClubAddMemberForm, ClubAdminEditForm, ClubEditForm, - ClubMemberForm, ClubOldMemberForm, + JoinClubForm, MailingForm, SellingsForm, ) @@ -266,17 +267,21 @@ class ClubAddMembersFragment( FragmentMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView ): template_name = "club/fragments/add_member.jinja" - form_class = ClubMemberForm model = Membership object = None reload_on_redirect = True permission_required = "club.view_club" - success_message = _("%(user)s has been added to club.") def dispatch(self, *args, **kwargs): self.club = get_object_or_404(Club, pk=kwargs.get("club_id")) return super().dispatch(*args, **kwargs) + def get_form_class(self): + user = self.request.user + if user.has_perm("club.add_membership") or self.club.get_membership_for(user): + return ClubAddMemberForm + return JoinClubForm + def get_form_kwargs(self): return super().get_form_kwargs() | { "request_user": self.request.user, @@ -293,6 +298,11 @@ class ClubAddMembersFragment( def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | {"club": self.club} + def get_success_message(self, cleaned_data): + if "user" not in cleaned_data or cleaned_data["user"] == self.request.user: + return _("You are now a member of this club.") + return _("%(user)s has been added to club.") % cleaned_data + class ClubMembersView( ClubTabsMixin, UseFragmentsMixin, PermissionRequiredMixin, DetailFormView diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 36ab0f12..aa135d87 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-25 15:33+0200\n" +"POT-Creation-Date: 2025-09-26 17:36+0200\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -174,14 +174,12 @@ msgid "You can not add the same user twice" msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur" #: club/forms.py -msgid "You cannot add other users to a club if you are not in the club board." -msgstr "" -"Vous ne pouvez pas ajouter d'autres utilisateurs dans un club si vous ne " -"faites pas partie de son bureau." +msgid "You must be subscribed to join a club" +msgstr "Vous devez être cotisant pour faire partie d'un club" -#: club/forms.py sas/forms.py -msgid "You do not have the permission to do that" -msgstr "Vous n'avez pas la permission de faire cela" +#: club/forms.py +msgid "You are already a member of this club" +msgstr "Vous êtes déjà membre de ce club." #: club/models.py msgid "slug name" @@ -328,10 +326,6 @@ msgstr "Il n'y a pas de club dans ce site web." msgid "Club members" msgstr "Membres du club" -#: club/templates/club/club_members.jinja -msgid "Add a new member" -msgstr "Ajouter un nouveau membre" - #: club/templates/club/club_members.jinja #: club/templates/club/club_old_members.jinja #: core/templates/core/user_clubs.jinja @@ -570,12 +564,24 @@ msgstr "" msgid "Save" msgstr "Sauver" +#: club/templates/club/fragments/add_member.jinja +msgid "Add a new member" +msgstr "Ajouter un nouveau membre" + +#: club/templates/club/fragments/add_member.jinja +msgid "Join club" +msgstr "Rejoindre le club" + #: club/templates/club/fragments/add_member.jinja #: core/templates/core/file_detail.jinja core/views/forms.py #: trombi/templates/trombi/detail.jinja msgid "Add" msgstr "Ajouter" +#: club/templates/club/fragments/add_member.jinja +msgid "Join" +msgstr "Rejoindre" + #: club/templates/club/mailing.jinja msgid "Mailing lists" msgstr "Mailing listes" @@ -687,6 +693,10 @@ msgstr "Listes de diffusion" msgid "%(user)s has been added to club." msgstr "%(user)s a été ajouté au club." +#: club/views.py +msgid "You are now a member of this club." +msgstr "Vous êtes maintenant membre de ce club." + #: com/forms.py msgid "Format: 16:9 | Resolution: 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080" @@ -4657,6 +4667,10 @@ msgstr "Pas de ban actif" msgid "Add a new album" msgstr "Ajouter un nouvel album" +#: sas/forms.py +msgid "You do not have the permission to do that" +msgstr "Vous n'avez pas la permission de faire cela" + #: sas/forms.py msgid "Upload images" msgstr "Envoyer les images" @@ -5558,4 +5572,4 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée." #: trombi/views.py #, python-format msgid "Maximum characters: %(max_length)s" -msgstr "Nombre de caractères max: %(max_length)s" \ No newline at end of file +msgstr "Nombre de caractères max: %(max_length)s"