2020-04-29 02:06:02 +00:00
|
|
|
import os
|
2020-05-25 16:27:07 +00:00
|
|
|
import random
|
2020-04-29 02:06:02 +00:00
|
|
|
|
2020-05-05 15:12:24 +00:00
|
|
|
from django.core.mail import send_mail
|
2020-04-29 02:06:02 +00:00
|
|
|
from django.db import models
|
2020-05-05 15:12:24 +00:00
|
|
|
from django.template.loader import render_to_string
|
2020-05-04 18:21:53 +00:00
|
|
|
from django.urls import reverse_lazy
|
2020-05-05 11:54:26 +00:00
|
|
|
from django.utils import timezone
|
2020-04-29 02:06:02 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
|
|
|
|
|
|
class Tournament(models.Model):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Store the information of a tournament.
|
|
|
|
"""
|
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
name = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("name"),
|
|
|
|
)
|
|
|
|
|
|
|
|
organizers = models.ManyToManyField(
|
|
|
|
'member.TFJMUser',
|
|
|
|
related_name="organized_tournaments",
|
|
|
|
verbose_name=_("organizers"),
|
2020-05-11 12:08:19 +00:00
|
|
|
help_text=_("List of all organizers that can see and manipulate data of the tournament and the teams."),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
size = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("size"),
|
2020-05-11 12:08:19 +00:00
|
|
|
help_text=_("Number of teams that are allowed to join the tournament."),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
place = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("place"),
|
|
|
|
)
|
|
|
|
|
|
|
|
price = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_("price"),
|
2020-05-11 12:08:19 +00:00
|
|
|
help_text=_("Price asked to participants. Free with a scholarship."),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
description = models.TextField(
|
|
|
|
verbose_name=_("description"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date_start = models.DateField(
|
2020-05-05 11:57:19 +00:00
|
|
|
default=timezone.now,
|
2020-04-29 02:06:02 +00:00
|
|
|
verbose_name=_("date start"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date_end = models.DateField(
|
2020-05-05 11:57:19 +00:00
|
|
|
default=timezone.now,
|
2020-04-29 14:59:59 +00:00
|
|
|
verbose_name=_("date end"),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
date_inscription = models.DateTimeField(
|
2020-05-05 11:51:47 +00:00
|
|
|
default=timezone.now,
|
2020-04-29 02:06:02 +00:00
|
|
|
verbose_name=_("date of registration closing"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date_solutions = models.DateTimeField(
|
2020-05-05 11:51:47 +00:00
|
|
|
default=timezone.now,
|
2020-04-29 02:06:02 +00:00
|
|
|
verbose_name=_("date of maximal solution submission"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date_syntheses = models.DateTimeField(
|
2020-05-05 11:51:47 +00:00
|
|
|
default=timezone.now,
|
2020-05-05 00:20:45 +00:00
|
|
|
verbose_name=_("date of maximal syntheses submission for the first round"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date_solutions_2 = models.DateTimeField(
|
2020-05-05 11:51:47 +00:00
|
|
|
default=timezone.now,
|
2020-05-05 00:20:45 +00:00
|
|
|
verbose_name=_("date when solutions of round 2 are available"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date_syntheses_2 = models.DateTimeField(
|
2020-05-05 11:51:47 +00:00
|
|
|
default=timezone.now,
|
2020-05-05 00:20:45 +00:00
|
|
|
verbose_name=_("date of maximal syntheses submission for the second round"),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
final = models.BooleanField(
|
|
|
|
verbose_name=_("final tournament"),
|
2020-05-11 12:08:19 +00:00
|
|
|
help_text=_("It should be only one final tournament."),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
year = models.PositiveIntegerField(
|
2020-05-05 11:54:26 +00:00
|
|
|
default=os.getenv("TFJM_YEAR", timezone.now().year),
|
2020-04-29 13:29:01 +00:00
|
|
|
verbose_name=_("year"),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
2020-05-05 12:17:06 +00:00
|
|
|
@property
|
|
|
|
def teams(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get all teams that are registered to this tournament, with a distinction for the final tournament.
|
|
|
|
"""
|
2020-05-05 12:17:06 +00:00
|
|
|
return self._teams if not self.final else Team.objects.filter(selected_for_final=True)
|
|
|
|
|
2020-05-04 22:11:38 +00:00
|
|
|
@property
|
|
|
|
def linked_organizers(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Display a list of the organizers with links to their personal page.
|
|
|
|
"""
|
2020-05-04 22:11:38 +00:00
|
|
|
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
|
|
|
|
for user in self.organizers.all()]
|
|
|
|
|
2020-04-30 18:11:03 +00:00
|
|
|
@property
|
|
|
|
def solutions(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get all sent solutions for this tournament.
|
|
|
|
"""
|
2020-04-30 18:11:03 +00:00
|
|
|
from member.models import Solution
|
2020-05-04 22:11:38 +00:00
|
|
|
return Solution.objects.filter(final=self.final) if self.final \
|
2020-05-11 12:08:19 +00:00
|
|
|
else Solution.objects.filter(team__tournament=self, final=False)
|
2020-05-04 22:11:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def syntheses(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get all sent syntheses for this tournament.
|
|
|
|
"""
|
2020-05-04 22:11:38 +00:00
|
|
|
from member.models import Synthesis
|
|
|
|
return Synthesis.objects.filter(final=self.final) if self.final \
|
2020-05-11 12:08:19 +00:00
|
|
|
else Synthesis.objects.filter(team__tournament=self, final=False)
|
2020-04-30 18:11:03 +00:00
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
@classmethod
|
|
|
|
def get_final(cls):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get the final tournament.
|
|
|
|
This should exist and be unique.
|
|
|
|
"""
|
2020-04-29 02:06:02 +00:00
|
|
|
return cls.objects.get(year=os.getenv("TFJM_YEAR"), final=True)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("tournament")
|
|
|
|
verbose_name_plural = _("tournaments")
|
|
|
|
|
2020-05-05 15:23:33 +00:00
|
|
|
def send_mail_to_organizers(self, template_name, subject="Contact TFJM²", **kwargs):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Send a mail to all organizers of the tournament.
|
|
|
|
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
|
|
|
|
version and in templates/mail_templates/<template_name>.txt for the plain text version.
|
|
|
|
The context of the template contains the tournament and the user. Extra context can be given through the kwargs.
|
|
|
|
"""
|
2020-05-05 15:23:33 +00:00
|
|
|
context = kwargs
|
|
|
|
context["tournament"] = self
|
|
|
|
for user in self.organizers.all():
|
|
|
|
context["user"] = user
|
|
|
|
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
|
|
|
|
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
|
|
|
|
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
|
|
|
|
from member.models import TFJMUser
|
|
|
|
for user in TFJMUser.objects.get(is_superuser=True).all():
|
|
|
|
context["user"] = user
|
|
|
|
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
|
|
|
|
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
|
|
|
|
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
|
|
|
|
|
2020-04-29 04:52:39 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
|
|
|
|
class Team(models.Model):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Store information about a registered team.
|
|
|
|
"""
|
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
name = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("name"),
|
|
|
|
)
|
|
|
|
|
|
|
|
trigram = models.CharField(
|
|
|
|
max_length=3,
|
|
|
|
verbose_name=_("trigram"),
|
2020-05-11 12:08:19 +00:00
|
|
|
help_text=_("The trigram should be composed of 3 capitalize letters, that is a funny acronym for the team."),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
tournament = models.ForeignKey(
|
|
|
|
Tournament,
|
|
|
|
on_delete=models.PROTECT,
|
2020-05-05 12:17:06 +00:00
|
|
|
related_name="_teams",
|
2020-04-29 02:06:02 +00:00
|
|
|
verbose_name=_("tournament"),
|
2020-05-11 12:08:19 +00:00
|
|
|
help_text=_("The tournament where the team is registered."),
|
2020-04-29 02:06:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
inscription_date = models.DateTimeField(
|
|
|
|
auto_now_add=True,
|
|
|
|
verbose_name=_("inscription date"),
|
|
|
|
)
|
|
|
|
|
|
|
|
validation_status = models.CharField(
|
|
|
|
max_length=8,
|
|
|
|
choices=[
|
2020-04-29 14:26:52 +00:00
|
|
|
("0invalid", _("Registration not validated")),
|
|
|
|
("1waiting", _("Waiting for validation")),
|
2020-04-29 14:59:59 +00:00
|
|
|
("2valid", _("Registration validated")),
|
2020-04-29 02:06:02 +00:00
|
|
|
],
|
|
|
|
verbose_name=_("validation status"),
|
|
|
|
)
|
|
|
|
|
|
|
|
selected_for_final = models.BooleanField(
|
|
|
|
default=False,
|
|
|
|
verbose_name=_("selected for final"),
|
|
|
|
)
|
|
|
|
|
|
|
|
access_code = models.CharField(
|
|
|
|
max_length=6,
|
|
|
|
unique=True,
|
|
|
|
verbose_name=_("access code"),
|
|
|
|
)
|
|
|
|
|
|
|
|
year = models.PositiveIntegerField(
|
2020-05-05 11:54:26 +00:00
|
|
|
default=os.getenv("TFJM_YEAR", timezone.now().year),
|
2020-04-29 02:06:02 +00:00
|
|
|
verbose_name=_("year"),
|
|
|
|
)
|
|
|
|
|
2020-04-29 05:39:52 +00:00
|
|
|
@property
|
|
|
|
def valid(self):
|
2020-04-29 15:58:11 +00:00
|
|
|
return self.validation_status == "2valid"
|
2020-04-29 05:39:52 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def waiting(self):
|
2020-04-29 15:58:11 +00:00
|
|
|
return self.validation_status == "1waiting"
|
2020-04-29 05:39:52 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def invalid(self):
|
2020-04-29 15:58:11 +00:00
|
|
|
return self.validation_status == "0invalid"
|
2020-04-29 05:39:52 +00:00
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
@property
|
2020-05-11 12:08:19 +00:00
|
|
|
def coaches(self):
|
|
|
|
"""
|
|
|
|
Get all coaches of a team.
|
|
|
|
"""
|
2020-04-29 15:58:11 +00:00
|
|
|
return self.users.all().filter(role="2coach")
|
2020-04-29 02:06:02 +00:00
|
|
|
|
2020-05-04 18:21:53 +00:00
|
|
|
@property
|
2020-05-11 12:08:19 +00:00
|
|
|
def linked_coaches(self):
|
|
|
|
"""
|
|
|
|
Get a list of the coaches of a team with html links to their pages.
|
|
|
|
"""
|
2020-05-04 18:21:53 +00:00
|
|
|
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
|
2020-05-11 12:08:19 +00:00
|
|
|
for user in self.coaches]
|
2020-05-04 18:21:53 +00:00
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
@property
|
|
|
|
def participants(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get all particpants of a team, coaches excluded.
|
|
|
|
"""
|
2020-04-29 15:58:11 +00:00
|
|
|
return self.users.all().filter(role="3participant")
|
2020-04-29 02:06:02 +00:00
|
|
|
|
2020-05-04 18:21:53 +00:00
|
|
|
@property
|
|
|
|
def linked_participants(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get a list of the participants of a team with html links to their pages.
|
|
|
|
"""
|
2020-05-04 18:21:53 +00:00
|
|
|
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
|
|
|
|
for user in self.participants]
|
|
|
|
|
2020-05-05 21:19:18 +00:00
|
|
|
@property
|
|
|
|
def future_tournament(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get the last tournament where the team is registered.
|
|
|
|
Only matters if the team is selected for final: if this is the case, we return the final tournament.
|
|
|
|
Useful for deadlines.
|
|
|
|
"""
|
2020-05-05 21:19:18 +00:00
|
|
|
return Tournament.get_final() if self.selected_for_final else self.tournament
|
|
|
|
|
2020-05-05 00:20:45 +00:00
|
|
|
@property
|
|
|
|
def can_validate(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Check if a given team is able to ask for validation.
|
|
|
|
A team can validate if:
|
|
|
|
* All participants filled the photo consent
|
|
|
|
* Minor participants filled the parental consent
|
|
|
|
* Minor participants filled the sanitary plug
|
|
|
|
* Teams sent their motivation letter
|
|
|
|
* The team contains at least 4 participants
|
|
|
|
* The team contains at least 1 coach
|
|
|
|
"""
|
2020-05-05 00:20:45 +00:00
|
|
|
# TODO In a normal time, team needs a motivation letter and authorizations.
|
2020-05-11 12:08:19 +00:00
|
|
|
return self.coaches.exists() and self.participants.count() >= 4\
|
|
|
|
and self.tournament.date_inscription <= timezone.now()
|
2020-05-05 00:20:45 +00:00
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("team")
|
|
|
|
verbose_name_plural = _("teams")
|
|
|
|
unique_together = (('name', 'year',), ('trigram', 'year',),)
|
|
|
|
|
2020-05-05 15:12:24 +00:00
|
|
|
def send_mail(self, template_name, subject="Contact TFJM²", **kwargs):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Send a mail to all members of a team with a given template.
|
|
|
|
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
|
|
|
|
version and in templates/mail_templates/<template_name>.txt for the plain text version.
|
|
|
|
The context of the template contains the team and the user. Extra context can be given through the kwargs.
|
|
|
|
"""
|
2020-05-05 15:12:24 +00:00
|
|
|
context = kwargs
|
|
|
|
context["team"] = self
|
|
|
|
for user in self.users.all():
|
|
|
|
context["user"] = user
|
|
|
|
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
|
|
|
|
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
|
|
|
|
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
|
|
|
|
|
2020-04-29 04:52:39 +00:00
|
|
|
def __str__(self):
|
2020-06-02 20:07:54 +00:00
|
|
|
return self.trigram + " — " + self.name
|
2020-05-05 02:45:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Pool(models.Model):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Store information of a pool.
|
|
|
|
A pool is only a list of accessible solutions to some teams and some juries.
|
|
|
|
TODO: check that the set of teams is equal to the set of the teams that have a solution in this set.
|
|
|
|
TODO: Moreover, a team should send only one solution.
|
|
|
|
"""
|
2020-05-05 02:45:38 +00:00
|
|
|
teams = models.ManyToManyField(
|
|
|
|
Team,
|
|
|
|
related_name="pools",
|
|
|
|
verbose_name=_("teams"),
|
|
|
|
)
|
|
|
|
|
|
|
|
solutions = models.ManyToManyField(
|
|
|
|
"member.Solution",
|
|
|
|
related_name="pools",
|
|
|
|
verbose_name=_("solutions"),
|
|
|
|
)
|
|
|
|
|
|
|
|
round = models.PositiveIntegerField(
|
|
|
|
choices=[
|
|
|
|
(1, _("Round 1")),
|
|
|
|
(2, _("Round 2")),
|
|
|
|
],
|
|
|
|
verbose_name=_("round"),
|
|
|
|
)
|
|
|
|
|
|
|
|
juries = models.ManyToManyField(
|
|
|
|
"member.TFJMUser",
|
|
|
|
related_name="pools",
|
|
|
|
verbose_name=_("juries"),
|
|
|
|
)
|
|
|
|
|
2020-05-25 16:27:07 +00:00
|
|
|
extra_access_token = models.CharField(
|
|
|
|
max_length=64,
|
|
|
|
default="",
|
|
|
|
verbose_name=_("extra access token"),
|
|
|
|
help_text=_("Let other users access to the pool data without logging in."),
|
|
|
|
)
|
|
|
|
|
2020-05-05 02:45:38 +00:00
|
|
|
@property
|
|
|
|
def problems(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get problem numbers of the sent solutions as a list of integers.
|
|
|
|
"""
|
2020-05-05 02:45:38 +00:00
|
|
|
return list(d["problem"] for d in self.solutions.values("problem").all())
|
|
|
|
|
|
|
|
@property
|
|
|
|
def tournament(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get the concerned tournament.
|
|
|
|
We assume that the pool is correct, so all solutions belong to the same tournament.
|
|
|
|
"""
|
2020-05-05 02:45:38 +00:00
|
|
|
return self.solutions.first().tournament
|
|
|
|
|
|
|
|
@property
|
|
|
|
def syntheses(self):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Get the syntheses of the teams that are in this pool, for the correct round.
|
|
|
|
"""
|
2020-05-05 02:45:38 +00:00
|
|
|
from member.models import Synthesis
|
|
|
|
return Synthesis.objects.filter(team__in=self.teams.all(), round=self.round, final=self.tournament.final)
|
|
|
|
|
2020-05-25 16:27:07 +00:00
|
|
|
def save(self, **kwargs):
|
|
|
|
if not self.extra_access_token:
|
|
|
|
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
|
|
|
|
code = "".join(random.choice(alphabet) for _ in range(64))
|
|
|
|
self.extra_access_token = code
|
|
|
|
super().save(**kwargs)
|
|
|
|
|
2020-05-05 02:45:38 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("pool")
|
|
|
|
verbose_name_plural = _("pools")
|
2020-04-29 04:52:39 +00:00
|
|
|
|
2020-04-29 02:06:02 +00:00
|
|
|
|
|
|
|
class Payment(models.Model):
|
2020-05-11 12:08:19 +00:00
|
|
|
"""
|
|
|
|
Store some information about payments, to recover data.
|
|
|
|
TODO: handle it...
|
|
|
|
"""
|
2020-04-29 02:06:02 +00:00
|
|
|
user = models.OneToOneField(
|
|
|
|
'member.TFJMUser',
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name="payment",
|
|
|
|
verbose_name=_("user"),
|
|
|
|
)
|
|
|
|
|
|
|
|
team = models.ForeignKey(
|
|
|
|
Team,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name="payments",
|
|
|
|
verbose_name=_("team"),
|
|
|
|
)
|
|
|
|
|
|
|
|
method = models.CharField(
|
|
|
|
max_length=16,
|
|
|
|
choices=[
|
|
|
|
("not_paid", _("Not paid")),
|
|
|
|
("credit_card", _("Credit card")),
|
|
|
|
("check", _("Bank check")),
|
|
|
|
("transfer", _("Bank transfer")),
|
|
|
|
("cash", _("Cash")),
|
|
|
|
("scholarship", _("Scholarship")),
|
|
|
|
],
|
|
|
|
default="not_paid",
|
|
|
|
verbose_name=_("payment method"),
|
|
|
|
)
|
|
|
|
|
|
|
|
validation_status = models.CharField(
|
|
|
|
max_length=8,
|
|
|
|
choices=[
|
2020-04-29 14:26:52 +00:00
|
|
|
("0invalid", _("Registration not validated")),
|
|
|
|
("1waiting", _("Waiting for validation")),
|
|
|
|
("2valid", _("Registration validated")),
|
2020-04-29 02:06:02 +00:00
|
|
|
],
|
|
|
|
verbose_name=_("validation status"),
|
|
|
|
)
|
2020-04-29 02:51:25 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("payment")
|
|
|
|
verbose_name_plural = _("payments")
|
2020-04-29 04:52:39 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return _("Payment of {user}").format(str(self.user))
|