plateforme-tfjm2/chat/models.py

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',)