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):
"""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()

View File

@@ -6,23 +6,22 @@
{% endblock %}
{% block additional_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") }}">
{% endblock %}
{% block content %}
{% block notifications %}
{# Notifications are moved inside the billing info fragment #}
{# Notifications are moved a little bit below #}
{% endblock %}
<h2>{% trans %}Club members{% endtrans %}</h2>
{% if add_member_fragment %}
<br />
<h4>{% trans %}Add a new member{% endtrans %}</h4>
{{ add_member_fragment }}
<br />
{% endif %}
{% include "core/base/notifications.jinja" %}
{% if members %}
<form action="{{ url('club:club_members', club_id=club.id) }}" id="members_old" method="post">
{% 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) }}"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML"
id="add_club_members_form"
>
{% csrf_token %}
{{ form.non_field_errors() }}
<fieldset>
<div>
{{ form.user.label_tag()}}
<span class="helptext">{{ form.user.help_text }}</span>
{{ form.user }}
{{ form.user.errors }}
</div>
<div>
{{ form.role.label_tag()}}
{{ form.role }}
{{ form.role.errors }}
</div>
<div>
{{ form.description.label_tag()}}
{{ form.description }}
{{ form.description.errors }}
</div>
</fieldset>
<button type="submit" class="btn btn-blue">
<i class="fa fa-user-plus"></i> {% trans %}Add{% endtrans %}</button>
</form>
<form
hx-post="{{ url('club:club_new_members', club_id=club.id) }}"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML"
hx-target="#member-fragment-container"
id="add_club_members_form"
>
{% csrf_token %}
{{ form.non_field_errors() }}
<fieldset>
{% if form.user %}
<div>
{{ form.user.label_tag() }}
<span class="helptext">{{ form.user.help_text }}</span>
{{ form.user }}
{{ form.user.errors }}
</div>
{% endif %}
<div>
{{ form.role.label_tag() }}
{{ form.role }}
{{ form.role.errors }}
</div>
<div>
{{ form.description.label_tag() }}
{{ form.description }}
{{ form.description.errors }}
</div>
</fieldset>
<button type="submit" class="btn btn-blue">
<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
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):

View File

@@ -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

View File

@@ -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 <thomas.girod@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"
#: 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"