import os import re from corres2math.lists import get_sympa_client from corres2math.matrix import Matrix, RoomPreset, RoomVisibility from django.core.exceptions import ObjectDoesNotExist from django.core.validators import RegexValidator from django.db import models from django.db.models import Index from django.template.loader import render_to_string 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 _ 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."), ) grant_animath_access_videos = models.BooleanField( verbose_name=_("Grant Animath to publish my video"), help_text=_("Give the authorisation to publish the video on the main website to promote the action."), default=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} des Correspondances", "education", raise_error=False, ) if self.pk and self.participation.valid: 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: 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 Participation(models.Model): """ The Participation model contains all data that are related to the participation: chosen problem, validity status, videos,... """ team = models.OneToOneField( Team, on_delete=models.CASCADE, verbose_name=_("team"), ) problem = models.IntegerField( choices=[(i, format_lazy(_("Problem #{problem:d}"), problem=i)) for i in range(1, 4)], null=True, default=None, verbose_name=_("problem number"), ) valid = models.BooleanField( null=True, default=None, verbose_name=_("valid"), help_text=_("The video got the validation of the administrators."), ) solution = models.OneToOneField( "participation.Video", on_delete=models.SET_NULL, related_name="participation_solution", null=True, default=None, verbose_name=_("solution video"), ) received_participation = models.OneToOneField( "participation.Participation", on_delete=models.PROTECT, related_name="sent_participation", null=True, default=None, verbose_name=_("received participation"), ) synthesis = models.OneToOneField( "participation.Video", on_delete=models.SET_NULL, related_name="participation_synthesis", null=True, default=None, verbose_name=_("synthesis video"), ) 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 Video(models.Model): """ The Video model only contains a link and a validity status. """ link = models.URLField( verbose_name=_("link"), help_text=_("The full video link."), ) valid = models.BooleanField( null=True, default=None, verbose_name=_("valid"), help_text=_("The video got the validation of the administrators."), ) @property def participation(self): """ Retrives the participation that is associated to this video, whatever it is a solution or a synthesis. """ try: # If this is a solution return self.participation_solution except ObjectDoesNotExist: # If this is a synthesis return self.participation_synthesis @property def platform(self): """ According to the link, retrieve the platform that is used to upload the video. """ if "youtube.com" in self.link or "youtu.be" in self.link: return "youtube" return "unknown" @property def youtube_code(self): """ If the video is uploaded on Youtube, search in the URL the video code. """ return re.compile("(https?://|)(www\\.|)(youtube\\.com/watch\\?v=|youtu\\.be/)([a-zA-Z0-9-_]*)?.*?")\ .match(self.link).group(4) def as_iframe(self): """ Generate the HTML code to embed the video in an iframe, according to the type of the host platform. """ if self.platform == "youtube": return render_to_string("participation/youtube_iframe.html", context=dict(youtube_code=self.youtube_code)) return None def __str__(self): return _("Video of team {name} ({trigram})")\ .format(name=self.participation.team.name, trigram=self.participation.team.trigram) class Meta: verbose_name = _("video") verbose_name_plural = _("videos") class Question(models.Model): """ Question to ask to the team that sent a solution. """ participation = models.ForeignKey( Participation, on_delete=models.CASCADE, verbose_name=_("participation"), related_name="questions", ) question = models.TextField( verbose_name=_("question"), ) def __str__(self): return self.question class Phase(models.Model): """ The Phase model corresponds to the dates of the phase. """ phase_number = models.AutoField( primary_key=True, unique=True, verbose_name=_("phase number"), ) description = models.CharField( max_length=255, verbose_name=_("phase description"), ) start = models.DateTimeField( verbose_name=_("start date of the given phase"), default=timezone.now, ) end = models.DateTimeField( verbose_name=_("end date of the given phase"), default=timezone.now, ) @classmethod def current_phase(cls): """ Retrieve the current phase of this day """ qs = Phase.objects.filter(start__lte=timezone.now(), end__gte=timezone.now()) if qs.exists(): return qs.get() qs = Phase.objects.order_by("phase_number").all() if timezone.now() < qs.first().start: return qs.first() return qs.last() def __str__(self): return _("Phase {phase_number:d} starts on {start:%Y-%m-%d %H:%M} and ends on {end:%Y-%m-%d %H:%M}")\ .format(phase_number=self.phase_number, start=self.start, end=self.end) class Meta: verbose_name = _("phase") verbose_name_plural = _("phases")