From 9b862cfefce7cb12b4f64ac01f2b6287a1be466d Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 19 Apr 2026 14:43:37 +0200 Subject: [PATCH] create default club roles on club creation --- club/admin.py | 13 ++++ .../0015_clubrole_alter_membership_role.py | 20 +++++ club/models.py | 40 +++++++++- club/tests/test_club.py | 78 +++++++++++++++++++ club/views.py | 5 ++ 5 files changed, 153 insertions(+), 3 deletions(-) diff --git a/club/admin.py b/club/admin.py index bff21208..08883913 100644 --- a/club/admin.py +++ b/club/admin.py @@ -13,6 +13,8 @@ # # from django.contrib import admin +from django.forms.models import ModelForm +from django.http import HttpRequest from club.models import Club, ClubRole, Membership @@ -29,6 +31,17 @@ class ClubAdmin(admin.ModelAdmin): "page", ) + def save_model( + self, + request: HttpRequest, + obj: Club, + form: ModelForm, + change: bool, # noqa: FBT001 + ): + super().save_model(request, obj, form, change) + if not change: + obj.create_default_roles() + @admin.register(ClubRole) class ClubRoleAdmin(admin.ModelAdmin): diff --git a/club/migrations/0015_clubrole_alter_membership_role.py b/club/migrations/0015_clubrole_alter_membership_role.py index f776b20c..6a1b58ee 100644 --- a/club/migrations/0015_clubrole_alter_membership_role.py +++ b/club/migrations/0015_clubrole_alter_membership_role.py @@ -121,6 +121,26 @@ class Migration(migrations.Migration): "verbose_name_plural": "club roles", }, ), + migrations.AlterField( + model_name="club", + name="board_group", + field=models.OneToOneField( + editable=False, + on_delete=django.db.models.deletion.PROTECT, + related_name="club_board", + to="core.group", + ), + ), + migrations.AlterField( + model_name="club", + name="members_group", + field=models.OneToOneField( + editable=False, + on_delete=django.db.models.deletion.PROTECT, + related_name="club", + to="core.group", + ), + ), migrations.AddConstraint( model_name="clubrole", constraint=models.CheckConstraint( diff --git a/club/models.py b/club/models.py index 5e072853..f8698d6b 100644 --- a/club/models.py +++ b/club/models.py @@ -28,7 +28,7 @@ from typing import Iterable, Self from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import RegexValidator, validate_email -from django.db import models, transaction +from django.db import ProgrammingError, models, transaction from django.db.models import Exists, F, OuterRef, Q from django.urls import reverse from django.utils import timezone @@ -92,10 +92,10 @@ class Club(models.Model): Page, related_name="club", blank=True, on_delete=models.PROTECT ) members_group = models.OneToOneField( - Group, related_name="club", on_delete=models.PROTECT + Group, related_name="club", on_delete=models.PROTECT, editable=False ) board_group = models.OneToOneField( - Group, related_name="club_board", on_delete=models.PROTECT + Group, related_name="club_board", on_delete=models.PROTECT, editable=False ) objects = ClubQuerySet.as_manager() @@ -183,6 +183,40 @@ class Club(models.Model): self.page.parent = self.parent.page self.page.save(force_lock=True) + def create_default_roles(self): + """Create some roles that should exist by default for this club. + + The created roles are : president, treasurer, active member and curious. + + Warnings: + When calling this method, no club must exist yet for this club. + """ + if self.roles.exists(): + raise ProgrammingError( + "Default roles can be created only for clubs " + "that don't have associated roles yet" + ) + # The names are written in French, because there is no gettext involved + # for strings stored in database, and the majority of users are french. + roles = [ + ClubRole(name="Président⸱e", is_board=True, is_presidency=True), + ClubRole(name="Trésorier⸱e", is_board=True, is_presidency=False), + ClubRole(name="Membre actif⸱ve", is_board=False, is_presidency=False), + ClubRole( + name="Curieux⸱euse", + description=( + "Les gens qui suivent l'activité " + "du club sans forcément y participer" + ), + is_board=False, + is_presidency=False, + ), + ] + for i, role in enumerate(roles): + role.club = self + role.order = i + ClubRole.objects.bulk_create(roles) + def delete(self, *args, **kwargs) -> tuple[int, dict[str, int]]: self.board_group.delete() self.members_group.delete() diff --git a/club/tests/test_club.py b/club/tests/test_club.py index 0e25995e..e64b4ce2 100644 --- a/club/tests/test_club.py +++ b/club/tests/test_club.py @@ -1,11 +1,14 @@ from datetime import timedelta import pytest +from django.conf import settings +from django.db import ProgrammingError from django.test import Client from django.urls import reverse from django.utils.timezone import localdate from model_bakery import baker from model_bakery.recipe import Recipe +from pytest_django.asserts import assertRedirects from club.models import Club, ClubRole, Membership from core.baker_recipes import subscriber_user @@ -47,3 +50,78 @@ def test_club_list(client: Client, nb_additional_clubs: int, is_fragment): headers = {"HX-Request": True} if is_fragment else {} res = client.get(reverse("club:club_list"), headers=headers) assert res.status_code == 200 + + +def assert_club_created(club_name: str): + club = Club.objects.last() + assert club.name == club_name + assert club.board_group.name == f"{club_name} - Bureau" + assert club.members_group.name == f"{club_name} - Membres" + # default roles should be added on club creation, + # whether the creation happens on the admin site or on the user site + assert list(club.roles.values("name", "is_presidency", "is_board")) == [ + {"name": "Président⸱e", "is_presidency": True, "is_board": True}, + {"name": "Trésorier⸱e", "is_presidency": False, "is_board": True}, + {"name": "Membre actif⸱ve", "is_presidency": False, "is_board": False}, + {"name": "Curieux⸱euse", "is_presidency": False, "is_board": False}, + ] + + +@pytest.mark.django_db +def test_create_view(admin_client: Client): + """Test that the club creation view works well""" + res = admin_client.get(reverse("club:club_new")) + assert res.status_code == 200 + res = admin_client.post( + reverse("club:club_new"), + data={"name": "foo", "parent": settings.SITH_MAIN_CLUB_ID}, + ) + club = Club.objects.last() + assertRedirects(res, club.get_absolute_url()) + assert_club_created("foo") + + +@pytest.mark.django_db +def test_default_roles_for_club_with_roles_fails(): + """Test that an Error is raised if trying to create + default roles for a club that already has roles. + """ + club = baker.make(Club) + baker.make(ClubRole, club=club) + with pytest.raises(ProgrammingError): + club.create_default_roles() + + +@pytest.mark.django_db +class TestAdminInterface: + def test_create(self, admin_client: Client): + """Test the creation of a club via the admin interface.""" + res = admin_client.post( + reverse("admin:club_club_add"), + data={ + "name": "foo", + "parent": settings.SITH_MAIN_CLUB_ID, + "address": "Rome", + }, + ) + assertRedirects(res, reverse("admin:club_club_changelist")) + assert_club_created("foo") + + def test_change(self, admin_client: Client): + """Test the edition of a club via the admin interface.""" + club = baker.make(Club) + res = admin_client.post( + reverse("admin:club_club_change", kwargs={"object_id": club.id}), + data={ + "name": "foo", + "page": club.page_id, + "home": club.home_id, + "address": club.address, + }, + ) + assertRedirects(res, reverse("admin:club_club_changelist")) + club.refresh_from_db() + assert club.name == "foo" + # Club roles shouldn't be modified when editing the club on the admin interface + # This club had no roles beforehand, therefore it shouldn't have roles now. + assert not club.roles.exists() diff --git a/club/views.py b/club/views.py index 92de52d2..cce60e8c 100644 --- a/club/views.py +++ b/club/views.py @@ -580,6 +580,11 @@ class ClubCreateView(PermissionRequiredMixin, CreateView): template_name = "core/create.jinja" permission_required = "club.add_club" + def form_valid(self, form): + res = super().form_valid(form) + self.object.create_default_roles() + return res + class MembershipSetOldView(CanEditMixin, SingleObjectMixin, View): """Set a membership as being old."""