mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-10-31 22:24:30 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			366 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			366 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright (C) 2024 by Animath
 | |
| # SPDX-License-Identifier: GPL-3.0-or-later
 | |
| 
 | |
| from asgiref.sync import sync_to_async
 | |
| from django.contrib.auth.models import User
 | |
| from django.db import models
 | |
| from django.db.models import Q, QuerySet
 | |
| from django.utils.text import format_lazy
 | |
| from django.utils.translation import gettext_lazy as _
 | |
| from participation.models import Pool, Team, Tournament
 | |
| from registration.models import ParticipantRegistration, Registration, VolunteerRegistration
 | |
| from tfjm.permissions import PermissionType
 | |
| 
 | |
| 
 | |
| class Channel(models.Model):
 | |
|     """
 | |
|     Ce modèle représente un canal de chat, défini par son nom, sa catégorie, les permissions de lecture et d'écriture
 | |
|     requises pour accéder au canal, et éventuellement un tournoi, une poule ou une équipe associée.
 | |
|     """
 | |
| 
 | |
|     class ChannelCategory(models.TextChoices):
 | |
|         GENERAL = 'general', _("General channels")
 | |
|         TOURNAMENT = 'tournament', _("Tournament channels")
 | |
|         TEAM = 'team', _("Team channels")
 | |
|         PRIVATE = 'private', _("Private channels")
 | |
| 
 | |
|     name = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("name"),
 | |
|         help_text=_("Visible name of the channel."),
 | |
|     )
 | |
| 
 | |
|     category = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("category"),
 | |
|         choices=ChannelCategory,
 | |
|         default=ChannelCategory.GENERAL,
 | |
|         help_text=_("Category of the channel, between general channels, tournament-specific channels, team channels "
 | |
|                     "or private channels. Will be used to sort channels in the channel list."),
 | |
|     )
 | |
| 
 | |
|     read_access = models.CharField(
 | |
|         max_length=16,
 | |
|         verbose_name=_("read permission"),
 | |
|         choices=PermissionType,
 | |
|         help_text=_("Permission type that is required to read the messages of the channels."),
 | |
|     )
 | |
| 
 | |
|     write_access = models.CharField(
 | |
|         max_length=16,
 | |
|         verbose_name=_("write permission"),
 | |
|         choices=PermissionType,
 | |
|         help_text=_("Permission type that is required to write a message to a channel."),
 | |
|     )
 | |
| 
 | |
|     tournament = models.ForeignKey(
 | |
|         'participation.Tournament',
 | |
|         on_delete=models.CASCADE,
 | |
|         blank=True,
 | |
|         null=True,
 | |
|         default=None,
 | |
|         verbose_name=_("tournament"),
 | |
|         related_name='chat_channels',
 | |
|         help_text=_("For a permission that concerns a tournament, indicates what is the concerned tournament."),
 | |
|     )
 | |
| 
 | |
|     pool = models.ForeignKey(
 | |
|         'participation.Pool',
 | |
|         on_delete=models.CASCADE,
 | |
|         blank=True,
 | |
|         null=True,
 | |
|         default=None,
 | |
|         verbose_name=_("pool"),
 | |
|         related_name='chat_channels',
 | |
|         help_text=_("For a permission that concerns a pool, indicates what is the concerned pool."),
 | |
|     )
 | |
| 
 | |
|     team = models.ForeignKey(
 | |
|         'participation.Team',
 | |
|         on_delete=models.CASCADE,
 | |
|         blank=True,
 | |
|         null=True,
 | |
|         default=None,
 | |
|         verbose_name=_("team"),
 | |
|         related_name='chat_channels',
 | |
|         help_text=_("For a permission that concerns a team, indicates what is the concerned team."),
 | |
|     )
 | |
| 
 | |
|     private = models.BooleanField(
 | |
|         verbose_name=_("private"),
 | |
|         default=False,
 | |
|         help_text=_("If checked, only users who have been explicitly added to the channel will be able to access it."),
 | |
|     )
 | |
| 
 | |
|     invited = models.ManyToManyField(
 | |
|         'auth.User',
 | |
|         verbose_name=_("invited users"),
 | |
|         related_name='+',
 | |
|         blank=True,
 | |
|         help_text=_("Extra users who have been invited to the channel, "
 | |
|                     "in addition to the permitted group of the channel."),
 | |
|     )
 | |
| 
 | |
|     def get_visible_name(self, user: User) -> str:
 | |
|         """
 | |
|         Renvoie le nom du channel tel qu'il est visible pour l'utilisateur⋅rice donné.
 | |
|         Dans le cas d'un canal classique, renvoie directement le nom.
 | |
|         Dans le cas d'un canal privé, renvoie la liste des personnes membres du canal,
 | |
|         à l'exception de la personne connectée, afin de ne pas afficher son propre nom.
 | |
|         Dans le cas d'un chat avec uniquement soi-même, on affiche que notre propre nom.
 | |
|         """
 | |
|         if self.private:
 | |
|             # Le canal est privé, on renvoie la liste des personnes membres du canal
 | |
|             # à l'exception de soi-même (sauf si on est la seule personne dans le canal)
 | |
|             users = [f"{u.first_name} {u.last_name}" for u in self.invited.all() if u != user] \
 | |
|                     or [f"{user.first_name} {user.last_name}"]
 | |
|             return ", ".join(users)
 | |
|         # Le canal est public, on renvoie directement le nom
 | |
|         return self.name
 | |
| 
 | |
|     def __str__(self):
 | |
|         return str(format_lazy(_("Channel {name}"), name=self.name))
 | |
| 
 | |
|     @staticmethod
 | |
|     async def get_accessible_channels(user: User, permission_type: str = 'read') -> QuerySet["Channel"]:
 | |
|         """
 | |
|         Renvoie les canaux auxquels l'utilisateur⋅rice donné a accès, en lecture ou en écriture.
 | |
| 
 | |
|         Types de permissions :
 | |
|         ANONYMOUS : Tout le monde, y compris les utilisateur⋅rices non connecté⋅es
 | |
|         AUTHENTICATED : Toustes les utilisateur⋅rices connecté⋅es
 | |
|         VOLUNTEER : Toustes les bénévoles
 | |
|         TOURNAMENT_MEMBER : Toustes les membres d'un tournoi donné (orgas, juré⋅es, participant⋅es)
 | |
|         TOURNAMENT_ORGANIZER : Les organisateur⋅rices d'un tournoi donné
 | |
|         TOURNAMENT_JURY_PRESIDENT : Les organisateur⋅rices et les président⋅es de jury d'un tournoi donné
 | |
|         JURY_MEMBER : Les membres du jury d'une poule donnée, ou les organisateur⋅rices du tournoi
 | |
|         POOL_MEMBER : Les membres du jury et les participant⋅es d'une poule donnée, ou les organisateur⋅rices du tournoi
 | |
|         TEAM_MEMBER : Les membres d'une équipe donnée
 | |
|         PRIVATE : Les utilisateur⋅rices explicitement invité⋅es
 | |
|         ADMIN : Les utilisateur⋅rices administrateur⋅rices (qui ont accès à tout)
 | |
| 
 | |
|         Les canaux privés sont utilisés pour les messages privés, et ne sont pas affichés aux admins.
 | |
| 
 | |
|         :param user: L'utilisateur⋅rice dont on veut récupérer la liste des canaux.
 | |
|         :param permission_type: Le type de permission concerné (read ou write).
 | |
|         :return: Le Queryset des canaux autorisés.
 | |
|         """
 | |
|         permission_type = 'write_access' if 'write' in permission_type.lower() else 'read_access'
 | |
| 
 | |
|         qs = Channel.objects.none()
 | |
|         if user.is_anonymous:
 | |
|             # Les utilisateur⋅rices non connecté⋅es ont accès aux canaux publics pour toustes
 | |
|             return Channel.objects.filter(**{permission_type: PermissionType.ANONYMOUS})
 | |
| 
 | |
|         # Les utilisateur⋅rices connecté⋅es ont accès aux canaux publics pour les personnes connectées
 | |
|         qs |= Channel.objects.filter(**{permission_type: PermissionType.AUTHENTICATED})
 | |
|         registration = await Registration.objects.prefetch_related('user').aget(user_id=user.id)
 | |
| 
 | |
|         if registration.is_admin:
 | |
|             # Les administrateur⋅rices ont accès à tous les canaux, sauf les canaux privés sont iels ne sont pas membres
 | |
|             return Channel.objects.prefetch_related('invited').exclude(~Q(invited=user) & Q(private=True)).all()
 | |
| 
 | |
|         if registration.is_volunteer:
 | |
|             registration = await VolunteerRegistration.objects \
 | |
|                 .prefetch_related('jury_in__tournament', 'organized_tournaments').aget(user_id=user.id)
 | |
| 
 | |
|             # Les bénévoles ont accès aux canaux pour bénévoles
 | |
|             qs |= Channel.objects.filter(**{permission_type: PermissionType.VOLUNTEER})
 | |
| 
 | |
|             # Iels ont accès aux tournois dont iels sont organisateur⋅rices ou juré⋅es
 | |
|             # pour la permission TOURNAMENT_MEMBER
 | |
|             qs |= Channel.objects.filter(Q(tournament__in=registration.interesting_tournaments),
 | |
|                                          **{permission_type: PermissionType.TOURNAMENT_MEMBER})
 | |
| 
 | |
|             # Iels ont accès aux canaux pour les organisateur⋅rices des tournois dont iels sont organisateur⋅rices
 | |
|             # pour la permission TOURNAMENT_ORGANIZER
 | |
|             qs |= Channel.objects.filter(Q(tournament__in=registration.organized_tournaments.all()),
 | |
|                                          **{permission_type: PermissionType.TOURNAMENT_ORGANIZER})
 | |
| 
 | |
|             # Iels ont accès aux canaux pour les organisateur⋅rices et président⋅es de jury des tournois dont iels sont
 | |
|             # organisateur⋅rices ou juré⋅es pour la permission TOURNAMENT_JURY_PRESIDENT
 | |
|             qs |= Channel.objects.filter(Q(tournament__pools__in=registration.pools_presided.all())
 | |
|                                          | Q(tournament__in=registration.organized_tournaments.all()),
 | |
|                                          **{permission_type: PermissionType.TOURNAMENT_JURY_PRESIDENT})
 | |
| 
 | |
|             # Iels ont accès aux canaux pour les juré⋅es des poules dont iels sont juré⋅es
 | |
|             # ou les organisateur⋅rices des tournois dont iels sont organisateur⋅rices
 | |
|             # pour la permission JURY_MEMBER
 | |
|             qs |= Channel.objects.filter(Q(pool__in=registration.jury_in.all())
 | |
|                                          | Q(pool__tournament__in=registration.organized_tournaments.all())
 | |
|                                          | Q(pool__tournament__pools__in=registration.pools_presided.all()),
 | |
|                                          **{permission_type: PermissionType.JURY_MEMBER})
 | |
| 
 | |
|             # Iels ont accès aux canaux pour les juré⋅es et participant⋅es des poules dont iels sont juré⋅es
 | |
|             # ou les organisateur⋅rices des tournois dont iels sont organisateur⋅rices
 | |
|             # pour la permission POOL_MEMBER
 | |
|             qs |= Channel.objects.filter(Q(pool__in=registration.jury_in.all())
 | |
|                                          | Q(pool__tournament__in=registration.organized_tournaments.all())
 | |
|                                          | Q(pool__tournament__pools__in=registration.pools_presided.all()),
 | |
|                                          **{permission_type: PermissionType.POOL_MEMBER})
 | |
|         else:
 | |
|             registration = await ParticipantRegistration.objects \
 | |
|                 .prefetch_related('team__participation__pools', 'team__participation__tournament').aget(user_id=user.id)
 | |
| 
 | |
|             team = registration.team
 | |
|             tournaments = []
 | |
|             if team.participation.valid:
 | |
|                 tournaments.append(team.participation.tournament)
 | |
|             if team.participation.final:
 | |
|                 tournaments.append(await Tournament.objects.aget(final=True))
 | |
| 
 | |
|             # Les participant⋅es ont accès aux canaux généraux pour le tournoi dont iels sont membres
 | |
|             # Cela comprend la finale s'iels sont finalistes
 | |
|             qs |= Channel.objects.filter(Q(tournament__in=tournaments),
 | |
|                                          **{permission_type: PermissionType.TOURNAMENT_MEMBER})
 | |
| 
 | |
|             # Iels ont accès aux canaux généraux pour les poules dont iels sont participant⋅es
 | |
|             qs |= Channel.objects.filter(Q(pool__in=team.participation.pools.all()),
 | |
|                                          **{permission_type: PermissionType.POOL_MEMBER})
 | |
| 
 | |
|             # Iels ont accès aux canaux propres à leur équipe
 | |
|             qs |= Channel.objects.filter(Q(team=team),
 | |
|                                          **{permission_type: PermissionType.TEAM_MEMBER})
 | |
| 
 | |
|         # Les utilisateur⋅rices ont de plus accès aux messages privés qui leur sont adressés
 | |
|         qs |= Channel.objects.filter(invited=user).prefetch_related('invited')
 | |
| 
 | |
|         return qs
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("channel")
 | |
|         verbose_name_plural = _("channels")
 | |
|         ordering = ('category', 'name',)
 | |
| 
 | |
| 
 | |
| class Message(models.Model):
 | |
|     """
 | |
|     Ce modèle représente un message de chat.
 | |
|     Un message appartient à un canal, et est défini par son contenu, son auteur⋅rice, sa date de création et sa date
 | |
|     de dernière modification.
 | |
|     De plus, on garde en mémoire les utilisateur⋅rices qui ont lu le message.
 | |
|     """
 | |
|     channel = models.ForeignKey(
 | |
|         Channel,
 | |
|         on_delete=models.CASCADE,
 | |
|         verbose_name=_("channel"),
 | |
|         related_name='messages',
 | |
|     )
 | |
| 
 | |
|     author = models.ForeignKey(
 | |
|         'auth.User',
 | |
|         verbose_name=_("author"),
 | |
|         on_delete=models.SET_NULL,
 | |
|         null=True,
 | |
|         related_name='chat_messages',
 | |
|     )
 | |
| 
 | |
|     created_at = models.DateTimeField(
 | |
|         verbose_name=_("created at"),
 | |
|         auto_now_add=True,
 | |
|     )
 | |
| 
 | |
|     updated_at = models.DateTimeField(
 | |
|         verbose_name=_("updated at"),
 | |
|         auto_now=True,
 | |
|     )
 | |
| 
 | |
|     content = models.TextField(
 | |
|         verbose_name=_("content"),
 | |
|     )
 | |
| 
 | |
|     users_read = models.ManyToManyField(
 | |
|         'auth.User',
 | |
|         verbose_name=_("users read"),
 | |
|         related_name='+',
 | |
|         blank=True,
 | |
|         help_text=_("Users who have read the message."),
 | |
|     )
 | |
| 
 | |
|     def get_author_name(self) -> str:
 | |
|         """
 | |
|         Renvoie le nom de l'auteur⋅rice du message, en fonction de son rôle dans l'organisation
 | |
|         dans le cadre d'un⋅e bénévole, ou de son équipe dans le cadre d'un⋅e participant⋅e.
 | |
|         """
 | |
|         registration = self.author.registration
 | |
| 
 | |
|         author_name = f"{self.author.first_name} {self.author.last_name}"
 | |
|         if registration.is_volunteer:
 | |
|             if registration.is_admin:
 | |
|                 # Les administrateur⋅rices ont le suffixe (CNO)
 | |
|                 author_name += " (CNO)"
 | |
| 
 | |
|             if self.channel.pool:
 | |
|                 if registration == self.channel.pool.jury_president:
 | |
|                     # Læ président⋅e de jury de la poule a le suffixe (P. jury)
 | |
|                     author_name += " (P. jury)"
 | |
|                 elif registration in self.channel.pool.juries.all():
 | |
|                     # Les juré⋅es de la poule ont le suffixe (Juré⋅e)
 | |
|                     author_name += " (Juré⋅e)"
 | |
|                 elif registration in self.channel.pool.tournament.organizers.all():
 | |
|                     # Les organisateur⋅rices du tournoi ont le suffixe (CRO)
 | |
|                     author_name += " (CRO)"
 | |
|                 else:
 | |
|                     # Les éventuel⋅les autres bénévoles ont le suffixe (Bénévole)
 | |
|                     author_name += " (Bénévole)"
 | |
|             elif self.channel.tournament:
 | |
|                 if registration in self.channel.tournament.organizers.all():
 | |
|                     # Les organisateur⋅rices du tournoi ont le suffixe (CRO)
 | |
|                     author_name += " (CRO)"
 | |
|                 elif any([registration.id == pool.jury_president
 | |
|                           for pool in self.channel.tournament.pools.all()]):
 | |
|                     # Les président⋅es de jury des poules ont le suffixe (P. jury)
 | |
|                     # mentionnant l'ensemble des poules qu'iels président
 | |
|                     pools = ", ".join([pool.short_name
 | |
|                                        for pool in self.channel.tournament.pools.all()
 | |
|                                        if pool.jury_president == registration])
 | |
|                     author_name += f" (P. jury {pools})"
 | |
|                 elif any([pool.juries.contains(registration)
 | |
|                           for pool in self.channel.tournament.pools.all()]):
 | |
|                     # Les juré⋅es des poules ont le suffixe (Juré⋅e)
 | |
|                     # mentionnant l'ensemble des poules auxquelles iels participent
 | |
|                     pools = ", ".join([pool.short_name
 | |
|                                        for pool in self.channel.tournament.pools.all()
 | |
|                                        if pool.juries.acontains(registration)])
 | |
|                     author_name += f" (Juré⋅e {pools})"
 | |
|                 else:
 | |
|                     # Les éventuel⋅les autres bénévoles ont le suffixe (Bénévole)
 | |
|                     author_name += " (Bénévole)"
 | |
|             else:
 | |
|                 if registration.organized_tournaments.exists():
 | |
|                     # Les organisateur⋅rices de tournois ont le suffixe (CRO) mentionnant les tournois organisés
 | |
|                     tournaments = ", ".join([tournament.name
 | |
|                                              for tournament in registration.organized_tournaments.all()])
 | |
|                     author_name += f" (CRO {tournaments})"
 | |
|                 if Pool.objects.filter(jury_president=registration).exists():
 | |
|                     # Les président⋅es de jury ont le suffixe (P. jury) mentionnant les tournois présidés
 | |
|                     tournaments = Tournament.objects.filter(pools__jury_president=registration).distinct()
 | |
|                     tournaments = ", ".join([tournament.name for tournament in tournaments])
 | |
|                     author_name += f" (P. jury {tournaments})"
 | |
|                 elif registration.jury_in.exists():
 | |
|                     # Les juré⋅es ont le suffixe (Juré⋅e) mentionnant les tournois auxquels iels participent
 | |
|                     tournaments = Tournament.objects.filter(pools__juries=registration).distinct()
 | |
|                     tournaments = ", ".join([tournament.name for tournament in tournaments])
 | |
|                     author_name += f" (Juré⋅e {tournaments})"
 | |
|         else:
 | |
|             if registration.team_id:
 | |
|                 # Le trigramme de l'équipe de læ participant⋅e est ajouté en suffixe
 | |
|                 team = Team.objects.get(id=registration.team_id)
 | |
|                 author_name += f" ({team.trigram})"
 | |
|             else:
 | |
|                 author_name += " (sans équipe)"
 | |
| 
 | |
|         return author_name
 | |
| 
 | |
|     async def aget_author_name(self) -> str:
 | |
|         """
 | |
|         Fonction asynchrone pour récupérer le nom de l'auteur⋅rice du message.
 | |
|         Voir `get_author_name` pour plus de détails.
 | |
|         """
 | |
|         return await sync_to_async(self.get_author_name)()
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("message")
 | |
|         verbose_name_plural = _("messages")
 | |
|         ordering = ('created_at',)
 |