mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-07-05 09:14:10 +02:00
Clone Corres2math platform
This commit is contained in:
307
apps/participation/models.py
Normal file
307
apps/participation/models.py
Normal file
@ -0,0 +1,307 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from tfjm.lists import get_sympa_client
|
||||
from tfjm.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} 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 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.filter(start__lte=timezone.now()).order_by("phase_number").all()
|
||||
return qs.last() if qs.exists() else None
|
||||
|
||||
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")
|
Reference in New Issue
Block a user