# Copyright (C) 2020 by Animath # SPDX-License-Identifier: GPL-3.0-or-later import os from address.models import AddressField from django.conf import settings from django.core.validators import RegexValidator from django.db import models from django.db.models import Index from django.urls import reverse_lazy from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from registration.models import VolunteerRegistration from tfjm.lists import get_sympa_client from tfjm.matrix import Matrix, RoomPreset, RoomVisibility class Team(models.Model): """ The Team model represents a real team that participates to the Correspondances. This only includes the registration detail. """ name = models.CharField( max_length=255, verbose_name=_("name"), unique=True, ) trigram = models.CharField( max_length=3, verbose_name=_("trigram"), help_text=_("The trigram must be composed of three uppercase letters."), unique=True, validators=[RegexValidator("[A-Z]{3}")], ) access_code = models.CharField( max_length=6, verbose_name=_("access code"), help_text=_("The access code let other people to join the team."), ) @property def students(self): return self.participants.filter(studentregistration__isnull=False) @property def coachs(self): return self.participants.filter(coachregistration__isnull=False) @property def email(self): """ :return: The mailing list to contact the team members. """ return f"equipe-{self.trigram.lower()}@{os.getenv('SYMPA_HOST', 'localhost')}" def create_mailing_list(self): """ Create a new Sympa mailing list to contact the team. """ get_sympa_client().create_list( f"equipe-{self.trigram.lower()}", f"Équipe {self.name} ({self.trigram})", "hotline", # TODO Use a custom sympa template f"Liste de diffusion pour contacter l'équipe {self.name} du TFJM²", "education", raise_error=False, ) if self.pk and self.participation.valid: # pragma: no cover get_sympa_client().subscribe(self.email, "equipes", False, f"Equipe {self.name}") get_sympa_client().subscribe(self.email, f"probleme-{self.participation.problem}", False, f"Equipe {self.name}") else: get_sympa_client().subscribe(self.email, "equipes-non-valides", False) def delete_mailing_list(self): """ Drop the Sympa mailing list, if the team is empty or if the trigram changed. """ if self.participation.valid: # pragma: no cover get_sympa_client().unsubscribe(self.email, "equipes", False) get_sympa_client().unsubscribe(self.email, f"probleme-{self.participation.problem}", False) else: get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False) get_sympa_client().delete_list(f"equipe-{self.trigram}") def save(self, *args, **kwargs): if not self.access_code: # if the team got created, generate the access code, create the contact mailing list # and create a dedicated Matrix room. self.access_code = get_random_string(6) self.create_mailing_list() Matrix.create_room( visibility=RoomVisibility.private, name=f"#équipe-{self.trigram.lower()}", alias=f"equipe-{self.trigram.lower()}", topic=f"Discussion de l'équipe {self.name}", preset=RoomPreset.private_chat, ) return super().save(*args, **kwargs) def get_absolute_url(self): return reverse_lazy("participation:team_detail", args=(self.pk,)) def __str__(self): return _("Team {name} ({trigram})").format(name=self.name, trigram=self.trigram) class Meta: verbose_name = _("team") verbose_name_plural = _("teams") indexes = [ Index(fields=("trigram", )), ] class Tournament(models.Model): name = models.CharField( max_length=255, verbose_name=_("name"), unique=True, ) date_start = models.DateField( verbose_name=_("start"), default=timezone.now, ) date_end = models.DateField( verbose_name=_("end"), default=timezone.now, ) place = AddressField( verbose_name=_("place"), ) max_teams = models.PositiveSmallIntegerField( verbose_name=_("max team count"), default=9, ) price = models.PositiveSmallIntegerField( verbose_name=_("price"), default=21, ) inscription_limit = models.DateTimeField( verbose_name=_("limit date for registrations"), default=timezone.now, ) solution_limit = models.DateTimeField( verbose_name=_("limit date to upload solutions"), default=timezone.now, ) solutions_draw = models.DateTimeField( verbose_name=_("random draw for solutions"), default=timezone.now, ) syntheses_first_phase_limit = models.DateTimeField( verbose_name=_("limit date to upload the syntheses for the first phase"), default=timezone.now, ) solutions_available_second_phase = models.DateTimeField( verbose_name=_("date when the solutions for the second round become available"), default=timezone.now, ) syntheses_second_phase_limit = models.DateTimeField( verbose_name=_("limit date to upload the syntheses for the second phase"), default=timezone.now, ) description = models.TextField( verbose_name=_("description"), blank=True, ) organizers = models.ManyToManyField( VolunteerRegistration, verbose_name=_("organizers"), related_name="organized_tournaments", ) final = models.BooleanField( verbose_name=_("final"), default=False, ) @staticmethod def final_tournament(): qs = Tournament.objects.filter(final=True) if qs.exists(): return qs.get() @property def participations(self): if self.final: return Participation.objects.filter(final=True) return self.participation_set @property def solutions(self): if self.final: return Solution.objects.filter(final_solution=True) return Solution.objects.filter(participation__tournament=self) @property def syntheses(self): if self.final: return Synthesis.objects.filter(final_solution=True) return Synthesis.objects.filter(participation__tournament=self) def get_absolute_url(self): return reverse_lazy("participation:tournament_detail", args=(self.pk,)) def __str__(self): return self.name class Meta: verbose_name = _("tournament") verbose_name_plural = _("tournaments") indexes = [ Index(fields=("name", "date_start", "date_end", )), ] class Participation(models.Model): """ The Participation model contains all data that are related to the participation: chosen problem, validity status, solutions,... """ team = models.OneToOneField( Team, on_delete=models.CASCADE, verbose_name=_("team"), ) tournament = models.ForeignKey( Tournament, on_delete=models.SET_NULL, null=True, blank=True, default=None, verbose_name=_("tournament"), ) valid = models.BooleanField( null=True, default=None, verbose_name=_("valid"), help_text=_("The participation got the validation of the organizers."), ) final = models.BooleanField( default=False, verbose_name=_("selected for final"), help_text=_("The team is selected for the final tournament."), ) def get_absolute_url(self): return reverse_lazy("participation:participation_detail", args=(self.pk,)) def __str__(self): return _("Participation of the team {name} ({trigram})").format(name=self.team.name, trigram=self.team.trigram) class Meta: verbose_name = _("participation") verbose_name_plural = _("participations") class Pool(models.Model): tournament = models.ForeignKey( Tournament, on_delete=models.CASCADE, related_name="pools", verbose_name=_("tournament"), ) round = models.PositiveSmallIntegerField( verbose_name=_("round"), ) participations = models.ManyToManyField( Participation, related_name="pools", verbose_name=_("participations"), ) juries = models.ManyToManyField( VolunteerRegistration, related_name="jury_in", verbose_name=_("juries"), ) @property def solutions(self): return Solution.objects.filter(participation__in=self.participations, final_solution=self.tournament.final) def __str__(self): return _("Pool {round} for tournament {tournament} with teams {teams}")\ .format(round=self.round, tournament=str(self.tournament), teams=", ".join(participation.team.trigram for participation in self.participations.all())) class Meta: verbose_name = _("pool") verbose_name_plural = _("pools") def get_solution_filename(instance, filename): return f"solutions/{instance.participation.team.trigram}_{instance.problem}" \ + ("final" if instance.final_solution else "") def get_random_synthesis_filename(instance, filename): return "syntheses/" + get_random_string(64) class Solution(models.Model): participation = models.ForeignKey( Participation, on_delete=models.CASCADE, verbose_name=_("participation"), related_name="solutions", ) problem = models.PositiveSmallIntegerField( verbose_name=_("problem"), choices=[ (i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1) ], ) final_solution = models.BooleanField( verbose_name=_("solution for the final tournament"), default=False, ) file = models.FileField( verbose_name=_("file"), upload_to=get_solution_filename, unique=True, blank=True, default="", ) def __str__(self): return _("Solution of team {team} for problem {problem}")\ .format(team=self.participation.team.name, problem=self.problem) class Meta: verbose_name = _("solution") verbose_name_plural = _("solutions") unique_together = (('participation', 'problem', 'final_solution', ), ) class Synthesis(models.Model): participation = models.ForeignKey( Participation, on_delete=models.CASCADE, verbose_name=_("participation"), ) pool = models.ForeignKey( Pool, on_delete=models.CASCADE, related_name="syntheses", verbose_name=_("pool"), ) type = models.PositiveSmallIntegerField( choices=[ (1, _("opponent"), ), (2, _("reporter"), ), ] ) file = models.FileField( verbose_name=_("file"), upload_to=get_random_synthesis_filename, unique=True, blank=True, default="", ) def __str__(self): return repr(self) class Meta: verbose_name = _("synthesis") verbose_name_plural = _("syntheses") unique_together = (('participation', 'pool', 'type', ), )