400 lines
11 KiB
Python
400 lines
11 KiB
Python
import os
|
|
from datetime import date
|
|
|
|
from django.contrib.auth.models import AbstractUser
|
|
from django.contrib.sites.models import Site
|
|
from django.db import models
|
|
from django.template import loader
|
|
from django.utils.encoding import force_bytes
|
|
from django.utils.http import urlsafe_base64_encode
|
|
from django.utils.translation import gettext_lazy as _
|
|
from polymorphic.models import PolymorphicModel
|
|
from tournament.models import Team, Tournament
|
|
|
|
from .tokens import email_validation_token
|
|
|
|
|
|
class TFJMUser(AbstractUser):
|
|
"""
|
|
The model of registered users (organizers/juries/admins/coachs/participants)
|
|
"""
|
|
USERNAME_FIELD = 'email'
|
|
REQUIRED_FIELDS = []
|
|
|
|
email = models.EmailField(
|
|
unique=True,
|
|
verbose_name=_("email"),
|
|
help_text=_("This should be valid and will be controlled."),
|
|
)
|
|
|
|
team = models.ForeignKey(
|
|
Team,
|
|
null=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name="users",
|
|
verbose_name=_("team"),
|
|
help_text=_("Concerns only coaches and participants."),
|
|
)
|
|
|
|
birth_date = models.DateField(
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("birth date"),
|
|
)
|
|
|
|
gender = models.CharField(
|
|
max_length=16,
|
|
null=True,
|
|
default=None,
|
|
choices=[
|
|
("male", _("Male")),
|
|
("female", _("Female")),
|
|
("non-binary", _("Non binary")),
|
|
],
|
|
verbose_name=_("gender"),
|
|
)
|
|
|
|
address = models.CharField(
|
|
max_length=255,
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("address"),
|
|
)
|
|
|
|
postal_code = models.PositiveIntegerField(
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("postal code"),
|
|
)
|
|
|
|
city = models.CharField(
|
|
max_length=255,
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("city"),
|
|
)
|
|
|
|
country = models.CharField(
|
|
max_length=255,
|
|
default="France",
|
|
null=True,
|
|
verbose_name=_("country"),
|
|
)
|
|
|
|
phone_number = models.CharField(
|
|
max_length=20,
|
|
null=True,
|
|
blank=True,
|
|
default=None,
|
|
verbose_name=_("phone number"),
|
|
)
|
|
|
|
school = models.CharField(
|
|
max_length=255,
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("school"),
|
|
)
|
|
|
|
student_class = models.CharField(
|
|
max_length=16,
|
|
choices=[
|
|
('seconde', _("Seconde or less")),
|
|
('première', _("Première")),
|
|
('terminale', _("Terminale")),
|
|
],
|
|
null=True,
|
|
default=None,
|
|
verbose_name="class",
|
|
)
|
|
|
|
responsible_name = models.CharField(
|
|
max_length=255,
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("responsible name"),
|
|
)
|
|
|
|
responsible_phone = models.CharField(
|
|
max_length=20,
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("responsible phone"),
|
|
)
|
|
|
|
responsible_email = models.EmailField(
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("responsible email"),
|
|
)
|
|
|
|
description = models.TextField(
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("description"),
|
|
)
|
|
|
|
role = models.CharField(
|
|
max_length=16,
|
|
choices=[
|
|
("0admin", _("Admin")),
|
|
("1volunteer", _("Organizer")),
|
|
("2coach", _("Coach")),
|
|
("3participant", _("Participant")),
|
|
]
|
|
)
|
|
|
|
year = models.PositiveIntegerField(
|
|
default=os.getenv("TFJM_YEAR", date.today().year),
|
|
verbose_name=_("year"),
|
|
)
|
|
|
|
email_confirmed = models.BooleanField(
|
|
verbose_name=_("email confirmed"),
|
|
default=False,
|
|
)
|
|
|
|
@property
|
|
def participates(self):
|
|
"""
|
|
Return True iff this user is a participant or a coach, ie. if the user is a member of a team that worked
|
|
for the tournament.
|
|
"""
|
|
return self.role == "3participant" or self.role == "2coach"
|
|
|
|
@property
|
|
def organizes(self):
|
|
"""
|
|
Return True iff this user is a local or global organizer of the tournament. This includes juries.
|
|
"""
|
|
return self.role == "1volunteer" or self.role == "0admin"
|
|
|
|
@property
|
|
def admin(self):
|
|
"""
|
|
Return True iff this user is a global organizer, ie. an administrator. This should be equivalent to be
|
|
a superuser.
|
|
"""
|
|
return self.role == "0admin"
|
|
|
|
class Meta:
|
|
verbose_name = _("user")
|
|
verbose_name_plural = _("users")
|
|
|
|
def save(self, *args, **kwargs):
|
|
# We ensure that the username is the email of the user.
|
|
self.username = self.email
|
|
super().save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return self.first_name + " " + self.last_name
|
|
|
|
def send_email_validation_link(self):
|
|
subject = "[TFJM²] " + str(_("Activate your Note Kfet account"))
|
|
token = email_validation_token.make_token(self)
|
|
uid = urlsafe_base64_encode(force_bytes(self.pk))
|
|
site = Site.objects.first()
|
|
message = loader.render_to_string('registration/mails/email_validation_email.txt',
|
|
{
|
|
'user': self,
|
|
'domain': site.domain,
|
|
'token': token,
|
|
'uid': uid,
|
|
})
|
|
html = loader.render_to_string('registration/mails/email_validation_email.html',
|
|
{
|
|
'user': self,
|
|
'domain': site.domain,
|
|
'token': token,
|
|
'uid': uid,
|
|
})
|
|
self.email_user(subject, message, html_message=html)
|
|
|
|
|
|
class Document(PolymorphicModel):
|
|
"""
|
|
Abstract model of any saved document (solution, synthesis, motivation letter, authorization)
|
|
"""
|
|
file = models.FileField(
|
|
unique=True,
|
|
verbose_name=_("file"),
|
|
)
|
|
|
|
uploaded_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_("uploaded at"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("document")
|
|
verbose_name_plural = _("documents")
|
|
|
|
def delete(self, *args, **kwargs):
|
|
self.file.delete(True)
|
|
return super().delete(*args, **kwargs)
|
|
|
|
|
|
class Authorization(Document):
|
|
"""
|
|
Model for authorization papers (parental consent, photo consent, sanitary plug, ...)
|
|
"""
|
|
user = models.ForeignKey(
|
|
TFJMUser,
|
|
on_delete=models.CASCADE,
|
|
related_name="authorizations",
|
|
verbose_name=_("user"),
|
|
)
|
|
|
|
type = models.CharField(
|
|
max_length=32,
|
|
choices=[
|
|
("parental_consent", _("Parental consent")),
|
|
("photo_consent", _("Photo consent")),
|
|
("sanitary_plug", _("Sanitary plug")),
|
|
("scholarship", _("Scholarship")),
|
|
],
|
|
verbose_name=_("type"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("authorization")
|
|
verbose_name_plural = _("authorizations")
|
|
|
|
def __str__(self):
|
|
return _("{authorization} for user {user}").format(authorization=self.type, user=str(self.user))
|
|
|
|
|
|
class MotivationLetter(Document):
|
|
"""
|
|
Model for motivation letters of a team.
|
|
"""
|
|
team = models.ForeignKey(
|
|
Team,
|
|
on_delete=models.CASCADE,
|
|
related_name="motivation_letters",
|
|
verbose_name=_("team"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("motivation letter")
|
|
verbose_name_plural = _("motivation letters")
|
|
|
|
def __str__(self):
|
|
return _("Motivation letter of team {team} ({trigram})").format(team=self.team.name, trigram=self.team.trigram)
|
|
|
|
|
|
class Solution(Document):
|
|
"""
|
|
Model for solutions of team for a given problem, for the regional or final tournament.
|
|
"""
|
|
team = models.ForeignKey(
|
|
Team,
|
|
on_delete=models.CASCADE,
|
|
related_name="solutions",
|
|
verbose_name=_("team"),
|
|
)
|
|
|
|
problem = models.PositiveSmallIntegerField(
|
|
verbose_name=_("problem"),
|
|
)
|
|
|
|
final = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("final solution"),
|
|
)
|
|
|
|
@property
|
|
def tournament(self):
|
|
"""
|
|
Get the concerned tournament of a solution.
|
|
Generally the local tournament of a team, but it can be the final tournament if this is a solution for the
|
|
final tournament.
|
|
"""
|
|
return Tournament.get_final() if self.final else self.team.tournament
|
|
|
|
class Meta:
|
|
verbose_name = _("solution")
|
|
verbose_name_plural = _("solutions")
|
|
unique_together = ('team', 'problem', 'final',)
|
|
|
|
def __str__(self):
|
|
if self.final:
|
|
return _("Solution of team {trigram} for problem {problem} for final")\
|
|
.format(trigram=self.team.trigram, problem=self.problem)
|
|
else:
|
|
return _("Solution of team {trigram} for problem {problem}")\
|
|
.format(trigram=self.team.trigram, problem=self.problem)
|
|
|
|
|
|
class Synthesis(Document):
|
|
"""
|
|
Model for syntheses of a team for a given round and for a given role, for the regional or final tournament.
|
|
"""
|
|
team = models.ForeignKey(
|
|
Team,
|
|
on_delete=models.CASCADE,
|
|
related_name="syntheses",
|
|
verbose_name=_("team"),
|
|
)
|
|
|
|
source = models.CharField(
|
|
max_length=16,
|
|
choices=[
|
|
("opponent", _("Opponent")),
|
|
("rapporteur", _("Rapporteur")),
|
|
],
|
|
verbose_name=_("source"),
|
|
)
|
|
|
|
round = models.PositiveSmallIntegerField(
|
|
choices=[
|
|
(1, _("Round 1")),
|
|
(2, _("Round 2")),
|
|
],
|
|
verbose_name=_("round"),
|
|
)
|
|
|
|
final = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("final synthesis"),
|
|
)
|
|
|
|
@property
|
|
def tournament(self):
|
|
"""
|
|
Get the concerned tournament of a solution.
|
|
Generally the local tournament of a team, but it can be the final tournament if this is a solution for the
|
|
final tournament.
|
|
"""
|
|
return Tournament.get_final() if self.final else self.team.tournament
|
|
|
|
class Meta:
|
|
verbose_name = _("synthesis")
|
|
verbose_name_plural = _("syntheses")
|
|
unique_together = ('team', 'source', 'round', 'final',)
|
|
|
|
def __str__(self):
|
|
return _("Synthesis of team {trigram} that is {source} for the round {round} of tournament {tournament}")\
|
|
.format(trigram=self.team.trigram, source=self.get_source_display().lower(), round=self.round,
|
|
tournament=self.tournament)
|
|
|
|
|
|
class Config(models.Model):
|
|
"""
|
|
Dictionary of configuration variables.
|
|
"""
|
|
key = models.CharField(
|
|
max_length=255,
|
|
primary_key=True,
|
|
verbose_name=_("key"),
|
|
)
|
|
|
|
value = models.TextField(
|
|
default="",
|
|
verbose_name=_("value"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("configuration")
|
|
verbose_name_plural = _("configurations")
|