948 lines
33 KiB
Python
948 lines
33 KiB
Python
# Copyright (C) 2020 by Animath
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from datetime import date
|
|
import os
|
|
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.core.validators import MaxValueValidator, MinValueValidator, 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
|
|
from django.utils.translation import gettext_lazy as _
|
|
from registration.models import VolunteerRegistration, Payment
|
|
from tfjm.lists import get_sympa_client
|
|
|
|
|
|
def get_motivation_letter_filename(instance, filename):
|
|
return f"authorization/motivation_letters/motivation_letter_{instance.trigram}"
|
|
|
|
|
|
class Team(models.Model):
|
|
"""
|
|
The Team model represents a real team that participates to the TFJM².
|
|
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(r"^[A-Z]{3}$"),
|
|
RegexValidator(fr"^(?!{'|'.join(f'{t}$' for t in settings.FORBIDDEN_TRIGRAMS)})",
|
|
message=_("This trigram is forbidden.")),
|
|
],
|
|
)
|
|
|
|
access_code = models.CharField(
|
|
max_length=6,
|
|
verbose_name=_("access code"),
|
|
help_text=_("The access code let other people to join the team."),
|
|
)
|
|
|
|
motivation_letter = models.FileField(
|
|
verbose_name=_("motivation letter"),
|
|
upload_to=get_motivation_letter_filename,
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
@property
|
|
def students(self):
|
|
return self.participants.filter(studentregistration__isnull=False)
|
|
|
|
@property
|
|
def coaches(self):
|
|
return self.participants.filter(coachregistration__isnull=False)
|
|
|
|
def can_validate(self):
|
|
if any(not r.email_confirmed for r in self.participants.all()):
|
|
return False
|
|
if self.students.count() < 4:
|
|
return False
|
|
if not self.coaches.exists():
|
|
return False
|
|
if not self.participation.tournament:
|
|
return False
|
|
if any(not r.photo_authorization for r in self.participants.all()):
|
|
return False
|
|
if not self.motivation_letter:
|
|
return False
|
|
if not self.participation.tournament.remote:
|
|
if any(r.under_18 and not r.health_sheet for r in self.students.all()):
|
|
return False
|
|
if any(r.under_18 and not r.vaccine_sheet for r in self.students.all()):
|
|
return False
|
|
if any(r.under_18 and not r.parental_authorization for r in self.students.all()):
|
|
return False
|
|
return True
|
|
|
|
def important_informations(self):
|
|
informations = []
|
|
|
|
if self.participation.valid is None:
|
|
if not self.participation.tournament:
|
|
text = _("The team {trigram} is not registered to any tournament. "
|
|
"You can register the team to a tournament using <a href='{url}'>this link</a>.")
|
|
url = reverse_lazy("participation:update_team", args=(self.pk,))
|
|
content = format_lazy(text, trigram=self.trigram, url=url)
|
|
informations.append({
|
|
'title': _("No tournament"),
|
|
'type': "danger",
|
|
'priority': 4,
|
|
'content': content,
|
|
})
|
|
else:
|
|
text = _("Registrations for the tournament of {tournament} are ending on the {date:%Y-%m-%d %H:%M}.")
|
|
content = format_lazy(text,
|
|
tournament=self.participation.tournament.name,
|
|
date=self.participation.tournament.inscription_limit)
|
|
informations.append({
|
|
'title': _("Registrations closure"),
|
|
'type': "info",
|
|
'priority': 1,
|
|
'content': content,
|
|
})
|
|
|
|
if not self.motivation_letter:
|
|
text = _("The team {trigram} has not uploaded a motivation letter. "
|
|
"You can upload your motivation letter using <a href='{url}'>this link</a>.")
|
|
url = reverse_lazy("participation:upload_team_motivation_letter", args=(self.pk,))
|
|
content = format_lazy(text, trigram=self.trigram, url=url)
|
|
informations.append({
|
|
'title': _("No motivation letter"),
|
|
'type': "danger",
|
|
'priority': 10,
|
|
'content': content,
|
|
})
|
|
|
|
nb_students = self.students.count()
|
|
nb_coaches = self.coaches.count()
|
|
if nb_students < 4:
|
|
text = _("The team {trigram} has less than 4 students ({nb_students}). "
|
|
"You can invite more students to join the team using "
|
|
"the invite code <strong>{code}</strong>.")
|
|
content = format_lazy(text, trigram=self.trigram, nb_students=nb_students, code=self.access_code)
|
|
informations.append({
|
|
'title': _("Not enough students"),
|
|
'type': "warning",
|
|
'priority': 7,
|
|
'content': content,
|
|
})
|
|
|
|
if not nb_coaches:
|
|
text = _("The team {trigram} has no coach. "
|
|
"You can invite a coach to join the team using the invite code <strong>{code}</strong>.")
|
|
content = format_lazy(text, trigram=self.trigram, nb_students=nb_students, code=self.access_code)
|
|
informations.append({
|
|
'title': _("No coach"),
|
|
'type': "warning",
|
|
'priority': 8,
|
|
'content': content,
|
|
})
|
|
|
|
if nb_students > 6 or nb_coaches > 2:
|
|
text = _("The team {trigram} has more than 6 students ({nb_students}) "
|
|
"or more than 2 coaches ({nb_coaches})."
|
|
"You have to restrict the number of students and coaches to 6 and 2, respectively.")
|
|
content = format_lazy(text, trigram=self.trigram, nb_students=nb_students, nb_coaches=nb_coaches)
|
|
informations.append({
|
|
'title': _("Too many members"),
|
|
'type': "warning",
|
|
'priority': 7,
|
|
'content': content,
|
|
})
|
|
elif nb_students >= 4 and nb_coaches >= 1:
|
|
if self.can_validate():
|
|
text = _("The team {trigram} is ready to be validated. "
|
|
"You can request validation on <a href='{url}'>the page of your team</a>.")
|
|
url = reverse_lazy("participation:team_detail", args=(self.pk,))
|
|
content = format_lazy(text, trigram=self.trigram, url=url)
|
|
informations.append({
|
|
'title': _("Validate team"),
|
|
'type': "success",
|
|
'priority': 2,
|
|
'content': content,
|
|
})
|
|
else:
|
|
text = _("The team {trigram} has enough participants, but is not ready to be validated. "
|
|
"Please make sure that all the participants have uploaded the required documents. "
|
|
"To invite more participants, use the invite code <strong>{code}</strong>.")
|
|
content = format_lazy(text, trigram=self.trigram, code=self.access_code)
|
|
informations.append({
|
|
'title': _("Validate team"),
|
|
'type': "warning",
|
|
'priority': 10,
|
|
'content': content,
|
|
})
|
|
elif self.participation.valid is False:
|
|
text = _("The team {trigram} has not been validated by the organizers yet. Please be patient.")
|
|
content = format_lazy(text, trigram=self.trigram)
|
|
informations.append({
|
|
'title': _("Pending validation"),
|
|
'type': "warning",
|
|
'priority': 2,
|
|
'content': content,
|
|
})
|
|
else:
|
|
informations.extend(self.participation.important_informations())
|
|
|
|
return informations
|
|
|
|
@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"Equipe {self.name} ({self.trigram})",
|
|
"hotline", # TODO Use a custom sympa template
|
|
f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2",
|
|
"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
|
|
get_sympa_client().unsubscribe(
|
|
self.email, f"equipes-{self.participation.tournament.name.lower().replace(' ', '-')}", 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
|
|
self.access_code = get_random_string(6)
|
|
self.create_mailing_list()
|
|
|
|
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")
|
|
ordering = ('trigram',)
|
|
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=date.today,
|
|
)
|
|
|
|
date_end = models.DateField(
|
|
verbose_name=_("end"),
|
|
default=date.today,
|
|
)
|
|
|
|
place = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_("place"),
|
|
)
|
|
|
|
max_teams = models.PositiveSmallIntegerField(
|
|
verbose_name=_("max team count"),
|
|
default=9,
|
|
)
|
|
|
|
price = models.PositiveSmallIntegerField(
|
|
verbose_name=_("price"),
|
|
default=21,
|
|
)
|
|
|
|
remote = models.BooleanField(
|
|
verbose_name=_("remote"),
|
|
default=False,
|
|
)
|
|
|
|
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,
|
|
)
|
|
|
|
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,
|
|
)
|
|
|
|
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,
|
|
)
|
|
|
|
@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.
|
|
"""
|
|
return f"jurys-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
|
|
|
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,
|
|
)
|
|
|
|
@staticmethod
|
|
def final_tournament():
|
|
qs = Tournament.objects.filter(final=True)
|
|
if qs.exists():
|
|
return qs.get()
|
|
|
|
@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)
|
|
|
|
@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]
|
|
return '+'.join(map(str, sorted(fmt, reverse=True)))
|
|
|
|
def get_absolute_url(self):
|
|
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("tournament")
|
|
verbose_name_plural = _("tournaments")
|
|
indexes = [
|
|
Index(fields=("name", "date_start", "date_end", )),
|
|
]
|
|
|
|
|
|
class Participation(models.Model):
|
|
"""
|
|
The Participation model contains all data that are related to the participation:
|
|
chosen problem, validity status, solutions,...
|
|
"""
|
|
team = models.OneToOneField(
|
|
Team,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("team"),
|
|
)
|
|
|
|
tournament = models.ForeignKey(
|
|
Tournament,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
default=None,
|
|
verbose_name=_("tournament"),
|
|
)
|
|
|
|
valid = models.BooleanField(
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("valid team"),
|
|
help_text=_("The participation got the validation of the organizers."),
|
|
)
|
|
|
|
final = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("selected for final"),
|
|
help_text=_("The team is selected for the final tournament."),
|
|
)
|
|
|
|
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)
|
|
|
|
def important_informations(self):
|
|
informations = []
|
|
|
|
missing_payments = Payment.objects.filter(registration__in=self.team.participants.all(), valid=False)
|
|
if missing_payments.exists():
|
|
text = _("<p>The team {trigram} has {nb_missing_payments} missing payments. Each member of the team "
|
|
"must have a valid payment (or send a scholarship notification) "
|
|
"to participate to the tournament.</p>"
|
|
"<p>Participants that have not paid yet are: {participants}.</p>")
|
|
content = format_lazy(text, trigram=self.team.trigram, nb_missing_payments=missing_payments.count(),
|
|
participants=", ".join(str(p.registration) for p in missing_payments.all()))
|
|
informations.append({
|
|
'title': _("Missing payments"),
|
|
'type': "danger",
|
|
'priority': 10,
|
|
'content': content,
|
|
})
|
|
|
|
if timezone.now() <= self.tournament.solution_limit:
|
|
text = _("<p>The solutions for the tournament of {tournament} are due on the {date:%Y-%m-%d %H:%M}.</p>"
|
|
"<p>You have currently sent <strong>{nb_solutions}</strong> solutions. "
|
|
"We suggest to send at least <strong>{min_solutions}</strong> different solutions.</p>"
|
|
"<p>You can upload your solutions on <a href='{url}'>your participation page</a>.</p>")
|
|
url = reverse_lazy("participation:participation_detail", args=(self.pk,))
|
|
content = format_lazy(text, tournament=self.tournament.name, date=self.tournament.solution_limit,
|
|
nb_solutions=self.solutions.count(), min_solutions=len(settings.PROBLEMS) - 3,
|
|
url=url)
|
|
informations.append({
|
|
'title': _("Solutions due"),
|
|
'type': "info",
|
|
'priority': 1,
|
|
'content': content,
|
|
})
|
|
|
|
return informations
|
|
|
|
class Meta:
|
|
verbose_name = _("participation")
|
|
verbose_name_plural = _("participations")
|
|
ordering = ('valid', 'team__trigram',)
|
|
|
|
|
|
class Pool(models.Model):
|
|
tournament = models.ForeignKey(
|
|
Tournament,
|
|
on_delete=models.CASCADE,
|
|
related_name="pools",
|
|
verbose_name=_("tournament"),
|
|
)
|
|
|
|
round = models.PositiveSmallIntegerField(
|
|
verbose_name=_("round"),
|
|
choices=[
|
|
(1, format_lazy(_("Round {round}"), round=1)),
|
|
(2, format_lazy(_("Round {round}"), round=2)),
|
|
]
|
|
)
|
|
|
|
letter = models.PositiveSmallIntegerField(
|
|
choices=[
|
|
(1, 'A'),
|
|
(2, 'B'),
|
|
(3, 'C'),
|
|
(4, 'D'),
|
|
],
|
|
verbose_name=_('letter'),
|
|
)
|
|
|
|
participations = models.ManyToManyField(
|
|
Participation,
|
|
related_name="pools",
|
|
verbose_name=_("participations"),
|
|
)
|
|
|
|
juries = models.ManyToManyField(
|
|
VolunteerRegistration,
|
|
related_name="jury_in",
|
|
verbose_name=_("juries"),
|
|
)
|
|
|
|
bbb_url = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default="",
|
|
verbose_name=_("BigBlueButton URL"),
|
|
help_text=_("The link of the BBB visio for this pool."),
|
|
)
|
|
|
|
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."),
|
|
)
|
|
|
|
@property
|
|
def solutions(self):
|
|
return [passage.defended_solution for passage in self.passages.all()]
|
|
|
|
def average(self, participation):
|
|
return sum(passage.average(participation) for passage in self.passages.all()) \
|
|
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
|
|
|
|
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()])
|
|
|
|
def get_absolute_url(self):
|
|
return reverse_lazy("participation:pool_detail", args=(self.pk,))
|
|
|
|
def __str__(self):
|
|
return _("Pool of day {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()))
|
|
|
|
class Meta:
|
|
verbose_name = _("pool")
|
|
verbose_name_plural = _("pools")
|
|
ordering = ('round', 'letter',)
|
|
|
|
|
|
class Passage(models.Model):
|
|
pool = models.ForeignKey(
|
|
Pool,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("pool"),
|
|
related_name="passages",
|
|
)
|
|
|
|
position = models.PositiveSmallIntegerField(
|
|
verbose_name=_("position"),
|
|
choices=zip(range(1, 6), range(1, 6)),
|
|
default=1,
|
|
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
|
)
|
|
|
|
solution_number = models.PositiveSmallIntegerField(
|
|
verbose_name=_("defended solution"),
|
|
choices=[
|
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
|
],
|
|
)
|
|
|
|
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="+",
|
|
)
|
|
|
|
observer = models.ForeignKey(
|
|
Participation,
|
|
on_delete=models.PROTECT,
|
|
null=True,
|
|
blank=True,
|
|
default=None,
|
|
verbose_name=_("observer"),
|
|
related_name="+",
|
|
)
|
|
|
|
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."),
|
|
)
|
|
|
|
@property
|
|
def defended_solution(self) -> "Solution":
|
|
return Solution.objects.get(
|
|
participation=self.defender,
|
|
problem=self.solution_number,
|
|
final_solution=self.pool.tournament.final)
|
|
|
|
def avg(self, iterator) -> float:
|
|
items = [i for i in iterator if i]
|
|
return sum(items) / len(items) if items else 0
|
|
|
|
@property
|
|
def average_defender_writing(self) -> float:
|
|
return self.avg(note.defender_writing for note in self.notes.all())
|
|
|
|
@property
|
|
def average_defender_oral(self) -> float:
|
|
return self.avg(note.defender_oral for note in self.notes.all())
|
|
|
|
@property
|
|
def average_defender(self) -> float:
|
|
return self.average_defender_writing + (2 - 0.5 * self.defender_penalties) * self.average_defender_oral
|
|
|
|
@property
|
|
def average_opponent_writing(self) -> float:
|
|
return self.avg(note.opponent_writing for note in self.notes.all())
|
|
|
|
@property
|
|
def average_opponent_oral(self) -> float:
|
|
return self.avg(note.opponent_oral for note in self.notes.all())
|
|
|
|
@property
|
|
def average_opponent(self) -> float:
|
|
return self.average_opponent_writing + 2 * self.average_opponent_oral
|
|
|
|
@property
|
|
def average_reporter_writing(self) -> float:
|
|
return self.avg(note.reporter_writing for note in self.notes.all())
|
|
|
|
@property
|
|
def average_reporter_oral(self) -> float:
|
|
return self.avg(note.reporter_oral for note in self.notes.all())
|
|
|
|
@property
|
|
def average_reporter(self) -> float:
|
|
return self.average_reporter_writing + self.average_reporter_oral
|
|
|
|
@property
|
|
def average_observer(self) -> float:
|
|
return self.avg(note.observer_oral for note in self.notes.all())
|
|
|
|
@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
|
|
|
|
def average(self, participation):
|
|
return self.average_defender if participation == self.defender else self.average_opponent \
|
|
if participation == self.opponent else self.average_reporter if participation == self.reporter \
|
|
else self.average_observer if participation == self.observer else 0
|
|
|
|
def get_absolute_url(self):
|
|
return reverse_lazy("participation:passage_detail", args=(self.pk,))
|
|
|
|
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))
|
|
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))
|
|
return super().clean()
|
|
|
|
def __str__(self):
|
|
return _("Passage of {defender} for problem {problem}")\
|
|
.format(defender=self.defender.team, problem=self.solution_number)
|
|
|
|
class Meta:
|
|
verbose_name = _("passage")
|
|
verbose_name_plural = _("passages")
|
|
ordering = ('pool', 'position',)
|
|
|
|
|
|
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"),
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"Tweak for {self.participation.team} of {self.diff} points"
|
|
|
|
class Meta:
|
|
verbose_name = _("tweak")
|
|
verbose_name_plural = _("tweaks")
|
|
|
|
|
|
def get_solution_filename(instance, filename):
|
|
return f"solutions/{instance.participation.team.trigram}_{instance.problem}" \
|
|
+ ("final" if instance.final_solution else "")
|
|
|
|
|
|
def get_synthesis_filename(instance, filename):
|
|
return f"syntheses/{instance.participation.team.trigram}_{instance.type}_{instance.passage.pk}"
|
|
|
|
|
|
class Solution(models.Model):
|
|
participation = models.ForeignKey(
|
|
Participation,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("participation"),
|
|
related_name="solutions",
|
|
)
|
|
|
|
problem = models.PositiveSmallIntegerField(
|
|
verbose_name=_("problem"),
|
|
choices=[
|
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
|
],
|
|
)
|
|
|
|
final_solution = models.BooleanField(
|
|
verbose_name=_("solution for the final tournament"),
|
|
default=False,
|
|
)
|
|
|
|
file = models.FileField(
|
|
verbose_name=_("file"),
|
|
upload_to=get_solution_filename,
|
|
unique=True,
|
|
)
|
|
|
|
def __str__(self):
|
|
return _("Solution of team {team} for problem {problem}")\
|
|
.format(team=self.participation.team.name, problem=self.problem)\
|
|
+ (" " + str(_("for final")) if self.final_solution else "")
|
|
|
|
class Meta:
|
|
verbose_name = _("solution")
|
|
verbose_name_plural = _("solutions")
|
|
unique_together = (('participation', 'problem', 'final_solution', ), )
|
|
ordering = ('participation__team__trigram', 'final_solution', 'problem',)
|
|
|
|
|
|
class Synthesis(models.Model):
|
|
participation = models.ForeignKey(
|
|
Participation,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("participation"),
|
|
)
|
|
|
|
passage = models.ForeignKey(
|
|
Passage,
|
|
on_delete=models.CASCADE,
|
|
related_name="syntheses",
|
|
verbose_name=_("passage"),
|
|
)
|
|
|
|
type = models.PositiveSmallIntegerField(
|
|
choices=[
|
|
(1, _("opponent"), ),
|
|
(2, _("reporter"), ),
|
|
]
|
|
)
|
|
|
|
file = models.FileField(
|
|
verbose_name=_("file"),
|
|
upload_to=get_synthesis_filename,
|
|
unique=True,
|
|
)
|
|
|
|
def __str__(self):
|
|
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,
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("synthesis")
|
|
verbose_name_plural = _("syntheses")
|
|
unique_together = (('participation', 'passage', 'type', ), )
|
|
ordering = ('passage__pool__round', 'type',)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
observer_oral = models.SmallIntegerField(
|
|
verbose_name=_("observer note"),
|
|
choices=zip(range(-4, 5), range(-4, 5)),
|
|
default=0,
|
|
)
|
|
|
|
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
|
|
|
|
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
|
|
reporter_writing: int, reporter_oral: int, observer_oral: int = 0):
|
|
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
|
|
self.observer_oral = observer_oral
|
|
|
|
def get_absolute_url(self):
|
|
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
|
|
|
|
def __str__(self):
|
|
return _("Notes of {jury} for {passage}").format(jury=self.jury, passage=self.passage)
|
|
|
|
def __bool__(self):
|
|
return any((self.defender_writing, self.defender_oral, self.opponent_writing, self.opponent_oral,
|
|
self.reporter_writing, self.reporter_oral, self.observer_oral))
|
|
|
|
class Meta:
|
|
verbose_name = _("note")
|
|
verbose_name_plural = _("notes")
|