1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-22 01:18:27 +02:00

Compare commits

...

8 Commits

13 changed files with 392 additions and 396 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
from django.conf import settings from django.conf import settings
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.db.models import Q from django.db.models import Q
from django.template.defaultfilters import slugify
from participation.models import Team, Tournament from participation.models import Team, Tournament
from registration.models import ParticipantRegistration, VolunteerRegistration from registration.models import ParticipantRegistration, VolunteerRegistration
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
@ -36,7 +37,7 @@ class Command(BaseCommand):
"education", raise_error=False) "education", raise_error=False)
for tournament in Tournament.objects.all(): for tournament in Tournament.objects.all():
slug = tournament.name.lower().replace(" ", "-") slug = slugify(tournament.name)
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline", sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}" f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
" du TFJM2.", "education", raise_error=False) " du TFJM2.", "education", raise_error=False)
@ -54,7 +55,7 @@ class Command(BaseCommand):
for team in Team.objects.filter(participation__valid=True).all(): for team in Team.objects.filter(participation__valid=True).all():
team.create_mailing_list() team.create_mailing_list()
sympa.unsubscribe(team.email, "equipes-non-valides", True) sympa.unsubscribe(team.email, "equipes-non-valides", True)
sympa.subscribe(team.email, f"equipes-{team.participation.tournament.name.lower().replace(' ', '-')}", sympa.subscribe(team.email, f"equipes-{slugify(team.participation.tournament.name)}",
True, f"Equipe {team.name}") True, f"Equipe {team.name}")
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all(): for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
@ -62,16 +63,16 @@ class Command(BaseCommand):
sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}") sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}")
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all(): for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
sympa.subscribe(participant.user.email, f"equipe-{participant.team.trigram.lower()}", sympa.subscribe(participant.user.email, f"equipe-{slugify(participant.team.trigram)}",
True, f"{participant}") True, f"{participant}")
for volunteer in VolunteerRegistration.objects.all(): for volunteer in VolunteerRegistration.objects.all():
for organized_tournament in volunteer.organized_tournaments.all(): for organized_tournament in volunteer.organized_tournaments.all():
slug = organized_tournament.name.lower().replace(" ", "-") slug = slugify(organized_tournament.name)
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True) sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
for jury_in in volunteer.jury_in.all(): for jury_in in volunteer.jury_in.all():
slug = jury_in.tournament.name.lower().replace(" ", "-") slug = slugify(jury_in.tournament.name)
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True) sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
for admin in VolunteerRegistration.objects.filter(admin=True).all(): for admin in VolunteerRegistration.objects.filter(admin=True).all():

View File

@ -15,6 +15,12 @@ from ...models import Tournament
class Command(BaseCommand): class Command(BaseCommand):
"""
Création de notifications Google Drive pour récupérer les modifications sur les tableurs de notes.
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
"""
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)", '--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",

View File

@ -10,6 +10,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models from django.db import models
from django.db.models import Index from django.db.models import Index
from django.template.defaultfilters import slugify
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone, translation from django.utils import timezone, translation
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
@ -210,14 +211,14 @@ class Team(models.Model):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"equipe-{self.trigram.lower()}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"equipe-{slugify(self.trigram)}@{os.getenv('SYMPA_HOST', 'localhost')}"
def create_mailing_list(self): def create_mailing_list(self):
""" """
Create a new Sympa mailing list to contact the team. Create a new Sympa mailing list to contact the team.
""" """
get_sympa_client().create_list( get_sympa_client().create_list(
f"equipe-{self.trigram.lower()}", f"equipe-{slugify(self.trigram)}",
f"Equipe {self.name} ({self.trigram})", f"Equipe {self.name} ({self.trigram})",
"hotline", # TODO Use a custom sympa template "hotline", # TODO Use a custom sympa template
f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2", f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2",
@ -231,7 +232,7 @@ class Team(models.Model):
""" """
if self.participation.valid: # pragma: no cover if self.participation.valid: # pragma: no cover
get_sympa_client().unsubscribe( get_sympa_client().unsubscribe(
self.email, f"equipes-{self.participation.tournament.name.lower().replace(' ', '-')}", False) self.email, f"equipes-{slugify(self.participation.tournament.name)}", False)
else: else:
get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False) get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False)
get_sympa_client().delete_list(f"equipe-{self.trigram}") get_sympa_client().delete_list(f"equipe-{self.trigram}")
@ -391,28 +392,28 @@ class Tournament(models.Model):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"equipes-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"equipes-{slugify(self.name)}@{os.getenv('SYMPA_HOST', 'localhost')}"
@property @property
def organizers_email(self): def organizers_email(self):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"organisateurs-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"organisateurs-{slugify(self.name)}@{os.getenv('SYMPA_HOST', 'localhost')}"
@property @property
def jurys_email(self): def jurys_email(self):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"jurys-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"jurys-{slugify(self.name)}@{os.getenv('SYMPA_HOST', 'localhost')}"
def create_mailing_lists(self): def create_mailing_lists(self):
""" """
Create a new Sympa mailing list to contact the team. Create a new Sympa mailing list to contact the team.
""" """
get_sympa_client().create_list( get_sympa_client().create_list(
f"equipes-{self.name.lower().replace(' ', '-')}", f"equipes-{slugify(self.name)}",
f"Equipes du tournoi de {self.name}", f"Equipes du tournoi de {self.name}",
"hotline", # TODO Use a custom sympa template "hotline", # TODO Use a custom sympa template
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²", f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
@ -420,7 +421,7 @@ class Tournament(models.Model):
raise_error=False, raise_error=False,
) )
get_sympa_client().create_list( get_sympa_client().create_list(
f"organisateurs-{self.name.lower().replace(' ', '-')}", f"organisateurs-{slugify(self.name)}",
f"Organisateurs du tournoi de {self.name}", f"Organisateurs du tournoi de {self.name}",
"hotline", # TODO Use a custom sympa template "hotline", # TODO Use a custom sympa template
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²", f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",

View File

@ -4,6 +4,7 @@
from typing import Union from typing import Union
from django.conf import settings from django.conf import settings
from django.template.defaultfilters import slugify
from participation.models import Note, Participation, Passage, Pool, Team, Tournament from participation.models import Note, Participation, Passage, Pool, Team, Tournament
from registration.models import Payment from registration.models import Payment
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
@ -34,10 +35,10 @@ def update_mailing_list(instance: Team, raw, **_):
instance.create_mailing_list() instance.create_mailing_list()
# Subscribe all team members in the mailing list # Subscribe all team members in the mailing list
for student in instance.students.all(): for student in instance.students.all():
get_sympa_client().subscribe(student.user.email, f"equipe-{instance.trigram.lower()}", False, get_sympa_client().subscribe(student.user.email, f"equipe-{slugify(instance.trigram)}", False,
f"{student.user.first_name} {student.user.last_name}") f"{student.user.first_name} {student.user.last_name}")
for coach in instance.coaches.all(): for coach in instance.coaches.all():
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False, get_sympa_client().subscribe(coach.user.email, f"equipe-{slugify(instance.trigram)}", False,
f"{coach.user.first_name} {coach.user.last_name}") f"{coach.user.first_name} {coach.user.last_name}")

View File

@ -22,6 +22,7 @@ from django.db import transaction
from django.db.models import F from django.db.models import F
from django.http import FileResponse, Http404, HttpResponse from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.defaultfilters import slugify
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone, translation from django.utils import timezone, translation
@ -88,7 +89,7 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
registration.save() registration.save()
# Subscribe the user mail address to the team mailing list # Subscribe the user mail address to the team mailing list
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False, get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
f"{user.first_name} {user.last_name}") f"{user.first_name} {user.last_name}")
return ret return ret
@ -130,7 +131,7 @@ class JoinTeamView(LoginRequiredMixin, FormView):
registration.save() registration.save()
# Subscribe to the team mailing list # Subscribe to the team mailing list
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False, get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
f"{user.first_name} {user.last_name}") f"{user.first_name} {user.last_name}")
return ret return ret
@ -313,6 +314,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
instance=self.object.participation) instance=self.object.participation)
if not self.request.user.registration.is_volunteer: if not self.request.user.registration.is_volunteer:
del context["participation_form"].fields['final'] del context["participation_form"].fields['final']
context["participation_form"].helper.layout.remove('final')
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram) context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
return context return context
@ -321,6 +323,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation) participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
if not self.request.user.registration.is_volunteer: if not self.request.user.registration.is_volunteer:
del participation_form.fields['final'] del participation_form.fields['final']
participation_form.helper.layout.remove('final')
if not participation_form.is_valid(): if not participation_form.is_valid():
return self.form_invalid(form) return self.form_invalid(form)
@ -517,7 +520,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
team = request.user.registration.team team = request.user.registration.team
request.user.registration.team = None request.user.registration.team = None
request.user.registration.save() request.user.registration.save()
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False) get_sympa_client().unsubscribe(request.user.email, f"equipe-{slugify(team.trigram)}", False)
if team.students.count() + team.coaches.count() == 0: if team.students.count() + team.coaches.count() == 0:
team.delete() team.delete()
return redirect(reverse_lazy("index")) return redirect(reverse_lazy("index"))
@ -1952,6 +1955,13 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class GSheetNotificationsView(View): class GSheetNotificationsView(View):
"""
Cette vue gère les notifications envoyées par Google Drive en cas de
modifications d'un tableur de notes sur Google Sheets.
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
"""
async def post(self, request, *args, **kwargs): async def post(self, request, *args, **kwargs):
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists(): if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
return HttpResponse(status=404) return HttpResponse(status=404)

View File

@ -7,8 +7,6 @@ from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms import FileInput from django.forms import FileInput
from django.utils import timezone
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import CoachRegistration, ParticipantRegistration, Payment, \ from .models import CoachRegistration, ParticipantRegistration, Payment, \
@ -38,19 +36,6 @@ class SignupForm(UserCreationForm):
self.add_error("email", _("This email address is already used.")) self.add_error("email", _("This email address is already used."))
return email return email
def clean(self):
# Check that registrations are opened
now = timezone.now()
if now < settings.REGISTRATION_DATES['open']:
self.add_error(None, format_lazy(_("Registrations are not opened yet. "
"They will open on the {opening_date:%Y-%m-%d %H:%M}."),
opening_date=settings.REGISTRATION_DATES['open']))
elif now > settings.REGISTRATION_DATES['close']:
self.add_error(None, format_lazy(_("Registrations for this year are closed since "
"{closing_date:%Y-%m-%d %H:%M}."),
closing_date=settings.REGISTRATION_DATES['close']))
return super().clean()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["first_name"].required = True self.fields["first_name"].required = True

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.template.defaultfilters import slugify
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
from .models import Registration, VolunteerRegistration from .models import Registration, VolunteerRegistration
@ -29,8 +30,8 @@ def send_email_link(instance, **_):
registration.send_email_validation_link() registration.send_email_validation_link()
if registration.participates and registration.team: if registration.participates and registration.team:
get_sympa_client().unsubscribe(old_instance.email, f"equipe-{registration.team.trigram.lower()}", False) get_sympa_client().unsubscribe(old_instance.email, f"equipe-{slugify(registration.team.trigram)}", False)
get_sympa_client().subscribe(instance.email, f"equipe-{registration.team.trigram.lower()}", False, get_sympa_client().subscribe(instance.email, f"equipe-{slugify(registration.team.trigram)}", False,
f"{instance.first_name} {instance.last_name}") f"{instance.first_name} {instance.last_name}")

View File

@ -10,13 +10,13 @@
{% block content %} {% block content %}
{% now "c" as now %} {% now "c" as now %}
{% if now < TFJM.REGISTRATION_DATES.open.isoformat %} {% if now < TFJM.REGISTRATION_DATES.open.isoformat and not user.registration.is_admin %}
<div class="alert alert-warning"> <div class="alert alert-warning">
{% trans "Thank you for your great interest, but registrations are not opened yet!" %} {% trans "Thank you for your great interest, but registrations are not opened yet!" %}
{% trans "They will open on:" %} {{ TFJM.REGISTRATION_DATES.open|date:'DATETIME_FORMAT' }}. {% trans "They will open on:" %} {{ TFJM.REGISTRATION_DATES.open|date:'DATETIME_FORMAT' }}.
{% trans "Please come back at this time to register!" %} {% trans "Please come back at this time to register!" %}
</div> </div>
{% elif now > TFJM.REGISTRATION_DATES.close.isoformat %} {% elif now > TFJM.REGISTRATION_DATES.close.isoformat and not user.registration.is_admin %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{% trans "Registrations are closed for this year. We hope to see you next year!" %} {% trans "Registrations are closed for this year. We hope to see you next year!" %}
{% trans "If needed, you can contact us by mail." %} {% trans "If needed, you can contact us by mail." %}

View File

@ -70,7 +70,11 @@ ses propres moyens et sous la responsabilité du/de la représentant\cdt{}e lég
\vspace{8ex} \vspace{8ex}
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %}, Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %}
\vspace{4ex}
Signature :
\vfill \vfill
\vfill \vfill

View File

@ -18,7 +18,7 @@ from django.http import FileResponse, Http404
from django.shortcuts import redirect, resolve_url from django.shortcuts import redirect, resolve_url
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import translation from django.utils import timezone, translation
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.text import format_lazy from django.utils.text import format_lazy
@ -60,6 +60,22 @@ class SignupView(CreateView):
return context return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
if self.request.method in ("POST", "PUT") \
and (not self.request.user.is_authenticated or not self.request.user.registration.is_admin):
# Check that registrations are opened
now = timezone.now()
if now < settings.REGISTRATION_DATES['open']:
form.add_error(None, format_lazy(_("Registrations are not opened yet. "
"They will open on the {opening_date:%Y-%m-%d %H:%M}."),
opening_date=settings.REGISTRATION_DATES['open']))
elif now > settings.REGISTRATION_DATES['close']:
form.add_error(None, format_lazy(_("Registrations for this year are closed since "
"{closing_date:%Y-%m-%d %H:%M}."),
closing_date=settings.REGISTRATION_DATES['close']))
return form
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
role = form.cleaned_data["role"] role = form.cleaned_data["role"]

View File

@ -11,15 +11,13 @@
7 3 * * * cd /code && python manage.py fix_sympa_lists &> /dev/null 7 3 * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
# Check payments from Hello Asso # Check payments from Hello Asso
*/6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null */30 * * 03-05 * cd /code && python manage.py check_hello_asso -v 0
# Send reminders for payments
30 6 * * 1 cd /code && python manage.py remind_payments &> /dev/null
# Check notation sheets every 15 minutes from 08:00 to 23:00 on fridays to mondays in april and may # Send reminders for payments
# */15 8-23 * 4-5 5,6,7,1 cd /code && python manage.py parse_notation_sheets -v 0 30 6 * 03-05 1 cd /code && python manage.py remind_payments -v 0
# Update Google Drive notifications daily # Update Google Drive notifications daily
0 0 * * * cd /code && python manage.py renew_gdrive_notifications &> /dev/null 0 0 * * * cd /code && python manage.py renew_gdrive_notifications -v 0
# Clean temporary files # Clean temporary files
30 * * * * rm -rf /tmp/* 30 * * * * rm -rf /tmp/*

View File

@ -30,15 +30,6 @@
</div> </div>
</div> </div>
<div class="alert alert-warning">
<h3 class="alert-heading"><i class="fas fa-warning"></i> {% trans "New in 2025" %}</h3>
{% blocktrans trimmed %}
Registration for Ile-de-France tournaments is now unified.
If you live in or near the Ile-de-France region, your registration will be pooled with each of the region's tournaments,
and the organizers will take care of team allocation. However, date constraints can be indicated in the motivation letter.
{% endblocktrans %}
</div>
<div class="jumbotron p-5 border rounded-5"> <div class="jumbotron p-5 border rounded-5">
<h5 class="display-4">{% trans "How does it work?" %}</h5> <h5 class="display-4">{% trans "How does it work?" %}</h5>
<p> <p>