Allow club role-group m2m links

This commit is contained in:
imperosol
2026-09-01 18:32:37 +02:00
parent 7fca5d8c75
commit 8153d7a105
4 changed files with 85 additions and 15 deletions
+2 -2
View File
@@ -46,8 +46,8 @@ class ClubAdmin(admin.ModelAdmin):
@admin.register(ClubRole) @admin.register(ClubRole)
class ClubRoleAdmin(admin.ModelAdmin): class ClubRoleAdmin(admin.ModelAdmin):
list_display = ("name", "club", "is_board", "is_presidency") list_display = ("name", "club", "is_board", "is_presidency")
search_fields = ("name",) search_fields = ("name", "club__name")
autocomplete_fields = ("club",) autocomplete_fields = ("club", "linked_groups")
list_select_related = ("club",) list_select_related = ("club",)
list_filter = ( list_filter = (
"is_board", "is_board",
@@ -0,0 +1,26 @@
# Generated by Django 5.2.17 on 2026-09-01 14:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("club", "0017_linktype_clublink"),
("core", "0050_alter_sithfile_moderator"),
]
operations = [
migrations.AddField(
model_name="clubrole",
name="linked_groups",
field=models.ManyToManyField(
help_text=(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="club_roles",
to="core.group",
verbose_name="Linked groups",
),
),
]
+32 -11
View File
@@ -23,6 +23,8 @@
# #
from __future__ import annotations from __future__ import annotations
import operator
from functools import reduce
from typing import Iterable, Self from typing import Iterable, Self
from django.conf import settings from django.conf import settings
@@ -282,6 +284,15 @@ class ClubRole(OrderedModel):
"If the role is inactive, people joining the club won't be able to get it." "If the role is inactive, people joining the club won't be able to get it."
), ),
) )
linked_groups = models.ManyToManyField(
Group,
verbose_name=_("Linked groups"),
help_text=_(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="linked_roles",
)
order_with_respect_to = "club" order_with_respect_to = "club"
@@ -534,7 +545,7 @@ class Membership(models.Model):
def _remove_club_groups( def _remove_club_groups(
memberships: Iterable[Membership], memberships: Iterable[Membership],
) -> tuple[int, dict[str, int]]: ) -> tuple[int, dict[str, int]]:
"""Remove users of those memberships from the club groups. """Remove users of those memberships from the club and club role groups.
For example, if a user is in the Troll club board, For example, if a user is in the Troll club board,
he is in the board group and the members group of the Troll. he is in the board group and the members group of the Troll.
@@ -553,15 +564,19 @@ class Membership(models.Model):
clubs = {m.club_id for m in memberships} clubs = {m.club_id for m in memberships}
users = {m.user_id for m in memberships} users = {m.user_id for m in memberships}
groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs)) groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs))
role_groups = [
Q(user_id=m.user_id, group__linked_roles=m.role_id) for m in memberships
]
return User.groups.through.objects.filter( return User.groups.through.objects.filter(
Q(group__in=groups) & Q(user__in=users) (Q(group__in=groups) & Q(user__in=users))
| reduce(operator.or_, role_groups)
).delete() ).delete()
@staticmethod @staticmethod
def _add_club_groups( def _add_club_groups(
memberships: Iterable[Membership], memberships: Iterable[Membership],
) -> list[User.groups.through]: ) -> list[User.groups.through]:
"""Add users of those memberships to the club groups. """Add users of those memberships to the club and club role groups.
For example, if a user just joined the Troll club board, For example, if a user just joined the Troll club board,
he will be added in both the members group and the board group he will be added in both the members group and the board group
@@ -582,34 +597,40 @@ class Membership(models.Model):
memberships = [m for m in memberships if m.end_date is None] memberships = [m for m in memberships if m.end_date is None]
if not memberships: if not memberships:
return [] return []
nb_prefetched = sum(
if sum(1 for m in memberships if not hasattr(m, "club")) > 1: 1 for m in memberships if not hasattr(m, "club") or not hasattr(m, "role")
)
if nb_prefetched > 1:
# if more than one membership hasn't its `club` attribute set # if more than one membership hasn't its `club` attribute set
# it's less expensive to reload the whole query with # it's less expensive to reload the whole query with
# a select_related than perform a distinct query # a select_related than perform a distinct query
# to fetch each club. # to fetch each club.
ids = {m.id for m in memberships} ids = {m.id for m in memberships}
memberships = list( memberships = list(
Membership.objects.filter(id__in=ids).select_related("club") Membership.objects.filter(id__in=ids)
.select_related("club", "role")
.prefetch_related("role__linked_groups")
) )
club_groups = [] groups = []
for membership in memberships: for membership in memberships:
club_groups.append( groups.append(
User.groups.through( User.groups.through(
user_id=membership.user_id, user_id=membership.user_id,
group_id=membership.club.members_group_id, group_id=membership.club.members_group_id,
) )
) )
if membership.role.is_board: if membership.role.is_board:
club_groups.append( groups.append(
User.groups.through( User.groups.through(
user_id=membership.user_id, user_id=membership.user_id,
group_id=membership.club.board_group_id, group_id=membership.club.board_group_id,
) )
) )
return User.groups.through.objects.bulk_create( groups.extend(
club_groups, ignore_conflicts=True User.groups.through(user_id=membership.user_id, group_id=g.id)
for g in membership.role.linked_groups.all()
) )
return User.groups.through.objects.bulk_create(groups, ignore_conflicts=True)
class Mailing(models.Model): class Mailing(models.Model):
+24 -1
View File
@@ -17,7 +17,7 @@ from club.forms import ClubAddMemberForm, JoinClubForm
from club.models import Club, ClubRole, Membership from club.models import Club, ClubRole, 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
from core.models import AnonymousUser, User from core.models import AnonymousUser, Group, User
class TestMembershipQuerySet(TestClub): class TestMembershipQuerySet(TestClub):
@@ -500,6 +500,29 @@ class TestMembership(TestClub):
assert self.subscriber.groups.contains(self.club.members_group) assert self.subscriber.groups.contains(self.club.members_group)
assert self.subscriber.groups.contains(self.club.board_group) assert self.subscriber.groups.contains(self.club.board_group)
def test_add_to_club_role_group(self):
groups = baker.make(Group, _quantity=4)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:3])
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
assert set(self.subscriber.groups.all()) == {
*groups[:3],
self.club.board_group,
self.club.members_group,
}
def test_remove_from_club_role_group(self):
groups = baker.make(Group, _quantity=3)
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:])
self.subscriber.memberships.update(end_date=localdate())
assert set(self.subscriber.groups.all()) == {groups[0]}
def test_change_position_in_club(self): def test_change_position_in_club(self):
"""Test that when moving from board to members, club group change""" """Test that when moving from board to members, club group change"""
membership = baker.make( membership = baker.make(