Split ClubMemberForm into JoinClubForm and ClubAddMemberForm

This commit is contained in:
imperosol
2025-09-26 18:23:49 +02:00
parent cc58479a19
commit eadf74604c
6 changed files with 190 additions and 82 deletions

View File

@@ -208,15 +208,14 @@ class ClubOldMemberForm(forms.Form):
class ClubMemberForm(forms.ModelForm): 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" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
class Meta: class Meta:
model = Membership model = Membership
fields = ["user", "role", "description"] fields = ["role", "description"]
widgets = {"user": AutoCompleteSelectUser}
def __init__(self, *args, club: Club, request_user: User, **kwargs): def __init__(self, *args, club: Club, request_user: User, **kwargs):
self.club = club self.club = club
@@ -231,22 +230,36 @@ class ClubMemberForm(forms.ModelForm):
] ]
self.instance.club = club 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 @cached_property
def max_available_role(self): def max_available_role(self):
"""The greatest role that will be obtainable with this form. """The greatest role that will be obtainable with this form.
Admins and the club president can attribute any role. Admins and the club president can attribute any role.
Board members can attribute roles lower than their own. 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"): if self.request_user.has_perm("club.add_subscription"):
return settings.SITH_CLUB_ROLES_ID["President"] return settings.SITH_CLUB_ROLES_ID["President"]
membership = self.request_user_membership membership = self.request_user_membership
if membership is not None and membership.role > settings.SITH_MAXIMUM_FREE_ROLE: if membership is None or membership.role <= settings.SITH_MAXIMUM_FREE_ROLE:
if membership.role == settings.SITH_CLUB_ROLES_ID["President"]: return -1
return membership.role if membership.role == settings.SITH_CLUB_ROLES_ID["President"]:
return membership.role - 1 return membership.role
return settings.SITH_MAXIMUM_FREE_ROLE return membership.role - 1
def clean_user(self): def clean_user(self):
"""Check that the user is not trying to add a user already in the club. """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 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): def clean(self):
"""Check user rights for adding a user.""" """Check that the user is subscribed and isn't already in the club."""
cleaned_data = super().clean() if not self.request_user.is_subscribed:
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"):
raise forms.ValidationError( raise forms.ValidationError(
_( _("You must be subscribed to join a club"), code="invalid"
"You cannot add other users to a club "
"if you are not in the club board."
),
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()

View File

@@ -6,23 +6,22 @@
{% endblock %} {% endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}"> <link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
<link rel="stylesheet" href="{{ static("club/members.scss") }}"> <link rel="stylesheet" href="{{ static("club/members.scss") }}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% block notifications %} {% block notifications %}
{# Notifications are moved inside the billing info fragment #} {# Notifications are moved a little bit below #}
{% endblock %} {% endblock %}
<h2>{% trans %}Club members{% endtrans %}</h2> <h2>{% trans %}Club members{% endtrans %}</h2>
{% if add_member_fragment %} {% if add_member_fragment %}
<br /> <br />
<h4>{% trans %}Add a new member{% endtrans %}</h4>
{{ add_member_fragment }} {{ add_member_fragment }}
<br /> <br />
{% endif %} {% endif %}
{% include "core/base/notifications.jinja" %}
{% if members %} {% if members %}
<form action="{{ url('club:club_members', club_id=club.id) }}" id="members_old" method="post"> <form action="{{ url('club:club_members', club_id=club.id) }}" id="members_old" method="post">
{% csrf_token %} {% csrf_token %}

View File

@@ -1,32 +1,46 @@
<section id="member-fragment-container">
{% if form.user %}
<h4>{% trans %}Add a new member{% endtrans %}</h4>
{% else %}
<h4>{% trans %}Join club{% endtrans %}</h4>
{% endif %}
{% include "core/base/notifications.jinja" %} <form
hx-post="{{ url('club:club_new_members', club_id=club.id) }}"
<form hx-disabled-elt="find input[type='submit']"
hx-post="{{ url('club:club_new_members', club_id=club.id) }}" hx-swap="outerHTML"
hx-disabled-elt="find input[type='submit']" hx-target="#member-fragment-container"
hx-swap="outerHTML" id="add_club_members_form"
id="add_club_members_form" >
> {% csrf_token %}
{% csrf_token %} {{ form.non_field_errors() }}
{{ form.non_field_errors() }} <fieldset>
<fieldset> {% if form.user %}
<div> <div>
{{ form.user.label_tag()}} {{ form.user.label_tag() }}
<span class="helptext">{{ form.user.help_text }}</span> <span class="helptext">{{ form.user.help_text }}</span>
{{ form.user }} {{ form.user }}
{{ form.user.errors }} {{ form.user.errors }}
</div> </div>
<div> {% endif %}
{{ form.role.label_tag()}} <div>
{{ form.role }} {{ form.role.label_tag() }}
{{ form.role.errors }} {{ form.role }}
</div> {{ form.role.errors }}
<div> </div>
{{ form.description.label_tag()}} <div>
{{ form.description }} {{ form.description.label_tag() }}
{{ form.description.errors }} {{ form.description }}
</div> {{ form.description.errors }}
</fieldset> </div>
<button type="submit" class="btn btn-blue"> </fieldset>
<i class="fa fa-user-plus"></i> {% trans %}Add{% endtrans %}</button> <button type="submit" class="btn btn-blue">
</form> <i class="fa fa-user-plus"></i>
{%- if club.user -%}
{% trans %}Add{% endtrans %}
{%- else -%}
{% trans %}Join{% endtrans %}
{%- endif -%}
</button>
</form>
</section>

View File

@@ -1,5 +1,7 @@
from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
import pytest
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission 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 model_bakery import baker
from pytest_django.asserts import assertRedirects 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.models import Club, Membership
from club.tests.base import TestClub from club.tests.base import TestClub
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
@@ -268,7 +270,7 @@ class TestMembership(TestClub):
cannot be members of clubs. cannot be members of clubs.
""" """
for user in self.public, self.old_subscriber: for user in self.public, self.old_subscriber:
form = ClubMemberForm( form = ClubAddMemberForm(
data={"user": user.id, "role": 1}, data={"user": user.id, "role": 1},
request_user=self.root, request_user=self.root,
club=self.club, club=self.club,
@@ -308,7 +310,7 @@ class TestMembership(TestClub):
nb_memberships = self.club.members.count() nb_memberships = self.club.members.count()
max_id = User.objects.aggregate(id=Max("id"))["id"] max_id = User.objects.aggregate(id=Max("id"))["id"]
for members in [max_id + 1], [max_id + 1, self.subscriber.id]: for members in [max_id + 1], [max_id + 1, self.subscriber.id]:
form = ClubMemberForm( form = ClubAddMemberForm(
data={"user": members, "role": 1}, data={"user": members, "role": 1},
request_user=self.root, request_user=self.root,
club=self.club, club=self.club,
@@ -346,7 +348,7 @@ class TestMembership(TestClub):
"""Test that a member of the club member cannot create """Test that a member of the club member cannot create
a membership with a greater role than its own. a membership with a greater role than its own.
""" """
form = ClubMemberForm( form = ClubAddMemberForm(
data={"user": self.subscriber.id, "role": 10}, data={"user": self.subscriber.id, "role": 10},
request_user=self.simple_board_member, request_user=self.simple_board_member,
club=self.club, club=self.club,
@@ -363,7 +365,7 @@ class TestMembership(TestClub):
def test_add_member_without_role(self): def test_add_member_without_role(self):
"""Test that trying to add members without specifying their role fails.""" """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 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."]} assert form.errors == {"role": ["Ce champ est obligatoire."]}
def test_add_member_already_there(self): def test_add_member_already_there(self):
form = ClubMemberForm( form = ClubAddMemberForm(
data={"user": self.simple_board_member, "role": 3}, data={"user": self.simple_board_member, "role": 3},
request_user=self.root, request_user=self.root,
club=self.club, club=self.club,
@@ -385,17 +387,14 @@ class TestMembership(TestClub):
non_member = subscriber_user.make() non_member = subscriber_user.make()
simple_member = baker.make(Membership, club=self.club, role=1).user simple_member = baker.make(Membership, club=self.club, role=1).user
for user in non_member, simple_member: for user in non_member, simple_member:
form = ClubMemberForm( form = ClubAddMemberForm(
data={"user": subscriber_user.make(), "role": 1}, data={"user": subscriber_user.make(), "role": 1},
request_user=user, request_user=user,
club=self.club, club=self.club,
) )
assert not form.is_valid() assert not form.is_valid()
assert form.errors == { assert form.errors == {
"__all__": [ "role": ["Sélectionnez un choix valide. 1 n\u2019en fait pas partie."]
"Vous ne pouvez pas ajouter d'autres utilisateurs "
"dans un club si vous ne faites pas partie de son bureau."
]
} }
def test_simple_members_dont_see_form_anymore(self): def test_simple_members_dont_see_form_anymore(self):
@@ -533,6 +532,57 @@ class TestMembership(TestClub):
assert new_board == initial_board 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): class TestOldMembersView(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@@ -47,10 +47,11 @@ from django.views.generic import DetailView, ListView, View
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from club.forms import ( from club.forms import (
ClubAddMemberForm,
ClubAdminEditForm, ClubAdminEditForm,
ClubEditForm, ClubEditForm,
ClubMemberForm,
ClubOldMemberForm, ClubOldMemberForm,
JoinClubForm,
MailingForm, MailingForm,
SellingsForm, SellingsForm,
) )
@@ -266,17 +267,21 @@ class ClubAddMembersFragment(
FragmentMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView FragmentMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
): ):
template_name = "club/fragments/add_member.jinja" template_name = "club/fragments/add_member.jinja"
form_class = ClubMemberForm
model = Membership model = Membership
object = None object = None
reload_on_redirect = True reload_on_redirect = True
permission_required = "club.view_club" permission_required = "club.view_club"
success_message = _("%(user)s has been added to club.")
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
self.club = get_object_or_404(Club, pk=kwargs.get("club_id")) self.club = get_object_or_404(Club, pk=kwargs.get("club_id"))
return super().dispatch(*args, **kwargs) 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): def get_form_kwargs(self):
return super().get_form_kwargs() | { return super().get_form_kwargs() | {
"request_user": self.request.user, "request_user": self.request.user,
@@ -293,6 +298,11 @@ class ClubAddMembersFragment(
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"club": self.club} 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( class ClubMembersView(
ClubTabsMixin, UseFragmentsMixin, PermissionRequiredMixin, DetailFormView ClubTabsMixin, UseFragmentsMixin, PermissionRequiredMixin, DetailFormView

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "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" "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"
@@ -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" msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur"
#: club/forms.py #: club/forms.py
msgid "You cannot add other users to a club if you are not in the club board." msgid "You must be subscribed to join a club"
msgstr "" msgstr "Vous devez être cotisant pour faire partie d'un club"
"Vous ne pouvez pas ajouter d'autres utilisateurs dans un club si vous ne "
"faites pas partie de son bureau."
#: club/forms.py sas/forms.py #: club/forms.py
msgid "You do not have the permission to do that" msgid "You are already a member of this club"
msgstr "Vous n'avez pas la permission de faire cela" msgstr "Vous êtes déjà membre de ce club."
#: club/models.py #: club/models.py
msgid "slug name" msgid "slug name"
@@ -328,10 +326,6 @@ msgstr "Il n'y a pas de club dans ce site web."
msgid "Club members" msgid "Club members"
msgstr "Membres du club" 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_members.jinja
#: club/templates/club/club_old_members.jinja #: club/templates/club/club_old_members.jinja
#: core/templates/core/user_clubs.jinja #: core/templates/core/user_clubs.jinja
@@ -570,12 +564,24 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "Sauver" 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 #: club/templates/club/fragments/add_member.jinja
#: core/templates/core/file_detail.jinja core/views/forms.py #: core/templates/core/file_detail.jinja core/views/forms.py
#: trombi/templates/trombi/detail.jinja #: trombi/templates/trombi/detail.jinja
msgid "Add" msgid "Add"
msgstr "Ajouter" msgstr "Ajouter"
#: club/templates/club/fragments/add_member.jinja
msgid "Join"
msgstr "Rejoindre"
#: club/templates/club/mailing.jinja #: club/templates/club/mailing.jinja
msgid "Mailing lists" msgid "Mailing lists"
msgstr "Mailing listes" msgstr "Mailing listes"
@@ -687,6 +693,10 @@ msgstr "Listes de diffusion"
msgid "%(user)s has been added to club." msgid "%(user)s has been added to club."
msgstr "%(user)s a été ajouté au 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 #: com/forms.py
msgid "Format: 16:9 | Resolution: 1920x1080" msgid "Format: 16:9 | Resolution: 1920x1080"
msgstr "Format : 16:9 | Résolution : 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080"
@@ -4657,6 +4667,10 @@ msgstr "Pas de ban actif"
msgid "Add a new album" msgid "Add a new album"
msgstr "Ajouter un nouvel 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 #: sas/forms.py
msgid "Upload images" msgid "Upload images"
msgstr "Envoyer les images" msgstr "Envoyer les images"