diff --git a/club/forms.py b/club/forms.py index dcd270e7..a18851a0 100644 --- a/club/forms.py +++ b/club/forms.py @@ -23,13 +23,12 @@ # from django import forms -from django.conf import settings -from django.db.models import Exists, OuterRef, Q +from django.db.models import Exists, OuterRef, Q, QuerySet from django.db.models.functions import Lower from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from club.models import Club, Mailing, MailingSubscription, Membership +from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership from core.models import User from core.views.forms import SelectDateTime from core.views.widgets.ajax_select import ( @@ -215,9 +214,7 @@ class ClubOldMemberForm(forms.Form): def __init__(self, *args, user: User, club: Club, **kwargs): super().__init__(*args, **kwargs) - self.fields["members_old"].queryset = ( - Membership.objects.ongoing().filter(club=club).editable_by(user) - ) + self.fields["members_old"].queryset = club.members.ongoing().editable_by(user) class ClubMemberForm(forms.ModelForm): @@ -235,19 +232,14 @@ class ClubMemberForm(forms.ModelForm): self.request_user = request_user self.request_user_membership = self.club.get_membership_for(self.request_user) super().__init__(*args, **kwargs) - self.fields["role"].required = True - self.fields["role"].choices = [ - (value, name) - for value, name in settings.SITH_CLUB_ROLES.items() - if value <= self.max_available_role - ] + self.fields["role"].queryset = self.available_roles self.instance.club = club @property - def max_available_role(self): + def available_roles(self) -> QuerySet[ClubRole]: """The greatest role that will be obtainable with this form.""" # this is unreachable, because it will be overridden by subclasses - return -1 # pragma: no cover + return ClubRole.objects.none() # pragma: no cover class ClubAddMemberForm(ClubMemberForm): @@ -258,7 +250,7 @@ class ClubAddMemberForm(ClubMemberForm): widgets = {"user": AutoCompleteSelectUser} @cached_property - def max_available_role(self): + def available_roles(self): """The greatest role that will be obtainable with this form. Admins and the club president can attribute any role. @@ -266,13 +258,13 @@ class ClubAddMemberForm(ClubMemberForm): Other users cannot attribute roles with this form """ if self.request_user.has_perm("club.add_membership"): - return settings.SITH_CLUB_ROLES_ID["President"] + return self.club.roles.all() membership = self.request_user_membership - 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 + if membership is None or not membership.role.is_board: + return ClubRole.objects.none() + if membership.role.is_presidency: + return self.club.roles.all() + return self.club.roles.above_instance(membership.role) def clean_user(self): """Check that the user is not trying to add a user already in the club. @@ -296,13 +288,11 @@ class JoinClubForm(ClubMemberForm): def __init__(self, *args, club: Club, request_user: User, **kwargs): super().__init__(*args, club=club, request_user=request_user, **kwargs) - # this form doesn't manage the user who will join the club, - # so we must set this here to avoid errors self.instance.user = self.request_user @cached_property - def max_available_role(self): - return settings.SITH_MAXIMUM_FREE_ROLE + def available_roles(self): + return self.club.roles.filter(is_board=False) def clean(self): """Check that the user is subscribed and isn't already in the club.""" diff --git a/club/migrations/0015_clubrole_alter_membership_role.py b/club/migrations/0015_clubrole_alter_membership_role.py index 083b9760..89eadd7f 100644 --- a/club/migrations/0015_clubrole_alter_membership_role.py +++ b/club/migrations/0015_clubrole_alter_membership_role.py @@ -5,24 +5,33 @@ from django.conf import settings from django.db import migrations, models from django.db.migrations.state import StateApps from django.db.models import Case, When -from django.utils import translation + +PRESIDENT_ROLE = 10 +SITH_CLUB_ROLES = { + 10: "Président⸱e", + 9: "Vice-Président⸱e", + 7: "Trésorier⸱e", + 5: "Responsable communication", + 4: "Secrétaire", + 3: "Responsable info", + 2: "Membre du bureau", + 1: "Membre actif⸱ve", + 0: "Curieux⸱euse", +} def migrate_roles(apps: StateApps, schema_editor): ClubRole = apps.get_model("club", "ClubRole") Membership = apps.get_model("club", "Membership") - translation.activate("fr") - updates = [] - presidency = settings.SITH_CLUB_ROLES_ID["President"] for club_id, role in Membership.objects.values_list("club", "role").distinct(): new_role = ClubRole.objects.create( name=SITH_CLUB_ROLES[role], is_board=role > settings.SITH_MAXIMUM_FREE_ROLE, - is_presidency=role == presidency, + is_presidency=role == PRESIDENT_ROLE, club_id=club_id, - order=presidency - role, + order=PRESIDENT_ROLE - role, ) updates.append(When(role=role, then=new_role.id)) # all updates must happen at the same time diff --git a/club/models.py b/club/models.py index edd86d7e..1e75bbf1 100644 --- a/club/models.py +++ b/club/models.py @@ -138,9 +138,7 @@ class Club(models.Model): @cached_property def president(self) -> Membership | None: """Fetch the membership of the current president of this club.""" - return self.members.filter( - role=settings.SITH_CLUB_ROLES_ID["President"], end_date=None - ).first() + return self.members.filter(end_date=None).order_by("role__order").first() def check_loop(self): """Raise a validation error when a loop is found within the parent list.""" @@ -208,7 +206,9 @@ class Club(models.Model): @cached_property def current_members(self) -> list[Membership]: - return list(self.members.ongoing().select_related("user").order_by("-role")) + return list( + self.members.ongoing().select_related("user", "role").order_by("-role") + ) def get_membership_for(self, user: User) -> Membership | None: """Return the current membership of the given user.""" @@ -256,7 +256,7 @@ class ClubRole(OrderedModel): ] def __str__(self): - return f"{self.name} - {self.club.name}" + return self.name def get_display_name(self): return f"{self.name} - {self.club.name}" @@ -265,14 +265,29 @@ class ClubRole(OrderedModel): return reverse("club:club_roles", kwargs={"club_id": self.club_id}) def clean(self): + errors = [] if self.is_presidency and not self.is_board: - raise ValidationError( - _( - "Role %(name)s was declared as a presidency role " - "without being a board role" + errors.append( + ValidationError( + _( + "Role %(name)s was declared as a presidency role " + "without being a board role" + ) + % {"name": self.name} ) - % {"name": self.name} ) + if ( + self.is_board + and self.club.roles.filter(is_board=False, order__lt=self.order).exists() + ): + errors.append( + ValidationError( + _("Board role %(role)s cannot be placed below a member role") + % {"role": self.name} + ) + ) + if errors: + raise ValidationError(errors) return super().clean() @@ -321,7 +336,7 @@ class MembershipQuerySet(models.QuerySet): user=user, club=OuterRef("club"), role__is_board=True, - role__order__gt=OuterRef("role__order"), + role__order__lt=OuterRef("role__order"), ) ) ) diff --git a/club/templates/club/club_members.jinja b/club/templates/club/club_members.jinja index 3aa43d56..18c14e84 100644 --- a/club/templates/club/club_members.jinja +++ b/club/templates/club/club_members.jinja @@ -45,7 +45,7 @@ {% for m in members %}