576 lines
20 KiB
Python
576 lines
20 KiB
Python
# Copyright (C) 2020 by Animath
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from datetime import date
|
|
|
|
from django.contrib.sites.models import Site
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
from django.db import models
|
|
from django.template import loader
|
|
from django.urls import reverse_lazy
|
|
from django.utils import timezone
|
|
from django.utils.crypto import get_random_string
|
|
from django.utils.encoding import force_bytes
|
|
from django.utils.http import urlsafe_base64_encode
|
|
from django.utils.text import format_lazy
|
|
from django.utils.translation import gettext_lazy as _
|
|
from phonenumber_field.modelfields import PhoneNumberField
|
|
from polymorphic.models import PolymorphicModel
|
|
from tfjm.tokens import email_validation_token
|
|
|
|
|
|
class Registration(PolymorphicModel):
|
|
"""
|
|
Registrations store extra content that are not asked in the User Model.
|
|
This is specific to the role of the user, see StudentRegistration,
|
|
CoachRegistration or VolunteerRegistration.
|
|
"""
|
|
user = models.OneToOneField(
|
|
"auth.User",
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("user"),
|
|
)
|
|
|
|
give_contact_to_animath = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Grant Animath to contact me in the future about other actions"),
|
|
)
|
|
|
|
email_confirmed = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("email confirmed"),
|
|
)
|
|
|
|
def send_email_validation_link(self):
|
|
"""
|
|
The account got created or the email got changed.
|
|
Send an email that contains a link to validate the address.
|
|
"""
|
|
subject = "[TFJM²] " + str(_("Activate your TFJM² account"))
|
|
token = email_validation_token.make_token(self.user)
|
|
uid = urlsafe_base64_encode(force_bytes(self.user.pk))
|
|
site = Site.objects.first()
|
|
message = loader.render_to_string('registration/mails/email_validation_email.txt',
|
|
{
|
|
'user': self.user,
|
|
'domain': site.domain,
|
|
'token': token,
|
|
'uid': uid,
|
|
})
|
|
html = loader.render_to_string('registration/mails/email_validation_email.html',
|
|
{
|
|
'user': self.user,
|
|
'domain': site.domain,
|
|
'token': token,
|
|
'uid': uid,
|
|
})
|
|
self.user.email_user(subject, message, html_message=html)
|
|
|
|
@property
|
|
def type(self): # pragma: no cover
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def form_class(self): # pragma: no cover
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def participates(self):
|
|
return isinstance(self, ParticipantRegistration)
|
|
|
|
@property
|
|
def is_admin(self):
|
|
return isinstance(self, VolunteerRegistration) and self.admin or self.user.is_superuser
|
|
|
|
@property
|
|
def is_volunteer(self):
|
|
return isinstance(self, VolunteerRegistration)
|
|
|
|
def get_absolute_url(self):
|
|
return reverse_lazy("registration:user_detail", args=(self.user_id,))
|
|
|
|
def registration_informations(self):
|
|
return []
|
|
|
|
def important_informations(self):
|
|
informations = []
|
|
if not self.email_confirmed:
|
|
text = _("Your email address is not validated. Please click on the link you received by email. "
|
|
"You can resend a mail by clicking on <a href=\"{send_email_url}\">this link</a>.")
|
|
send_email_url = reverse_lazy("registration:email_validation_resend", args=(self.user_id,))
|
|
content = format_lazy(text, send_email_url=send_email_url)
|
|
informations.append({
|
|
'title': "Validation e-mail",
|
|
'type': "warning",
|
|
'priority': 0,
|
|
'content': content,
|
|
})
|
|
|
|
informations.extend(self.registration_informations())
|
|
|
|
informations.sort(key=lambda info: (info['priority'], info['title']))
|
|
return informations
|
|
|
|
def __str__(self):
|
|
return f"{self.user.first_name} {self.user.last_name}"
|
|
|
|
class Meta:
|
|
verbose_name = _("registration")
|
|
verbose_name_plural = _("registrations")
|
|
|
|
|
|
def get_random_photo_filename(instance, filename):
|
|
return "authorization/photo/" + get_random_string(64)
|
|
|
|
|
|
def get_random_health_filename(instance, filename):
|
|
return "authorization/health/" + get_random_string(64)
|
|
|
|
|
|
def get_random_vaccine_filename(instance, filename):
|
|
return "authorization/vaccine/" + get_random_string(64)
|
|
|
|
|
|
def get_random_parental_filename(instance, filename):
|
|
return "authorization/parental/" + get_random_string(64)
|
|
|
|
|
|
class ParticipantRegistration(Registration):
|
|
team = models.ForeignKey(
|
|
"participation.Team",
|
|
related_name="participants",
|
|
on_delete=models.PROTECT,
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
verbose_name=_("team"),
|
|
)
|
|
|
|
gender = models.CharField(
|
|
max_length=6,
|
|
verbose_name=_("gender"),
|
|
choices=[
|
|
("female", _("Female")),
|
|
("male", _("Male")),
|
|
("other", _("Other")),
|
|
],
|
|
default="other",
|
|
)
|
|
|
|
address = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_("address"),
|
|
)
|
|
|
|
zip_code = models.PositiveIntegerField(
|
|
verbose_name=_("zip code"),
|
|
validators=[MinValueValidator(1000), MaxValueValidator(99999)],
|
|
)
|
|
|
|
city = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_("city"),
|
|
)
|
|
|
|
phone_number = PhoneNumberField(
|
|
verbose_name=_("phone number"),
|
|
blank=True,
|
|
)
|
|
|
|
health_issues = models.TextField(
|
|
verbose_name=_("health issues"),
|
|
blank=True,
|
|
help_text=_("You can indicate here your allergies or anything that is important to know for organizers."),
|
|
)
|
|
|
|
housing_constraints = models.TextField(
|
|
verbose_name=_("housing constraints"),
|
|
blank=True,
|
|
help_text=_("You can fill in something here if you have any housing constraints, "
|
|
"e.g. medical problems, scheduling issues, gender issues, "
|
|
"or anything else you feel is relevant to the organizers. "
|
|
"Leave empty if you have nothing specific to declare."),
|
|
)
|
|
|
|
photo_authorization = models.FileField(
|
|
verbose_name=_("photo authorization"),
|
|
upload_to=get_random_photo_filename,
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
@property
|
|
def under_18(self):
|
|
if isinstance(self, CoachRegistration):
|
|
return False # In normal case
|
|
important_date = timezone.now().date()
|
|
if self.team and self.team.participation.tournament:
|
|
important_date = self.team.participation.tournament.date_start
|
|
if self.team.participation.final:
|
|
from participation.models import Tournament
|
|
important_date = Tournament.final_tournament().date_start
|
|
return (important_date - self.birth_date).days < 18 * 365.24
|
|
|
|
@property
|
|
def type(self): # pragma: no cover
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def form_class(self): # pragma: no cover
|
|
raise NotImplementedError
|
|
|
|
def registration_informations(self):
|
|
informations = []
|
|
if not self.team:
|
|
text = _("You are not in a team. You can <a href=\"{create_url}\">create one</a> "
|
|
"or <a href=\"{join_url}\">join an existing one</a> to participate.")
|
|
create_url = reverse_lazy("participation:create_team")
|
|
join_url = reverse_lazy("participation:join_team")
|
|
content = format_lazy(text, create_url=create_url, join_url=join_url)
|
|
informations.append({
|
|
'title': _("No team"),
|
|
'type': "danger",
|
|
'priority': 1,
|
|
'content': content,
|
|
})
|
|
else:
|
|
if self.team.participation.tournament:
|
|
if not self.photo_authorization:
|
|
text = _("You have not uploaded your photo authorization. "
|
|
"You can do it by clicking on <a href=\"{photo_url}\">this link</a>.")
|
|
photo_url = reverse_lazy("registration:upload_user_photo_authorization", args=(self.id,))
|
|
content = format_lazy(text, photo_url=photo_url)
|
|
informations.append({
|
|
'title': _("Photo authorization"),
|
|
'type': "danger",
|
|
'priority': 5,
|
|
'content': content,
|
|
})
|
|
|
|
informations.extend(self.team.important_informations())
|
|
|
|
return informations
|
|
|
|
class Meta:
|
|
verbose_name = _("participant registration")
|
|
verbose_name_plural = _("participant registrations")
|
|
|
|
|
|
class StudentRegistration(ParticipantRegistration):
|
|
"""
|
|
Specific registration for students.
|
|
They have a team, a student class and a school.
|
|
"""
|
|
birth_date = models.DateField(
|
|
verbose_name=_("birth date"),
|
|
default=date.today,
|
|
)
|
|
|
|
student_class = models.IntegerField(
|
|
choices=[
|
|
(12, _("12th grade")),
|
|
(11, _("11th grade")),
|
|
(10, _("10th grade or lower")),
|
|
],
|
|
verbose_name=_("student class"),
|
|
)
|
|
|
|
school = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_("school"),
|
|
)
|
|
|
|
responsible_name = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_("responsible name"),
|
|
default="",
|
|
)
|
|
|
|
responsible_phone = PhoneNumberField(
|
|
verbose_name=_("responsible phone number"),
|
|
default="",
|
|
)
|
|
|
|
responsible_email = models.EmailField(
|
|
verbose_name=_("responsible email address"),
|
|
default="",
|
|
)
|
|
|
|
parental_authorization = models.FileField(
|
|
verbose_name=_("parental authorization"),
|
|
upload_to=get_random_parental_filename,
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
health_sheet = models.FileField(
|
|
verbose_name=_("health sheet"),
|
|
upload_to=get_random_health_filename,
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
vaccine_sheet = models.FileField(
|
|
verbose_name=_("vaccine sheet"),
|
|
upload_to=get_random_vaccine_filename,
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
@property
|
|
def type(self):
|
|
return _("student")
|
|
|
|
@property
|
|
def form_class(self):
|
|
from registration.forms import StudentRegistrationForm
|
|
return StudentRegistrationForm
|
|
|
|
def registration_informations(self):
|
|
informations = super().registration_informations()
|
|
if self.team and self.team.participation.tournament and self.under_18:
|
|
if not self.parental_authorization:
|
|
text = _("You have not uploaded your parental authorization. "
|
|
"You can do it by clicking on <a href=\"{parental_url}\">this link</a>.")
|
|
parental_url = reverse_lazy("registration:upload_user_parental_authorization", args=(self.id,))
|
|
content = format_lazy(text, parental_url=parental_url)
|
|
informations.append({
|
|
'title': _("Parental authorization"),
|
|
'type': "danger",
|
|
'priority': 5,
|
|
'content': content,
|
|
})
|
|
if not self.health_sheet:
|
|
text = _("You have not uploaded your health sheet. "
|
|
"You can do it by clicking on <a href=\"{health_url}\">this link</a>.")
|
|
health_url = reverse_lazy("registration:upload_user_health_sheet", args=(self.id,))
|
|
content = format_lazy(text, health_url=health_url)
|
|
informations.append({
|
|
'title': _("Health sheet"),
|
|
'type': "danger",
|
|
'priority': 5,
|
|
'content': content,
|
|
})
|
|
if not self.vaccine_sheet:
|
|
text = _("You have not uploaded your vaccine sheet. "
|
|
"You can do it by clicking on <a href=\"{vaccine_url}\">this link</a>.")
|
|
vaccine_url = reverse_lazy("registration:upload_user_vaccine_sheet", args=(self.id,))
|
|
content = format_lazy(text, vaccine_url=vaccine_url)
|
|
informations.append({
|
|
'title': _("Vaccine sheet"),
|
|
'type': "danger",
|
|
'priority': 5,
|
|
'content': content,
|
|
})
|
|
|
|
if self.team and self.team.participation.valid:
|
|
for payment in self.payments.all():
|
|
if payment.valid is False:
|
|
text = _("You have to pay {amount} € for your registration, or send a scholarship "
|
|
"notification or a payment proof. "
|
|
"You can do it on <a href=\"{url}\">the payment page</a>.")
|
|
url = reverse_lazy("registration:update_payment", args=(payment.id,))
|
|
content = format_lazy(text, amount=payment.amount, url=url)
|
|
informations.append({
|
|
'title': _("Payment"),
|
|
'type': "danger",
|
|
'priority': 3,
|
|
'content': content,
|
|
})
|
|
elif self.payment.valid is None:
|
|
text = _("Your payment is under approval.")
|
|
content = text
|
|
informations.append({
|
|
'title': _("Payment"),
|
|
'type': "warning",
|
|
'priority': 3,
|
|
'content': content,
|
|
})
|
|
|
|
return informations
|
|
|
|
class Meta:
|
|
verbose_name = _("student registration")
|
|
verbose_name_plural = _("student registrations")
|
|
|
|
|
|
class CoachRegistration(ParticipantRegistration):
|
|
"""
|
|
Specific registration for coaches.
|
|
They have a team and a professional activity.
|
|
"""
|
|
last_degree = models.CharField(
|
|
max_length=255,
|
|
default="",
|
|
verbose_name=_("most recent degree in mathematics, computer science or physics"),
|
|
help_text=_("Your most recent degree in maths, computer science or physics, "
|
|
"or your last entrance exam (CAPES, Agrégation,…)"),
|
|
)
|
|
|
|
professional_activity = models.TextField(
|
|
verbose_name=_("professional activity"),
|
|
)
|
|
|
|
@property
|
|
def type(self):
|
|
return _("coach")
|
|
|
|
@property
|
|
def form_class(self):
|
|
from registration.forms import CoachRegistrationForm
|
|
return CoachRegistrationForm
|
|
|
|
class Meta:
|
|
verbose_name = _("coach registration")
|
|
verbose_name_plural = _("coach registrations")
|
|
|
|
|
|
class VolunteerRegistration(Registration):
|
|
"""
|
|
Specific registration for organizers and juries.
|
|
"""
|
|
professional_activity = models.TextField(
|
|
verbose_name=_("professional activity"),
|
|
)
|
|
|
|
admin = models.BooleanField(
|
|
verbose_name=_("administrator"),
|
|
help_text=_("An administrator has all rights. Please don't give this right to all juries and volunteers."),
|
|
default=False,
|
|
)
|
|
|
|
@property
|
|
def interesting_tournaments(self) -> set:
|
|
return set(self.organized_tournaments.all()).union(map(lambda pool: pool.tournament, self.jury_in.all()))
|
|
|
|
@property
|
|
def type(self):
|
|
return _('admin') if self.is_admin else _('volunteer')
|
|
|
|
@property
|
|
def form_class(self):
|
|
from registration.forms import VolunteerRegistrationForm
|
|
return VolunteerRegistrationForm
|
|
|
|
def important_informations(self):
|
|
informations = []
|
|
|
|
for tournament in self.organized_tournaments.all():
|
|
if timezone.now() < tournament.inscription_limit \
|
|
or tournament.participations.filter(valid=True).count() < tournament.max_teams:
|
|
text = _("Registrations for tournament {tournament} are closing on {date:%Y-%m-%d %H:%M}. "
|
|
"There are for now {validated_teams} validated teams (+ {pending_teams} pending) "
|
|
"on {max_teams} expected.")
|
|
content = format_lazy(text, tournament=tournament.name, date=tournament.inscription_limit,
|
|
validated_teams=tournament.participations.filter(valid=True).count(),
|
|
pending_teams=tournament.participations.filter(valid=False).count(),
|
|
max_teams=tournament.max_teams)
|
|
informations.append({
|
|
'title': _("Registrations"),
|
|
'type': "info",
|
|
'priority': 2,
|
|
'content': content,
|
|
})
|
|
|
|
for pending_participation in tournament.participations.filter(valid=False).all():
|
|
text = _("The team {trigram} requested to be validated for the tournament of {tournament}. "
|
|
"You can check the status of the team on the <a href=\"{url}\">team page</a>.")
|
|
url = reverse_lazy("participation:team_detail", args=(pending_participation.team.id,))
|
|
content = format_lazy(text, trigram=pending_participation.team.trigram,
|
|
tournament=tournament.name, url=url)
|
|
informations.append({
|
|
'title': _("Pending validation"),
|
|
'type': "warning",
|
|
'priority': 4,
|
|
'content': content,
|
|
})
|
|
|
|
return informations
|
|
|
|
class Meta:
|
|
verbose_name = _("volunteer registration")
|
|
verbose_name_plural = _("volunteer registrations")
|
|
|
|
|
|
def get_scholarship_filename(instance, filename):
|
|
return f"authorization/scholarship/scholarship_{instance.registration.pk}"
|
|
|
|
|
|
class Payment(models.Model):
|
|
registrations = models.ManyToManyField(
|
|
ParticipantRegistration,
|
|
related_name="payments",
|
|
verbose_name=_("registrations"),
|
|
)
|
|
|
|
grouped = models.BooleanField(
|
|
verbose_name=_("grouped"),
|
|
default=False,
|
|
help_text=_("If set to true, then one payment is made for the full team, "
|
|
"for example if the school pays for all."),
|
|
)
|
|
|
|
amount = models.PositiveSmallIntegerField(
|
|
verbose_name=_("total amount"),
|
|
help_text=_("Corresponds to the total required amount to pay, in euros."),
|
|
default=0,
|
|
)
|
|
|
|
final = models.BooleanField(
|
|
verbose_name=_("for final tournament"),
|
|
default=False,
|
|
)
|
|
|
|
type = models.CharField(
|
|
verbose_name=_("type"),
|
|
max_length=16,
|
|
choices=[
|
|
('', _("No payment")),
|
|
('helloasso', "Hello Asso"),
|
|
('scholarship', _("Scholarship")),
|
|
('bank_transfer', _("Bank transfer")),
|
|
('other', _("Other (please indicate)")),
|
|
('free', _("The tournament is free")),
|
|
],
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
checkout_intent_id = models.IntegerField(
|
|
verbose_name=_("Hello Asso checkout intent ID"),
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
|
|
receipt = models.FileField(
|
|
verbose_name=_("receipt"),
|
|
help_text=_("only if you have a scholarship or if you chose a bank transfer."),
|
|
upload_to=get_scholarship_filename,
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
additional_information = models.TextField(
|
|
verbose_name=_("additional information"),
|
|
help_text=_("To help us to find your payment."),
|
|
blank=True,
|
|
default="",
|
|
)
|
|
|
|
valid = models.BooleanField(
|
|
verbose_name=_("payment valid"),
|
|
null=True,
|
|
default=False,
|
|
)
|
|
|
|
def get_absolute_url(self):
|
|
return reverse_lazy("registration:update_payment", args=(self.pk,))
|
|
|
|
def __str__(self):
|
|
return _("Payment of {registrations}").format(registrations=", ".join(map(str, self.registrations.all())))
|
|
|
|
class Meta:
|
|
verbose_name = _("payment")
|
|
verbose_name_plural = _("payments")
|