plateforme-corres2math/apps/participation/models.py

296 lines
8.8 KiB
Python
Raw Normal View History

import os
2020-09-27 12:32:05 +00:00
import re
2020-10-09 11:58:42 +00:00
from corres2math.lists import get_sympa_client
from corres2math.matrix import Matrix, RoomPreset, RoomVisibility
2020-09-27 12:32:05 +00:00
from django.core.exceptions import ObjectDoesNotExist
2020-09-21 15:53:07 +00:00
from django.core.validators import RegexValidator
2020-09-21 13:41:55 +00:00
from django.db import models
2020-09-21 15:53:07 +00:00
from django.db.models import Index
2020-10-29 14:09:24 +00:00
from django.template.loader import render_to_string
2020-10-15 12:55:45 +00:00
from django.urls import reverse_lazy
from django.utils import timezone
2020-09-23 21:20:44 +00:00
from django.utils.crypto import get_random_string
2020-09-24 09:15:54 +00:00
from django.utils.text import format_lazy
2020-09-21 15:53:07 +00:00
from django.utils.translation import gettext_lazy as _
2020-09-21 15:53:07 +00:00
class Team(models.Model):
2020-10-30 18:46:46 +00:00
"""
The Team model represents a real team that participates to the Correspondances.
This only includes the registration detail.
"""
2020-09-21 15:53:07 +00:00
name = models.CharField(
max_length=255,
verbose_name=_("name"),
unique=True,
)
trigram = models.CharField(
max_length=3,
verbose_name=_("trigram"),
2020-09-23 21:20:44 +00:00
help_text=_("The trigram must be composed of three uppercase letters."),
2020-09-21 15:53:07 +00:00
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):
2020-10-30 18:46:46 +00:00
"""
:return: The mailing list to contact the team members.
"""
return f"equipe-{self.trigram.lower()}@{os.getenv('SYMPA_HOST', 'localhost')}"
2020-10-09 11:49:09 +00:00
def create_mailing_list(self):
2020-10-30 18:46:46 +00:00
"""
Create a new Sympa mailing list to contact the team.
"""
2020-10-09 11:49:09 +00:00
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,
2020-10-09 11:49:09 +00:00
)
def delete_mailing_list(self):
2020-10-30 18:46:46 +00:00
"""
Drop the Sympa mailing list, if the team is empty or if the trigram changed.
"""
2020-10-09 11:49:09 +00:00
get_sympa_client().delete_list(f"equipe-{self.trigram}")
2020-09-23 21:20:44 +00:00
def save(self, *args, **kwargs):
if not self.access_code:
2020-10-30 18:46:46 +00:00
# if the team got created, generate the access code, create the contact mailing list
# and create a dedicated Matrix room.
2020-09-23 21:20:44 +00:00
self.access_code = get_random_string(6)
2020-10-09 11:49:09 +00:00
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,
)
2020-09-23 21:20:44 +00:00
return super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse_lazy("participation:team_detail", args=(self.pk,))
2020-09-21 15:53:07 +00:00
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):
2020-10-30 18:46:46 +00:00
"""
The Participation model contains all data that are related to the participation:
chosen problem, validity status, videos,...
"""
2020-09-21 15:53:07 +00:00
team = models.OneToOneField(
Team,
on_delete=models.CASCADE,
verbose_name=_("team"),
)
problem = models.IntegerField(
2020-10-31 12:36:03 +00:00
choices=[(i, format_lazy(_("Problem #{problem:d}"), problem=i)) for i in range(1, 4)],
2020-09-21 15:53:07 +00:00
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."),
)
2020-09-27 12:32:05 +00:00
solution = models.OneToOneField(
2020-09-21 15:53:07 +00:00
"participation.Video",
on_delete=models.SET_NULL,
2020-09-27 12:32:05 +00:00
related_name="participation_solution",
2020-09-21 15:53:07 +00:00
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"),
)
2020-09-27 12:32:05 +00:00
synthesis = models.OneToOneField(
2020-09-21 15:53:07 +00:00
"participation.Video",
on_delete=models.SET_NULL,
2020-09-27 12:32:05 +00:00
related_name="participation_synthesis",
2020-09-21 15:53:07 +00:00
null=True,
default=None,
verbose_name=_("synthesis video"),
)
def get_absolute_url(self):
2020-10-15 12:55:45 +00:00
return reverse_lazy("participation:participation_detail", args=(self.pk,))
2020-09-21 15:53:07 +00:00
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):
2020-10-30 18:46:46 +00:00
"""
The Video model only contains a link and a validity status.
"""
2020-09-21 15:53:07 +00:00
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."),
)
2020-09-27 12:32:05 +00:00
@property
def participation(self):
2020-10-30 18:46:46 +00:00
"""
Retrives the participation that is associated to this video,
whatever it is a solution or a synthesis.
"""
2020-09-27 12:32:05 +00:00
try:
2020-10-30 18:46:46 +00:00
# If this is a solution
2020-09-27 12:32:05 +00:00
return self.participation_solution
except ObjectDoesNotExist:
2020-10-30 18:46:46 +00:00
# If this is a synthesis
2020-09-27 12:32:05 +00:00
return self.participation_synthesis
@property
def platform(self):
2020-10-30 18:46:46 +00:00
"""
According to the link, retrieve the platform that is used to upload the video.
"""
2020-09-27 12:32:05 +00:00
if "youtube.com" in self.link or "youtu.be" in self.link:
return "youtube"
return "unknown"
@property
def youtube_code(self):
2020-10-30 18:46:46 +00:00
"""
If the video is uploaded on Youtube, search in the URL the video code.
"""
2020-09-27 12:32:05 +00:00
return re.compile("(https?://|)(www\\.|)(youtube\\.com/watch\\?v=|youtu\\.be/)([a-zA-Z0-9-_]*)?.*?")\
.match(self.link).group(4)
2020-09-27 12:32:05 +00:00
2020-10-20 13:20:33 +00:00
def as_iframe(self):
2020-10-30 18:46:46 +00:00
"""
Generate the HTML code to embed the video in an iframe, according to the type of the host platform.
"""
2020-10-20 13:20:33 +00:00
if self.platform == "youtube":
return render_to_string("participation/youtube_iframe.html", context=dict(youtube_code=self.youtube_code))
return None
2020-09-21 15:53:07 +00:00
def __str__(self):
return _("Video of team {name} ({trigram})")\
2020-09-24 09:44:43 +00:00
.format(name=self.participation.team.name, trigram=self.participation.team.trigram)
2020-09-21 15:53:07 +00:00
class Meta:
verbose_name = _("video")
verbose_name_plural = _("videos")
2020-10-31 13:37:36 +00:00
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"),
)
2020-11-01 19:30:02 +00:00
def __str__(self):
return self.question
2020-10-31 13:37:36 +00:00
class Phase(models.Model):
2020-10-30 18:46:46 +00:00
"""
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):
2020-10-30 18:46:46 +00:00
"""
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):
2020-10-20 10:54:49 +00:00
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")