1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2024-11-27 00:07:11 +00:00
plateforme-tfjm2/apps/participation/models.py

482 lines
14 KiB
Python
Raw Normal View History

2020-12-27 10:49:54 +00:00
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import os
2020-12-31 11:13:42 +00:00
from address.models import AddressField
from django.conf import settings
2021-01-14 13:22:45 +00:00
from django.core.exceptions import ValidationError
2020-12-27 10:49:54 +00:00
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
2020-12-27 10:49:54 +00:00
from django.utils.translation import gettext_lazy as _
2020-12-28 18:19:01 +00:00
from registration.models import VolunteerRegistration
from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
2020-12-27 10:49:54 +00:00
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."),
)
2020-12-28 17:52:50 +00:00
@property
def students(self):
return self.participants.filter(studentregistration__isnull=False)
@property
def coachs(self):
return self.participants.filter(coachregistration__isnull=False)
2020-12-27 10:49:54 +00:00
@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(
2020-12-30 11:13:05 +00:00
verbose_name=_("end"),
default=timezone.now,
)
2020-12-31 11:13:42 +00:00
place = AddressField(
verbose_name=_("place"),
)
max_teams = models.PositiveSmallIntegerField(
verbose_name=_("max team count"),
default=9,
)
2021-01-01 11:11:09 +00:00
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,
)
2021-01-01 11:11:09 +00:00
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,
)
2021-01-01 11:11:09 +00:00
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()
2020-12-28 16:49:59 +00:00
@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)
2020-12-31 11:23:09 +00:00
def get_absolute_url(self):
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
2020-12-28 18:19:01 +00:00
def __str__(self):
2021-01-01 23:06:58 +00:00
return self.name
2020-12-28 18:19:01 +00:00
class Meta:
verbose_name = _("tournament")
verbose_name_plural = _("tournaments")
indexes = [
Index(fields=("name", "date_start", "date_end", )),
]
2020-12-27 10:49:54 +00:00
class Participation(models.Model):
"""
The Participation model contains all data that are related to the participation:
2021-01-12 14:42:32 +00:00
chosen problem, validity status, solutions,...
2020-12-27 10:49:54 +00:00
"""
team = models.OneToOneField(
Team,
on_delete=models.CASCADE,
verbose_name=_("team"),
)
tournament = models.ForeignKey(
Tournament,
on_delete=models.SET_NULL,
2020-12-27 10:49:54 +00:00
null=True,
blank=True,
2020-12-27 10:49:54 +00:00
default=None,
verbose_name=_("tournament"),
2020-12-27 10:49:54 +00:00
)
valid = models.BooleanField(
null=True,
default=None,
verbose_name=_("valid"),
2021-01-01 16:07:28 +00:00
help_text=_("The participation got the validation of the organizers."),
2020-12-27 10:49:54 +00:00
)
2021-01-12 16:24:46 +00:00
final = models.BooleanField(
default=False,
verbose_name=_("selected for final"),
help_text=_("The team is selected for the final tournament."),
)
2020-12-27 10:49:54 +00:00
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")
2020-12-28 16:49:59 +00:00
class Pool(models.Model):
tournament = models.ForeignKey(
Tournament,
on_delete=models.CASCADE,
related_name="pools",
verbose_name=_("tournament"),
)
round = models.PositiveSmallIntegerField(
verbose_name=_("round"),
2021-01-13 16:00:50 +00:00
choices=[
(1, format_lazy(_("Round {round}"), round=1)),
(2, format_lazy(_("Round {round}"), round=2)),
]
2020-12-28 16:49:59 +00:00
)
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)
2021-01-13 16:00:50 +00:00
def get_absolute_url(self):
return reverse_lazy("participation:pool_detail", args=(self.pk,))
2020-12-28 18:19:01 +00:00
def __str__(self):
2021-01-13 15:22:26 +00:00
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()))
2020-12-28 18:19:01 +00:00
2020-12-28 16:49:59 +00:00
class Meta:
verbose_name = _("pool")
verbose_name_plural = _("pools")
2021-01-14 13:22:45 +00:00
class Passage(models.Model):
pool = models.ForeignKey(
Pool,
on_delete=models.CASCADE,
verbose_name=_("pool"),
related_name="passages",
)
place = models.CharField(
verbose_name=_("place"),
max_length=255,
help_text=_("Where the solution is presented?"),
default="Non indiqué",
)
2021-01-14 14:59:11 +00:00
solution_number = models.PositiveSmallIntegerField(
verbose_name=_("defended solution"),
choices=[
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
],
)
2021-01-14 13:22:45 +00:00
defender = models.ForeignKey(
Participation,
on_delete=models.PROTECT,
verbose_name=_("defender"),
related_name="+",
)
opponent = models.ForeignKey(
Participation,
on_delete=models.PROTECT,
verbose_name=_("opponent"),
related_name="+",
)
reporter = models.ForeignKey(
Participation,
on_delete=models.PROTECT,
verbose_name=_("reporter"),
related_name="+",
)
2021-01-14 14:59:11 +00:00
@property
def defended_solution(self) -> "Solution":
return Solution.objects.get(
participation=self.defender,
problem=self.solution_number,
final_solution=self.pool.tournament.final)
def get_absolute_url(self):
return reverse_lazy("participation:passage_detail", args=(self.pk,))
2021-01-14 13:22:45 +00:00
def clean(self):
if self.defender not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.defender.team.trigram))
if self.opponent not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.opponent.team.trigram))
if self.reporter not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.reporter.team.trigram))
return super().clean()
2021-01-14 14:59:11 +00:00
def __str__(self):
return _("Passage of {defender} for problem {problem}")\
.format(defender=self.defender.team, problem=self.solution_number)
2021-01-14 13:22:45 +00:00
class Meta:
verbose_name = _("passage")
verbose_name_plural = _("passages")
2021-01-12 16:51:55 +00:00
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",
2020-12-27 10:49:54 +00:00
)
problem = models.PositiveSmallIntegerField(
verbose_name=_("problem"),
choices=[
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
],
2020-12-27 10:49:54 +00:00
)
final_solution = models.BooleanField(
verbose_name=_("solution for the final tournament"),
default=False,
)
2020-12-27 10:49:54 +00:00
file = models.FileField(
verbose_name=_("file"),
2021-01-12 16:51:55 +00:00
upload_to=get_solution_filename,
unique=True,
blank=True,
default="",
)
2020-12-27 10:49:54 +00:00
2020-12-28 18:19:01 +00:00
def __str__(self):
2021-01-12 16:51:55 +00:00
return _("Solution of team {team} for problem {problem}")\
.format(team=self.participation.team.name, problem=self.problem)
2020-12-28 18:19:01 +00:00
2020-12-27 10:49:54 +00:00
class Meta:
verbose_name = _("solution")
verbose_name_plural = _("solutions")
2020-12-28 17:52:50 +00:00
unique_together = (('participation', 'problem', 'final_solution', ), )
2020-12-27 10:49:54 +00:00
class Synthesis(models.Model):
2020-12-27 10:49:54 +00:00
participation = models.ForeignKey(
Participation,
on_delete=models.CASCADE,
verbose_name=_("participation"),
)
2021-01-14 13:22:45 +00:00
passage = models.ForeignKey(
Passage,
on_delete=models.CASCADE,
2020-12-28 16:49:59 +00:00
related_name="syntheses",
2021-01-14 13:22:45 +00:00
verbose_name=_("passage"),
2020-12-27 10:49:54 +00:00
)
type = models.PositiveSmallIntegerField(
choices=[
(1, _("opponent"), ),
(2, _("reporter"), ),
]
2020-12-27 10:49:54 +00:00
)
file = models.FileField(
verbose_name=_("file"),
2021-01-12 16:51:55 +00:00
upload_to=get_random_synthesis_filename,
unique=True,
blank=True,
default="",
2020-12-27 10:49:54 +00:00
)
class Meta:
verbose_name = _("synthesis")
verbose_name_plural = _("syntheses")
2021-01-14 13:22:45 +00:00
unique_together = (('participation', 'passage', 'type', ), )