Aller au contenu

Models

Club

Bases: Model

The Club class, made as a tree to allow nice tidy organization.

president()

Fetch the membership of the current president of this club.

Source code in club/models.py
@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()

check_loop()

Raise a validation error when a loop is found within the parent list.

Source code in club/models.py
def check_loop(self):
    """Raise a validation error when a loop is found within the parent list."""
    objs = []
    cur = self
    while cur.parent is not None:
        if cur in objs:
            raise ValidationError(_("You can not make loops in clubs"))
        objs.append(cur)
        cur = cur.parent

is_owned_by(user)

Method to see if that object can be super edited by the given user.

Source code in club/models.py
def is_owned_by(self, user: User) -> bool:
    """Method to see if that object can be super edited by the given user."""
    if user.is_anonymous:
        return False
    return user.is_root or user.is_board_member

can_be_edited_by(user)

Method to see if that object can be edited by the given user.

Source code in club/models.py
def can_be_edited_by(self, user: User) -> bool:
    """Method to see if that object can be edited by the given user."""
    return self.has_rights_in_club(user)

can_be_viewed_by(user)

Method to see if that object can be seen by the given user.

Source code in club/models.py
def can_be_viewed_by(self, user: User) -> bool:
    """Method to see if that object can be seen by the given user."""
    return user.was_subscribed

get_membership_for(user)

Return the current membership the given user.

Note

The result is cached.

Source code in club/models.py
def get_membership_for(self, user: User) -> Membership | None:
    """Return the current membership the given user.

    Note:
        The result is cached.
    """
    if user.is_anonymous:
        return None
    membership = cache.get(f"membership_{self.id}_{user.id}")
    if membership == "not_member":
        return None
    if membership is None:
        membership = self.members.filter(user=user, end_date=None).first()
        if membership is None:
            cache.set(f"membership_{self.id}_{user.id}", "not_member")
        else:
            cache.set(f"membership_{self.id}_{user.id}", membership)
    return membership

MembershipQuerySet

Bases: QuerySet

ongoing()

Filter all memberships which are not finished yet.

Source code in club/models.py
def ongoing(self) -> Self:
    """Filter all memberships which are not finished yet."""
    return self.filter(Q(end_date=None) | Q(end_date__gt=localdate()))

board()

Filter all memberships where the user is/was in the board.

Be aware that users who were in the board in the past are included, even if there are no more members.

If you want to get the users who are currently in the board, mind combining this with the :meth:ongoing queryset method

Source code in club/models.py
def board(self) -> Self:
    """Filter all memberships where the user is/was in the board.

    Be aware that users who were in the board in the past
    are included, even if there are no more members.

    If you want to get the users who are currently in the board,
    mind combining this with the :meth:`ongoing` queryset method
    """
    return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)

update(**kwargs)

Refresh the cache and edit group ownership.

Update the cache, when necessary, remove users from club groups they are no more in and add them in the club groups they should be in.

Be aware that this adds three db queries : one to retrieve the updated memberships, one to perform group removal and one to perform group attribution.

Source code in club/models.py
def update(self, **kwargs) -> int:
    """Refresh the cache and edit group ownership.

    Update the cache, when necessary, remove
    users from club groups they are no more in
    and add them in the club groups they should be in.

    Be aware that this adds three db queries :
    one to retrieve the updated memberships,
    one to perform group removal and one to perform
    group attribution.
    """
    nb_rows = super().update(**kwargs)
    if nb_rows == 0:
        # if no row was affected, no need to refresh the cache
        return 0

    cache_memberships = {}
    memberships = set(self.select_related("club"))
    # delete all User-Group relations and recreate the necessary ones
    # It's more concise to write and more reliable
    Membership._remove_club_groups(memberships)
    Membership._add_club_groups(memberships)
    for member in memberships:
        cache_key = f"membership_{member.club_id}_{member.user_id}"
        if member.end_date is None:
            cache_memberships[cache_key] = member
        else:
            cache_memberships[cache_key] = "not_member"
    cache.set_many(cache_memberships)
    return nb_rows

delete()

Work just like the default Django's delete() method, but add a cache invalidation for the elements of the queryset before the deletion, and a removal of the user from the club groups.

Be aware that this adds some db queries :

  • 1 to retrieve the deleted elements in order to perform post-delete operations. As we can't know if a delete will affect rows or not, this query will always happen
  • 1 query to remove the users from the club groups. If the delete operation affected no row, this query won't happen.
Source code in club/models.py
def delete(self) -> tuple[int, dict[str, int]]:
    """Work just like the default Django's delete() method,
    but add a cache invalidation for the elements of the queryset
    before the deletion,
    and a removal of the user from the club groups.

    Be aware that this adds some db queries :

    - 1 to retrieve the deleted elements in order to perform
      post-delete operations.
      As we can't know if a delete will affect rows or not,
      this query will always happen
    - 1 query to remove the users from the club groups.
      If the delete operation affected no row,
      this query won't happen.
    """
    memberships = set(self.all())
    nb_rows, rows_counts = super().delete()
    if nb_rows > 0:
        Membership._remove_club_groups(memberships)
        cache.set_many(
            {
                f"membership_{m.club_id}_{m.user_id}": "not_member"
                for m in memberships
            }
        )
    return nb_rows, rows_counts

Membership

Bases: Model

The Membership class makes the connection between User and Clubs.

Both Users and Clubs can have many Membership objects
  • a user can be a member of many clubs at a time
  • a club can have many members at a time too

A User is currently member of all the Clubs where its Membership has an end_date set to null/None. Otherwise, it's a past membership kept because it can be very useful to see who was in which Club in the past.

is_owned_by(user)

Method to see if that object can be super edited by the given user.

Source code in club/models.py
def is_owned_by(self, user: User) -> bool:
    """Method to see if that object can be super edited by the given user."""
    if user.is_anonymous:
        return False
    return user.is_root or user.is_board_member

can_be_edited_by(user)

Check if that object can be edited by the given user.

Source code in club/models.py
def can_be_edited_by(self, user: User) -> bool:
    """Check if that object can be edited by the given user."""
    if user.is_root or user.is_board_member:
        return True
    membership = self.club.get_membership_for(user)
    return membership is not None and membership.role >= self.role

_remove_club_groups(memberships) staticmethod

Remove users of those memberships from the club groups.

For example, if a user is in the Troll club board, he is in the board group and the members group of the Troll. After calling this function, he will be in neither.

Returns:

Type Description
tuple[int, dict[str, int]]

The result of the deletion queryset.

Source code in club/models.py
@staticmethod
def _remove_club_groups(
    memberships: Iterable[Membership],
) -> tuple[int, dict[str, int]]:
    """Remove users of those memberships from the club groups.

    For example, if a user is in the Troll club board,
    he is in the board group and the members group of the Troll.
    After calling this function, he will be in neither.

    Returns:
        The result of the deletion queryset.

    Warnings:
        If this function isn't used in combination
        with an actual deletion of the memberships,
        it will result in an inconsistent state,
        where users will be in the clubs, without
        having the associated rights.
    """
    clubs = {m.club_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))
    return User.groups.through.objects.filter(
        Q(group__in=groups) & Q(user__in=users)
    ).delete()

_add_club_groups(memberships) staticmethod

Add users of those memberships to the club groups.

For example, if a user just joined the Troll club board, he will be added in both the members group and the board group of the club.

Returns:

Type Description
list[through]

The created User-Group relations.

Source code in club/models.py
@staticmethod
def _add_club_groups(
    memberships: Iterable[Membership],
) -> list[User.groups.through]:
    """Add users of those memberships to the club groups.

    For example, if a user just joined the Troll club board,
    he will be added in both the members group and the board group
    of the club.

    Returns:
        The created User-Group relations.

    Warnings:
        If this function isn't used in combination
        with an actual update/creation of the memberships,
        it will result in an inconsistent state,
        where users will have the rights associated to the
        club, without actually being part of it.
    """
    # only active membership (i.e. `end_date=None`)
    # grant the attribution of club groups.
    memberships = [m for m in memberships if m.end_date is None]
    if not memberships:
        return []

    if sum(1 for m in memberships if not hasattr(m, "club")) > 1:
        # if more than one membership hasn't its `club` attribute set
        # it's less expensive to reload the whole query with
        # a select_related than perform a distinct query
        # to fetch each club.
        ids = {m.id for m in memberships}
        memberships = list(
            Membership.objects.filter(id__in=ids).select_related("club")
        )
    club_groups = []
    for membership in memberships:
        club_groups.append(
            User.groups.through(
                user_id=membership.user_id,
                group_id=membership.club.members_group_id,
            )
        )
        if membership.role > settings.SITH_MAXIMUM_FREE_ROLE:
            club_groups.append(
                User.groups.through(
                    user_id=membership.user_id,
                    group_id=membership.club.board_group_id,
                )
            )
    return User.groups.through.objects.bulk_create(
        club_groups, ignore_conflicts=True
    )

Mailing

Bases: Model

A Mailing list for a club.

Warning

Remember that mailing lists should be validated by UTBM.

MailingSubscription

Bases: Model

Link between user and mailing list.