2020-12-27 10:49:54 +00:00
|
|
|
# Copyright (C) 2020 by Animath
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2021-01-18 21:28:43 +00:00
|
|
|
from datetime import date
|
2020-12-27 10:49:54 +00:00
|
|
|
import os
|
|
|
|
|
2021-01-12 17:02:00 +00:00
|
|
|
from django.conf import settings
|
2021-01-14 13:22:45 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2023-04-06 22:05:56 +00:00
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
2020-12-27 10:49:54 +00:00
|
|
|
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
|
2021-01-12 17:02:00 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2021-01-22 08:40:28 +00:00
|
|
|
def get_motivation_letter_filename(instance, filename):
|
|
|
|
return f"authorization/motivation_letters/motivation_letter_{instance.trigram}"
|
|
|
|
|
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
class Team(models.Model):
|
|
|
|
"""
|
2021-01-18 14:52:09 +00:00
|
|
|
The Team model represents a real team that participates to the TFJM².
|
2020-12-27 10:49:54 +00:00
|
|
|
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,
|
2023-02-19 18:49:03 +00:00
|
|
|
validators=[
|
|
|
|
RegexValidator(r"^[A-Z]{3}$"),
|
|
|
|
RegexValidator(fr"^(?!{'|'.join(f'{t}$' for t in settings.FORBIDDEN_TRIGRAMS)})",
|
|
|
|
message=_("This trigram is forbidden.")),
|
|
|
|
],
|
2020-12-27 10:49:54 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
access_code = models.CharField(
|
|
|
|
max_length=6,
|
|
|
|
verbose_name=_("access code"),
|
|
|
|
help_text=_("The access code let other people to join the team."),
|
|
|
|
)
|
|
|
|
|
2021-01-22 08:40:28 +00:00
|
|
|
motivation_letter = models.FileField(
|
|
|
|
verbose_name=_("motivation letter"),
|
|
|
|
upload_to=get_motivation_letter_filename,
|
|
|
|
blank=True,
|
|
|
|
default="",
|
|
|
|
)
|
|
|
|
|
2020-12-28 17:52:50 +00:00
|
|
|
@property
|
|
|
|
def students(self):
|
|
|
|
return self.participants.filter(studentregistration__isnull=False)
|
|
|
|
|
|
|
|
@property
|
2021-01-18 20:30:26 +00:00
|
|
|
def coaches(self):
|
2020-12-28 17:52:50 +00:00
|
|
|
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()}",
|
2021-01-16 21:29:10 +00:00
|
|
|
f"Equipe {self.name} ({self.trigram})",
|
2020-12-27 10:49:54 +00:00
|
|
|
"hotline", # TODO Use a custom sympa template
|
2021-01-16 21:29:10 +00:00
|
|
|
f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2",
|
2020-12-27 10:49:54 +00:00
|
|
|
"education",
|
|
|
|
raise_error=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
|
2021-01-16 21:29:10 +00:00
|
|
|
get_sympa_client().unsubscribe(
|
|
|
|
self.email, f"equipes-{self.participation.tournament.name.lower().replace(' ', '-')}", False)
|
2020-12-27 10:49:54 +00:00
|
|
|
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")
|
2023-04-03 17:13:15 +00:00
|
|
|
ordering = ('trigram',)
|
2020-12-27 10:49:54 +00:00
|
|
|
indexes = [
|
|
|
|
Index(fields=("trigram", )),
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
class Tournament(models.Model):
|
|
|
|
name = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("name"),
|
|
|
|
unique=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
date_start = models.DateField(
|
|
|
|
verbose_name=_("start"),
|
2021-01-18 21:28:43 +00:00
|
|
|
default=date.today,
|
2020-12-28 12:47:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
date_end = models.DateField(
|
2020-12-30 11:13:05 +00:00
|
|
|
verbose_name=_("end"),
|
2021-01-18 21:28:43 +00:00
|
|
|
default=date.today,
|
2020-12-28 12:47:05 +00:00
|
|
|
)
|
|
|
|
|
2023-01-10 19:24:06 +00:00
|
|
|
place = models.CharField(
|
|
|
|
max_length=255,
|
2020-12-31 11:13:42 +00:00
|
|
|
verbose_name=_("place"),
|
|
|
|
)
|
|
|
|
|
2020-12-31 11:26:49 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2021-01-23 18:57:25 +00:00
|
|
|
remote = models.BooleanField(
|
|
|
|
verbose_name=_("remote"),
|
|
|
|
default=False,
|
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2021-01-16 21:29:10 +00:00
|
|
|
@property
|
|
|
|
def teams_email(self):
|
|
|
|
"""
|
|
|
|
:return: The mailing list to contact the team members.
|
|
|
|
"""
|
|
|
|
return f"equipes-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def organizers_email(self):
|
|
|
|
"""
|
|
|
|
:return: The mailing list to contact the team members.
|
|
|
|
"""
|
|
|
|
return f"organisateurs-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def jurys_email(self):
|
|
|
|
"""
|
|
|
|
:return: The mailing list to contact the team members.
|
|
|
|
"""
|
2021-01-18 01:07:31 +00:00
|
|
|
return f"jurys-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
2021-01-16 21:29:10 +00:00
|
|
|
|
|
|
|
def create_mailing_lists(self):
|
|
|
|
"""
|
|
|
|
Create a new Sympa mailing list to contact the team.
|
|
|
|
"""
|
|
|
|
get_sympa_client().create_list(
|
|
|
|
f"equipes-{self.name.lower().replace(' ', '-')}",
|
|
|
|
f"Equipes du tournoi de {self.name}",
|
|
|
|
"hotline", # TODO Use a custom sympa template
|
|
|
|
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
|
|
|
"education",
|
|
|
|
raise_error=False,
|
|
|
|
)
|
|
|
|
get_sympa_client().create_list(
|
|
|
|
f"organisateurs-{self.name.lower().replace(' ', '-')}",
|
|
|
|
f"Organisateurs du tournoi de {self.name}",
|
|
|
|
"hotline", # TODO Use a custom sympa template
|
|
|
|
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
|
|
|
"education",
|
|
|
|
raise_error=False,
|
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
@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)
|
|
|
|
|
2023-03-22 17:44:49 +00:00
|
|
|
@property
|
|
|
|
def best_format(self):
|
|
|
|
n = len(self.participations.filter(valid=True).all())
|
|
|
|
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
|
2023-03-31 15:15:34 +00:00
|
|
|
return '+'.join(map(str, sorted(fmt, reverse=True)))
|
2023-03-22 17:44:49 +00:00
|
|
|
|
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
|
|
|
|
2020-12-28 12:47:05 +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"),
|
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
tournament = models.ForeignKey(
|
|
|
|
Tournament,
|
|
|
|
on_delete=models.SET_NULL,
|
2020-12-27 10:49:54 +00:00
|
|
|
null=True,
|
2020-12-28 12:47:05 +00:00
|
|
|
blank=True,
|
2020-12-27 10:49:54 +00:00
|
|
|
default=None,
|
2020-12-28 12:47:05 +00:00
|
|
|
verbose_name=_("tournament"),
|
2020-12-27 10:49:54 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
valid = models.BooleanField(
|
|
|
|
null=True,
|
|
|
|
default=None,
|
2023-04-05 08:44:27 +00:00
|
|
|
verbose_name=_("valid team"),
|
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")
|
2023-04-11 20:41:52 +00:00
|
|
|
ordering = ('valid', 'team__trigram',)
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
)
|
|
|
|
|
2023-03-31 15:23:40 +00:00
|
|
|
letter = models.PositiveSmallIntegerField(
|
|
|
|
choices=[
|
|
|
|
(1, 'A'),
|
|
|
|
(2, 'B'),
|
|
|
|
(3, 'C'),
|
|
|
|
(4, 'D'),
|
|
|
|
],
|
|
|
|
verbose_name=_('letter'),
|
|
|
|
)
|
|
|
|
|
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"),
|
|
|
|
)
|
|
|
|
|
2021-01-22 17:25:37 +00:00
|
|
|
bbb_url = models.CharField(
|
|
|
|
max_length=255,
|
2021-01-21 16:55:20 +00:00
|
|
|
blank=True,
|
|
|
|
default="",
|
2021-01-22 17:25:37 +00:00
|
|
|
verbose_name=_("BigBlueButton URL"),
|
|
|
|
help_text=_("The link of the BBB visio for this pool."),
|
2021-01-21 16:55:20 +00:00
|
|
|
)
|
|
|
|
|
2021-04-10 07:59:04 +00:00
|
|
|
results_available = models.BooleanField(
|
|
|
|
default=False,
|
|
|
|
verbose_name=_("results available"),
|
|
|
|
help_text=_("Check this case when results become accessible to teams. "
|
|
|
|
"They stay accessible to you. Only averages are given."),
|
|
|
|
)
|
|
|
|
|
2020-12-28 16:49:59 +00:00
|
|
|
@property
|
|
|
|
def solutions(self):
|
2023-05-19 12:44:31 +00:00
|
|
|
return [passage.defended_solution for passage in self.passages.all()]
|
2020-12-28 16:49:59 +00:00
|
|
|
|
2021-01-14 16:54:47 +00:00
|
|
|
def average(self, participation):
|
2022-05-15 14:43:07 +00:00
|
|
|
return sum(passage.average(participation) for passage in self.passages.all()) \
|
|
|
|
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
|
2021-01-14 16:54:47 +00:00
|
|
|
|
2023-04-04 13:10:28 +00:00
|
|
|
async def aaverage(self, participation):
|
|
|
|
return sum([passage.average(participation) async for passage in self.passages.all()]) \
|
|
|
|
+ sum([tweak.diff async for tweak in participation.tweaks.filter(pool=self).all()])
|
|
|
|
|
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-04-29 13:47:46 +00:00
|
|
|
return _("Pool of day {round} for tournament {tournament} with teams {teams}")\
|
2021-01-13 15:22:26 +00:00
|
|
|
.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")
|
2023-03-31 15:23:40 +00:00
|
|
|
ordering = ('round', 'letter',)
|
2020-12-28 16:49:59 +00:00
|
|
|
|
|
|
|
|
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",
|
|
|
|
)
|
|
|
|
|
2023-04-06 22:05:56 +00:00
|
|
|
position = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("position"),
|
|
|
|
choices=zip(range(1, 6), range(1, 6)),
|
|
|
|
default=1,
|
|
|
|
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
|
|
|
)
|
|
|
|
|
2021-01-14 14:59:11 +00:00
|
|
|
solution_number = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("defended solution"),
|
|
|
|
choices=[
|
2023-04-04 09:56:13 +00:00
|
|
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
2021-01-14 14:59:11 +00:00
|
|
|
],
|
|
|
|
)
|
|
|
|
|
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="+",
|
|
|
|
)
|
|
|
|
|
2023-04-07 10:10:25 +00:00
|
|
|
observer = models.ForeignKey(
|
|
|
|
Participation,
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
null=True,
|
|
|
|
blank=True,
|
|
|
|
default=None,
|
|
|
|
verbose_name=_("observer"),
|
|
|
|
related_name="+",
|
|
|
|
)
|
|
|
|
|
2021-04-03 19:59:06 +00:00
|
|
|
defender_penalties = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("penalties"),
|
|
|
|
default=0,
|
|
|
|
help_text=_("Number of penalties for the defender. "
|
|
|
|
"The defender will loose a 0.5 coefficient per penalty."),
|
|
|
|
)
|
|
|
|
|
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)
|
|
|
|
|
2021-04-03 19:59:06 +00:00
|
|
|
def avg(self, iterator) -> float:
|
2021-01-14 17:21:22 +00:00
|
|
|
items = [i for i in iterator if i]
|
|
|
|
return sum(items) / len(items) if items else 0
|
2021-01-14 16:54:47 +00:00
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_defender_writing(self) -> float:
|
2021-01-14 16:54:47 +00:00
|
|
|
return self.avg(note.defender_writing for note in self.notes.all())
|
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_defender_oral(self) -> float:
|
2021-01-14 16:54:47 +00:00
|
|
|
return self.avg(note.defender_oral for note in self.notes.all())
|
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_defender(self) -> float:
|
|
|
|
return self.average_defender_writing + (2 - 0.5 * self.defender_penalties) * self.average_defender_oral
|
2021-01-14 16:54:47 +00:00
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_opponent_writing(self) -> float:
|
2021-01-14 16:54:47 +00:00
|
|
|
return self.avg(note.opponent_writing for note in self.notes.all())
|
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_opponent_oral(self) -> float:
|
2021-01-14 16:54:47 +00:00
|
|
|
return self.avg(note.opponent_oral for note in self.notes.all())
|
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_opponent(self) -> float:
|
2021-01-14 17:43:53 +00:00
|
|
|
return self.average_opponent_writing + 2 * self.average_opponent_oral
|
2021-01-14 16:54:47 +00:00
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_reporter_writing(self) -> float:
|
2021-01-14 16:54:47 +00:00
|
|
|
return self.avg(note.reporter_writing for note in self.notes.all())
|
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_reporter_oral(self) -> float:
|
2021-01-14 16:54:47 +00:00
|
|
|
return self.avg(note.reporter_oral for note in self.notes.all())
|
|
|
|
|
|
|
|
@property
|
2021-04-03 19:59:06 +00:00
|
|
|
def average_reporter(self) -> float:
|
2021-01-14 16:54:47 +00:00
|
|
|
return self.average_reporter_writing + self.average_reporter_oral
|
|
|
|
|
2023-04-07 10:10:25 +00:00
|
|
|
@property
|
|
|
|
def average_observer(self) -> float:
|
|
|
|
return self.avg(note.observer_oral for note in self.notes.all())
|
|
|
|
|
2023-04-07 19:47:06 +00:00
|
|
|
@property
|
|
|
|
def averages(self):
|
|
|
|
yield self.average_defender_writing
|
|
|
|
yield self.average_defender_oral
|
|
|
|
yield self.average_opponent_writing
|
|
|
|
yield self.average_opponent_oral
|
|
|
|
yield self.average_reporter_writing
|
|
|
|
yield self.average_reporter_oral
|
|
|
|
if self.observer:
|
|
|
|
yield self.average_observer
|
|
|
|
|
2021-01-14 16:54:47 +00:00
|
|
|
def average(self, participation):
|
|
|
|
return self.average_defender if participation == self.defender else self.average_opponent \
|
2023-04-07 10:10:25 +00:00
|
|
|
if participation == self.opponent else self.average_reporter if participation == self.reporter \
|
|
|
|
else self.average_observer if participation == self.observer else 0
|
2021-01-14 16:54:47 +00:00
|
|
|
|
2021-01-14 14:59:11 +00:00
|
|
|
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))
|
2023-04-07 10:10:25 +00:00
|
|
|
if self.observer and self.observer not in self.pool.participations.all():
|
|
|
|
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
|
|
|
.format(trigram=self.observer.team.trigram))
|
2021-01-14 13:22:45 +00:00
|
|
|
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")
|
2023-04-06 22:05:56 +00:00
|
|
|
ordering = ('pool', 'position',)
|
2021-01-14 13:22:45 +00:00
|
|
|
|
|
|
|
|
2022-05-15 14:43:07 +00:00
|
|
|
class Tweak(models.Model):
|
|
|
|
pool = models.ForeignKey(
|
|
|
|
Pool,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_("passage"),
|
|
|
|
)
|
|
|
|
|
|
|
|
participation = models.ForeignKey(
|
|
|
|
Participation,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_("participation"),
|
|
|
|
related_name='tweaks',
|
|
|
|
)
|
|
|
|
|
|
|
|
diff = models.IntegerField(
|
|
|
|
verbose_name=_("difference"),
|
|
|
|
help_text=_("Score to add/remove on the final score"),
|
|
|
|
)
|
|
|
|
|
2022-11-08 14:52:44 +00:00
|
|
|
def __str__(self):
|
|
|
|
return f"Tweak for {self.participation.team} of {self.diff} points"
|
|
|
|
|
2022-05-15 14:43:07 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("tweak")
|
|
|
|
verbose_name_plural = _("tweaks")
|
|
|
|
|
|
|
|
|
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 "")
|
|
|
|
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
def get_synthesis_filename(instance, filename):
|
|
|
|
return f"syntheses/{instance.participation.team.trigram}_{instance.type}_{instance.passage.pk}"
|
2021-01-12 16:51:55 +00:00
|
|
|
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
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
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
problem = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("problem"),
|
2021-01-12 17:02:00 +00:00
|
|
|
choices=[
|
2023-04-04 09:56:13 +00:00
|
|
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
2021-01-12 17:02:00 +00:00
|
|
|
],
|
2020-12-27 10:49:54 +00:00
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
final_solution = models.BooleanField(
|
|
|
|
verbose_name=_("solution for the final tournament"),
|
|
|
|
default=False,
|
|
|
|
)
|
2020-12-27 10:49:54 +00:00
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
file = models.FileField(
|
|
|
|
verbose_name=_("file"),
|
2021-01-12 16:51:55 +00:00
|
|
|
upload_to=get_solution_filename,
|
2020-12-28 12:47:05 +00:00
|
|
|
unique=True,
|
|
|
|
)
|
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}")\
|
2021-04-29 13:46:38 +00:00
|
|
|
.format(team=self.participation.team.name, problem=self.problem)\
|
2021-05-11 14:56:44 +00:00
|
|
|
+ (" " + str(_("for final")) if self.final_solution else "")
|
2020-12-28 18:19:01 +00:00
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
class Meta:
|
2020-12-28 12:47:05 +00:00
|
|
|
verbose_name = _("solution")
|
|
|
|
verbose_name_plural = _("solutions")
|
2020-12-28 17:52:50 +00:00
|
|
|
unique_together = (('participation', 'problem', 'final_solution', ), )
|
2021-04-29 13:46:38 +00:00
|
|
|
ordering = ('participation__team__trigram', 'final_solution', 'problem',)
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
|
2020-12-28 12:47:05 +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,
|
2020-12-28 12:47:05 +00:00
|
|
|
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
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
type = models.PositiveSmallIntegerField(
|
|
|
|
choices=[
|
|
|
|
(1, _("opponent"), ),
|
|
|
|
(2, _("reporter"), ),
|
|
|
|
]
|
2020-12-27 10:49:54 +00:00
|
|
|
)
|
|
|
|
|
2020-12-28 12:47:05 +00:00
|
|
|
file = models.FileField(
|
|
|
|
verbose_name=_("file"),
|
2021-01-14 16:26:08 +00:00
|
|
|
upload_to=get_synthesis_filename,
|
2020-12-28 12:47:05 +00:00
|
|
|
unique=True,
|
2020-12-27 10:49:54 +00:00
|
|
|
)
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
def __str__(self):
|
2021-04-10 08:02:49 +00:00
|
|
|
return _("Synthesis of {team} as {type} for problem {problem} of {defender}").format(
|
|
|
|
team=self.participation.team.trigram,
|
|
|
|
type=self.get_type_display(),
|
|
|
|
problem=self.passage.solution_number,
|
|
|
|
defender=self.passage.defender.team.trigram,
|
|
|
|
)
|
2021-01-14 16:26:08 +00:00
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
class Meta:
|
2020-12-28 12:47:05 +00:00
|
|
|
verbose_name = _("synthesis")
|
|
|
|
verbose_name_plural = _("syntheses")
|
2021-01-14 13:22:45 +00:00
|
|
|
unique_together = (('participation', 'passage', 'type', ), )
|
2021-04-13 08:05:00 +00:00
|
|
|
ordering = ('passage__pool__round', 'type',)
|
2021-01-14 16:54:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Note(models.Model):
|
|
|
|
jury = models.ForeignKey(
|
|
|
|
VolunteerRegistration,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_("jury"),
|
|
|
|
related_name="notes",
|
|
|
|
)
|
|
|
|
|
|
|
|
passage = models.ForeignKey(
|
|
|
|
Passage,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_("passage"),
|
|
|
|
related_name="notes",
|
|
|
|
)
|
|
|
|
|
|
|
|
defender_writing = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("defender writing note"),
|
|
|
|
choices=[(i, i) for i in range(0, 21)],
|
|
|
|
default=0,
|
|
|
|
)
|
|
|
|
|
|
|
|
defender_oral = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("defender oral note"),
|
|
|
|
choices=[(i, i) for i in range(0, 17)],
|
|
|
|
default=0,
|
|
|
|
)
|
|
|
|
|
|
|
|
opponent_writing = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("opponent writing note"),
|
|
|
|
choices=[(i, i) for i in range(0, 10)],
|
|
|
|
default=0,
|
|
|
|
)
|
|
|
|
|
|
|
|
opponent_oral = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("opponent oral note"),
|
|
|
|
choices=[(i, i) for i in range(0, 11)],
|
|
|
|
default=0,
|
|
|
|
)
|
|
|
|
|
|
|
|
reporter_writing = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("reporter writing note"),
|
|
|
|
choices=[(i, i) for i in range(0, 10)],
|
|
|
|
default=0,
|
|
|
|
)
|
|
|
|
|
|
|
|
reporter_oral = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("reporter oral note"),
|
|
|
|
choices=[(i, i) for i in range(0, 11)],
|
|
|
|
default=0,
|
|
|
|
)
|
|
|
|
|
2023-04-07 10:10:25 +00:00
|
|
|
observer_oral = models.SmallIntegerField(
|
|
|
|
verbose_name=_("observer note"),
|
|
|
|
choices=zip(range(-4, 5), range(-4, 5)),
|
|
|
|
default=0,
|
|
|
|
)
|
|
|
|
|
2023-04-07 19:47:06 +00:00
|
|
|
def get_all(self):
|
|
|
|
yield self.defender_writing
|
|
|
|
yield self.defender_oral
|
|
|
|
yield self.opponent_writing
|
|
|
|
yield self.opponent_oral
|
|
|
|
yield self.reporter_writing
|
|
|
|
yield self.reporter_oral
|
|
|
|
if self.passage.observer:
|
|
|
|
yield self.observer_oral
|
|
|
|
|
2022-05-15 10:23:17 +00:00
|
|
|
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
|
2023-04-07 10:10:25 +00:00
|
|
|
reporter_writing: int, reporter_oral: int, observer_oral: int = 0):
|
2022-05-15 10:23:17 +00:00
|
|
|
self.defender_writing = defender_writing
|
|
|
|
self.defender_oral = defender_oral
|
|
|
|
self.opponent_writing = opponent_writing
|
|
|
|
self.opponent_oral = opponent_oral
|
|
|
|
self.reporter_writing = reporter_writing
|
|
|
|
self.reporter_oral = reporter_oral
|
2023-04-07 10:10:25 +00:00
|
|
|
self.observer_oral = observer_oral
|
2022-05-15 10:23:17 +00:00
|
|
|
|
2021-01-14 17:21:22 +00:00
|
|
|
def get_absolute_url(self):
|
|
|
|
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
|
|
|
|
|
2021-01-14 16:54:47 +00:00
|
|
|
def __str__(self):
|
|
|
|
return _("Notes of {jury} for {passage}").format(jury=self.jury, passage=self.passage)
|
|
|
|
|
2021-01-14 17:43:53 +00:00
|
|
|
def __bool__(self):
|
|
|
|
return any((self.defender_writing, self.defender_oral, self.opponent_writing, self.opponent_oral,
|
2023-04-07 10:10:25 +00:00
|
|
|
self.reporter_writing, self.reporter_oral, self.observer_oral))
|
2021-01-14 17:43:53 +00:00
|
|
|
|
2021-01-14 16:54:47 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("note")
|
|
|
|
verbose_name_plural = _("notes")
|