mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-10-31 15:40:01 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			860 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			860 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright (C) 2020 by Animath
 | |
| # SPDX-License-Identifier: GPL-3.0-or-later
 | |
| 
 | |
| from datetime import date
 | |
| 
 | |
| from django.conf import settings
 | |
| from django.contrib.sites.models import Site
 | |
| from django.core.mail import send_mail
 | |
| from django.core.validators import MaxValueValidator, MinValueValidator
 | |
| from django.db import models
 | |
| from django.db.models import Q
 | |
| from django.template import loader
 | |
| from django.urls import reverse, reverse_lazy
 | |
| from django.utils import timezone, translation
 | |
| 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.timezone import localtime
 | |
| from django.utils.translation import gettext_lazy as _
 | |
| from phonenumber_field.modelfields import PhoneNumberField
 | |
| from polymorphic.models import PolymorphicModel
 | |
| from tfjm import helloasso
 | |
| 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 = f"[{settings.APP_NAME}] " + str(_("Activate your 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_student(self):
 | |
|         return isinstance(self, StudentRegistration)
 | |
| 
 | |
|     @property
 | |
|     def is_coach(self):
 | |
|         return isinstance(self, CoachRegistration)
 | |
| 
 | |
|     @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")),
 | |
|         ],
 | |
|     )
 | |
| 
 | |
|     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"),
 | |
|     )
 | |
| 
 | |
|     country = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("country"),
 | |
|         default="France",
 | |
|     )
 | |
| 
 | |
|     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="",
 | |
|     )
 | |
| 
 | |
|     photo_authorization_final = models.FileField(
 | |
|         verbose_name=_("photo authorization (final)"),
 | |
|         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 = localtime(timezone.now()).date()
 | |
|         if self.team and self.team.participation.tournament:
 | |
|             important_date = self.team.participation.tournament.date_start
 | |
|         birth_date = self.birth_date
 | |
|         if birth_date.month == 2 and birth_date.day == 29:
 | |
|             # If the birth date is the 29th of February, we consider it as the 1st of March
 | |
|             birth_date = birth_date.replace(month=3, day=1)
 | |
|         over_18_on = birth_date.replace(year=birth_date.year + 18)
 | |
|         return important_date < over_18_on
 | |
| 
 | |
|     @property
 | |
|     def under_18_final(self):
 | |
|         if isinstance(self, CoachRegistration):
 | |
|             return False  # In normal case
 | |
|         from participation.models import Tournament
 | |
|         important_date = Tournament.final_tournament().date_start
 | |
|         birth_date = self.birth_date
 | |
|         if birth_date.month == 2 and birth_date.day == 29:
 | |
|             # If the birth date is the 29th of February, we consider it as the 1st of March
 | |
|             birth_date = birth_date.replace(month=3, day=1)
 | |
|         over_18_on = birth_date.replace(year=birth_date.year + 18)
 | |
|         return important_date < over_18_on
 | |
| 
 | |
|     @property
 | |
|     def type(self):  # pragma: no cover
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     @property
 | |
|     def form_class(self):  # pragma: no cover
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def registration_informations(self):
 | |
|         from survey.models import Survey
 | |
| 
 | |
|         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,
 | |
|                     })
 | |
| 
 | |
|                 if self.team.participation.final:
 | |
|                     if not self.photo_authorization_final:
 | |
|                         text = _("You have not uploaded your photo authorization for the final tournament. "
 | |
|                                  "You can do it by clicking on <a href=\"{photo_url}\">this link</a>.")
 | |
|                         photo_url = reverse_lazy("registration:upload_user_photo_authorization_final", args=(self.id,))
 | |
|                         content = format_lazy(text, photo_url=photo_url)
 | |
|                         informations.append({
 | |
|                             'title': _("Photo authorization"),
 | |
|                             'type': "danger",
 | |
|                             'priority': 5,
 | |
|                             'content': content,
 | |
|                         })
 | |
| 
 | |
|                 if self.team.participation.valid:
 | |
|                     for survey in Survey.objects.filter(Q(tournament__isnull=True) | Q(tournament=self.team.participation.tournament),
 | |
|                                                         Q(invite_team=False), Q(invite_coaches=True) | Q(invite_coaches=self.is_coach),
 | |
|                                                         ~Q(completed_registrations=self)):
 | |
|                         text = _("Please answer to the survey \"{name}\". You can go to the survey on <a href=\"{survey_link}\">that link</a>, "
 | |
|                                  "using the token code you received by mail.")
 | |
|                         content = format_lazy(text, name=survey.name, survey_link=f"{settings.LIMESURVEY_URL}/index.php/{survey.survey_id}")
 | |
|                         informations.append({
 | |
|                             'title': _("Required answer to survey"),
 | |
|                             'type': "warning",
 | |
|                             'priority': 12,
 | |
|                             'content': content
 | |
|                         })
 | |
| 
 | |
|             informations.extend(self.team.important_informations())
 | |
| 
 | |
|         return informations
 | |
| 
 | |
|     def send_email_final_selection(self):
 | |
|         """
 | |
|         The team is selected for final.
 | |
|         """
 | |
|         with translation.override(settings.PREFERRED_LANGUAGE_CODE):
 | |
|             subject = f"[{settings.APP_NAME}] " + str(_("Team selected for the final tournament"))
 | |
|             site = Site.objects.first()
 | |
|             from participation.models import Tournament
 | |
|             tournament = Tournament.final_tournament()
 | |
|             payment = self.payments.filter(final=True).first() if self.is_student else None
 | |
|             message = loader.render_to_string('registration/mails/final_selection.txt',
 | |
|                                               {
 | |
|                                                   'user': self.user,
 | |
|                                                   'domain': site.domain,
 | |
|                                                   'tournament': tournament,
 | |
|                                                   'payment': payment,
 | |
|                                               })
 | |
|             html = loader.render_to_string('registration/mails/final_selection.html',
 | |
|                                            {
 | |
|                                                'user': self.user,
 | |
|                                                'domain': site.domain,
 | |
|                                                'tournament': tournament,
 | |
|                                                'payment': payment,
 | |
|                                            })
 | |
|             self.user.email_user(subject, message, html_message=html)
 | |
| 
 | |
|     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="",
 | |
|     )
 | |
| 
 | |
|     parental_authorization_final = models.FileField(
 | |
|         verbose_name=_("parental authorization (final)"),
 | |
|         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 settings.HEALTH_SHEET_REQUIRED and 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 settings.VACCINE_SHEET_REQUIRED and 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 payment.valid is None:
 | |
|                     text = _("Your payment is under approval.")
 | |
|                     content = text
 | |
|                     informations.append({
 | |
|                         'title': _("Payment"),
 | |
|                         'type': "warning",
 | |
|                         'priority': 3,
 | |
|                         'content': content,
 | |
|                     })
 | |
| 
 | |
|             if self.team.participation.final:
 | |
|                 if self.under_18_final and not self.parental_authorization_final:
 | |
|                     text = _("You have not uploaded your parental authorization for the final tournament. "
 | |
|                              "You can do it by clicking on <a href=\"{parental_url}\">this link</a>.")
 | |
|                     parental_url = reverse_lazy("registration:upload_user_parental_authorization_final",
 | |
|                                                 args=(self.id,))
 | |
|                     content = format_lazy(text, parental_url=parental_url)
 | |
|                     informations.append({
 | |
|                         'title': _("Parental authorization"),
 | |
|                         'type': "danger",
 | |
|                         'priority': 5,
 | |
|                         '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=localtime(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,
 | |
|                 })
 | |
| 
 | |
|             payments = Payment.objects.filter(registrations__team__participation__tournament=tournament).all()
 | |
|             valid = payments.filter(valid=True).count()
 | |
|             pending = payments.filter(valid=None).count()
 | |
|             invalid = payments.filter(valid=False).count()
 | |
|             if pending + invalid > 0:
 | |
|                 text = _("There are {valid} validated payments, {pending} pending and {invalid} invalid for the "
 | |
|                          "tournament of {tournament}. You can check the status of the payments on the "
 | |
|                          "<a href=\"{url}\">payments list</a>.")
 | |
|                 url = reverse_lazy("participation:tournament_payments", args=(tournament.id,))
 | |
|                 content = format_lazy(text, valid=valid, pending=pending, invalid=invalid, tournament=tournament.name,
 | |
|                                       url=url)
 | |
|                 informations.append({
 | |
|                     'title': _("Payments"),
 | |
|                     'type': "info",
 | |
|                     'priority': 5,
 | |
|                     'content': content,
 | |
|                 })
 | |
| 
 | |
|             if timezone.now() > tournament.solution_limit and timezone.now() < tournament.solutions_draw:
 | |
|                 text = _("<p>The draw of the solutions for the tournament {tournament} is planned on the "
 | |
|                          "{date:%Y-%m-%d %H:%M}. You can join it on <a href='{url}'>this link</a>.</p>")
 | |
|                 url = reverse_lazy("draw:index")
 | |
|                 content = format_lazy(text, tournament=tournament.name,
 | |
|                                       date=localtime(tournament.solutions_draw),
 | |
|                                       url=url)
 | |
|                 informations.append({
 | |
|                     'title': _("Draw of solutions"),
 | |
|                     'type': "info",
 | |
|                     'priority': 1,
 | |
|                     'content': content,
 | |
|                 })
 | |
| 
 | |
|         for tournament in self.interesting_tournaments:
 | |
|             pools = tournament.pools.filter(juries=self).order_by('round').all()
 | |
|             for pool in pools:
 | |
|                 if pool.round == 1 and timezone.now().date() <= tournament.date_start:
 | |
|                     text = _("<p>You are in the jury of the pool {pool} for the tournament of {tournament}. "
 | |
|                              "You can find the pool page <a href='{pool_url}'>here</a>.</p>")
 | |
|                     pool_url = reverse_lazy("participation:pool_detail", args=(pool.id,))
 | |
|                     content = format_lazy(text, pool=pool.short_name, tournament=tournament.name, pool_url=pool_url)
 | |
|                     informations.append({
 | |
|                         'title': _("First round"),
 | |
|                         'type': "info",
 | |
|                         'priority': 1,
 | |
|                         'content': content,
 | |
|                     })
 | |
|                 elif pool.round == 2 and timezone.now().date() <= tournament.date_end:
 | |
|                     text = _("<p>You are in the jury of the pool {pool} for the tournament of {tournament}. "
 | |
|                              "You can find the pool page <a href='{pool_url}'>here</a>.</p>")
 | |
|                     pool_url = reverse_lazy("participation:pool_detail", args=(pool.id,))
 | |
|                     content = format_lazy(text, pool=pool.short_name, tournament=tournament.name, pool_url=pool_url)
 | |
|                     informations.append({
 | |
|                         'title': _("Second round"),
 | |
|                         'type': "info",
 | |
|                         'priority': 2,
 | |
|                         'content': content,
 | |
|                     })
 | |
| 
 | |
|                 for note in self.notes.filter(passage__pool=pool).all():
 | |
|                     if not note.has_any_note():
 | |
|                         text = _("<p>You don't have given any note as a jury for the passage {passage} "
 | |
|                                  "in the pool {pool} of {tournament}. "
 | |
|                                  "You can set your notes <a href='{passage_url}'>here</a>.</p>")
 | |
|                         passage_url = reverse_lazy("participation:passage_detail", args=(note.passage.id,))
 | |
|                         content = format_lazy(text, passage=note.passage.position, pool=pool.short_name,
 | |
|                                               tournament=tournament.name, passage_url=passage_url)
 | |
|                         informations.append({
 | |
|                             'title': _("Note"),
 | |
|                             'type': "warning",
 | |
|                             'priority': 3 + note.passage.position,
 | |
|                             'content': content,
 | |
|                         })
 | |
| 
 | |
|         return informations
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("volunteer registration")
 | |
|         verbose_name_plural = _("volunteer registrations")
 | |
| 
 | |
| 
 | |
| def get_receipt_filename(instance, filename):
 | |
|     return f"authorization/receipt/receipt_{instance.id}"
 | |
| 
 | |
| 
 | |
| def get_random_token():
 | |
|     return get_random_string(32)
 | |
| 
 | |
| 
 | |
| 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,
 | |
|     )
 | |
| 
 | |
|     token = models.CharField(
 | |
|         verbose_name=_("token"),
 | |
|         max_length=32,
 | |
|         default=get_random_token,
 | |
|         help_text=_("A token to authorize external users to make this payment."),
 | |
|     )
 | |
| 
 | |
|     final = models.BooleanField(
 | |
|         verbose_name=_("for final tournament"),
 | |
|         default=False,
 | |
|     )
 | |
| 
 | |
|     type = models.CharField(
 | |
|         verbose_name=_("type"),
 | |
|         max_length=16,
 | |
|         choices=[
 | |
|             ('', _("No payment")),
 | |
|             ('helloasso', _("Credit card")),
 | |
|             ('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_receipt_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,
 | |
|     )
 | |
| 
 | |
|     @property
 | |
|     def team(self):
 | |
|         return self.registrations.first().team
 | |
|     team.fget.short_description = _("team")
 | |
|     team.fget.admin_order_field = 'registrations__team__trigram'
 | |
| 
 | |
|     @property
 | |
|     def tournament(self):
 | |
|         if self.final:
 | |
|             from participation.models import Tournament
 | |
|             return Tournament.final_tournament()
 | |
|         return self.registrations.first().team.participation.tournament
 | |
|     tournament.fget.short_description = _("tournament")
 | |
|     tournament.fget.admin_order_field = 'registrations__team__participation__tournament'
 | |
| 
 | |
|     def get_checkout_intent(self, none_if_link_disabled=False):
 | |
|         if self.checkout_intent_id is None:
 | |
|             return None
 | |
|         return helloasso.get_checkout_intent(self.checkout_intent_id, none_if_link_disabled=none_if_link_disabled)
 | |
| 
 | |
|     def create_checkout_intent(self):
 | |
|         checkout_intent = self.get_checkout_intent(none_if_link_disabled=True)
 | |
|         if checkout_intent is not None:
 | |
|             return checkout_intent
 | |
| 
 | |
|         tournament = self.tournament
 | |
|         year = timezone.now().year
 | |
|         base_site = "https://" + Site.objects.first().domain
 | |
|         checkout_intent = helloasso.create_checkout_intent(
 | |
|             amount=100 * self.amount,
 | |
|             name=f"Participation au TFJM² {year} - {tournament.name} - {self.team.trigram}",
 | |
|             back_url=base_site + reverse('registration:update_payment', args=(self.id,)),
 | |
|             error_url=f"{base_site}{reverse('registration:payment_hello_asso_return', args=(self.id,))}?type=error",
 | |
|             return_url=f"{base_site}{reverse('registration:payment_hello_asso_return', args=(self.id,))}?type=return",
 | |
|             contains_donation=False,
 | |
|             metadata=dict(
 | |
|                 users=[
 | |
|                     dict(user_id=registration.user.id,
 | |
|                          first_name=registration.user.first_name,
 | |
|                          last_name=registration.user.last_name,
 | |
|                          email=registration.user.email,)
 | |
|                     for registration in self.registrations.all()
 | |
|                 ],
 | |
|                 payment_id=self.id,
 | |
|                 final=self.final,
 | |
|                 tournament_id=tournament.id,
 | |
|             )
 | |
|         )
 | |
|         self.checkout_intent_id = checkout_intent["id"]
 | |
|         self.save()
 | |
| 
 | |
|         return checkout_intent
 | |
| 
 | |
|     def send_remind_mail(self):
 | |
|         with translation.override(settings.PREFERRED_LANGUAGE_CODE):
 | |
|             subject = f"[{settings.APP_NAME}] " + str(_("Reminder for your payment"))
 | |
|             site = Site.objects.first()
 | |
|             for registration in self.registrations.all():
 | |
|                 message = loader.render_to_string('registration/mails/payment_reminder.txt',
 | |
|                                                   dict(registration=registration, payment=self, domain=site.domain))
 | |
|                 html = loader.render_to_string('registration/mails/payment_reminder.html',
 | |
|                                                dict(registration=registration, payment=self, domain=site.domain))
 | |
|                 registration.user.email_user(subject, message, html_message=html)
 | |
| 
 | |
|     def send_helloasso_payment_confirmation_mail(self):
 | |
|         with translation.override(settings.PREFERRED_LANGUAGE_CODE):
 | |
|             subject = f"[{settings.APP_NAME}] " + str(_("Payment confirmation"))
 | |
|             site = Site.objects.first()
 | |
|             for registration in self.registrations.all():
 | |
|                 message = loader.render_to_string('registration/mails/payment_confirmation.txt',
 | |
|                                                   dict(registration=registration, payment=self, domain=site.domain))
 | |
|                 html = loader.render_to_string('registration/mails/payment_confirmation.html',
 | |
|                                                dict(registration=registration, payment=self, domain=site.domain))
 | |
|                 registration.user.email_user(subject, message, html_message=html)
 | |
| 
 | |
|             payer = self.get_checkout_intent()['order']['payer']
 | |
|             payer_name = f"{payer['firstName']} {payer['lastName']}"
 | |
|             if not self.registrations.filter(user__email=payer['email']).exists():
 | |
|                 message = loader.render_to_string('registration/mails/payment_confirmation.txt',
 | |
|                                                   dict(registration=payer_name, payment=self, domain=site.domain))
 | |
|                 html = loader.render_to_string('registration/mails/payment_confirmation.html',
 | |
|                                                dict(registration=payer_name, payment=self, domain=site.domain))
 | |
|                 send_mail(subject, message, None, [payer['email']], html_message=html)
 | |
| 
 | |
|     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")
 |