2020-12-27 10:49:54 +00:00
|
|
|
# Copyright (C) 2020 by Animath
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
2023-04-05 14:54:16 +00:00
|
|
|
|
2022-04-22 16:02:27 +00:00
|
|
|
import csv
|
2020-12-27 10:49:54 +00:00
|
|
|
from io import BytesIO
|
2021-01-22 08:40:28 +00:00
|
|
|
import os
|
2023-04-06 21:38:03 +00:00
|
|
|
import subprocess
|
|
|
|
from tempfile import mkdtemp
|
2024-02-23 21:58:23 +00:00
|
|
|
from typing import Any, Dict
|
2020-12-27 10:49:54 +00:00
|
|
|
from zipfile import ZipFile
|
|
|
|
|
2021-01-23 20:48:01 +00:00
|
|
|
from django.conf import settings
|
2022-05-15 10:23:17 +00:00
|
|
|
from django.contrib import messages
|
2020-12-27 10:49:54 +00:00
|
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
|
|
from django.contrib.sites.models import Site
|
|
|
|
from django.core.exceptions import PermissionDenied
|
|
|
|
from django.core.mail import send_mail
|
|
|
|
from django.db import transaction
|
2024-02-23 21:58:23 +00:00
|
|
|
from django.db.models import F
|
2021-01-22 08:40:28 +00:00
|
|
|
from django.http import FileResponse, Http404, HttpResponse
|
2020-12-27 10:49:54 +00:00
|
|
|
from django.shortcuts import redirect
|
|
|
|
from django.template.loader import render_to_string
|
|
|
|
from django.urls import reverse_lazy
|
2021-01-18 23:11:52 +00:00
|
|
|
from django.utils import timezone
|
2023-04-05 14:54:16 +00:00
|
|
|
from django.utils.crypto import get_random_string
|
2020-12-27 10:49:54 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2021-01-22 08:40:28 +00:00
|
|
|
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
|
2020-12-27 10:49:54 +00:00
|
|
|
from django.views.generic.edit import FormMixin, ProcessFormView
|
2024-02-23 21:58:23 +00:00
|
|
|
from django_tables2 import MultiTableMixin, SingleTableMixin, SingleTableView
|
2020-12-27 10:49:54 +00:00
|
|
|
from magic import Magic
|
2023-04-07 19:47:06 +00:00
|
|
|
from odf.opendocument import OpenDocumentSpreadsheet
|
|
|
|
from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties
|
|
|
|
from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
|
|
|
|
from odf.text import P
|
2024-02-12 21:30:27 +00:00
|
|
|
from registration.models import Payment, StudentRegistration, VolunteerRegistration
|
2024-02-23 21:58:23 +00:00
|
|
|
from registration.tables import PaymentTable
|
2020-12-28 18:19:01 +00:00
|
|
|
from tfjm.lists import get_sympa_client
|
2021-01-17 11:40:23 +00:00
|
|
|
from tfjm.views import AdminMixin, VolunteerMixin
|
2020-12-27 10:49:54 +00:00
|
|
|
|
2023-04-05 14:54:16 +00:00
|
|
|
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
|
|
|
|
PoolForm, PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
|
2022-05-15 10:23:17 +00:00
|
|
|
UploadNotesForm, ValidateParticipationForm
|
2021-01-17 15:23:48 +00:00
|
|
|
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
|
|
|
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CreateTeamView(LoginRequiredMixin, CreateView):
|
|
|
|
"""
|
|
|
|
Display the page to create a team for new users.
|
|
|
|
"""
|
|
|
|
|
|
|
|
model = Team
|
|
|
|
form_class = TeamForm
|
|
|
|
extra_context = dict(title=_("Create team"))
|
|
|
|
template_name = "participation/create_team.html"
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
|
|
|
if not user.is_authenticated:
|
|
|
|
return super().handle_no_permission()
|
|
|
|
registration = user.registration
|
|
|
|
if not registration.participates:
|
|
|
|
raise PermissionDenied(_("You don't participate, so you can't create a team."))
|
|
|
|
elif registration.team:
|
|
|
|
raise PermissionDenied(_("You are already in a team."))
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
@transaction.atomic
|
|
|
|
def form_valid(self, form):
|
|
|
|
"""
|
|
|
|
When a team is about to be created, the user automatically
|
|
|
|
joins the team, a mailing list got created and the user is
|
2024-01-13 15:46:19 +00:00
|
|
|
automatically subscribed to this mailing list.
|
2020-12-27 10:49:54 +00:00
|
|
|
"""
|
|
|
|
ret = super().form_valid(form)
|
|
|
|
# The user joins the team
|
|
|
|
user = self.request.user
|
|
|
|
registration = user.registration
|
|
|
|
registration.team = form.instance
|
|
|
|
registration.save()
|
|
|
|
|
|
|
|
# Subscribe the user mail address to the team mailing list
|
|
|
|
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
|
|
|
f"{user.first_name} {user.last_name}")
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
|
|
class JoinTeamView(LoginRequiredMixin, FormView):
|
|
|
|
"""
|
|
|
|
Participants can join a team with the access code of the team.
|
|
|
|
"""
|
|
|
|
model = Team
|
|
|
|
form_class = JoinTeamForm
|
|
|
|
extra_context = dict(title=_("Join team"))
|
2024-02-11 20:40:06 +00:00
|
|
|
template_name = "participation/join_team.html"
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
|
|
|
if not user.is_authenticated:
|
|
|
|
return super().handle_no_permission()
|
|
|
|
registration = user.registration
|
|
|
|
if not registration.participates:
|
|
|
|
raise PermissionDenied(_("You don't participate, so you can't create a team."))
|
|
|
|
elif registration.team:
|
|
|
|
raise PermissionDenied(_("You are already in a team."))
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
@transaction.atomic
|
|
|
|
def form_valid(self, form):
|
|
|
|
"""
|
|
|
|
When a user joins a team, the user is automatically subscribed to
|
2024-01-13 15:46:19 +00:00
|
|
|
the team mailing list.
|
2020-12-27 10:49:54 +00:00
|
|
|
"""
|
|
|
|
self.object = form.instance
|
|
|
|
ret = super().form_valid(form)
|
|
|
|
|
|
|
|
# Join the team
|
|
|
|
user = self.request.user
|
|
|
|
registration = user.registration
|
|
|
|
registration.team = form.instance
|
|
|
|
registration.save()
|
|
|
|
|
|
|
|
# Subscribe to the team mailing list
|
|
|
|
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
|
|
|
f"{user.first_name} {user.last_name}")
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy("participation:team_detail", args=(self.object.pk,))
|
|
|
|
|
|
|
|
|
|
|
|
class TeamListView(AdminMixin, SingleTableView):
|
|
|
|
"""
|
|
|
|
Display the whole list of teams
|
|
|
|
"""
|
|
|
|
model = Team
|
|
|
|
table_class = TeamTable
|
2021-01-01 11:11:09 +00:00
|
|
|
ordering = ('trigram',)
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
class MyTeamDetailView(LoginRequiredMixin, RedirectView):
|
|
|
|
"""
|
|
|
|
Redirect to the detail of the team in which the user is.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
|
|
user = self.request.user
|
|
|
|
registration = user.registration
|
|
|
|
if registration.participates:
|
|
|
|
if registration.team:
|
|
|
|
return reverse_lazy("participation:team_detail", args=(registration.team_id,))
|
|
|
|
raise PermissionDenied(_("You are not in a team."))
|
|
|
|
raise PermissionDenied(_("You don't participate, so you don't have any team."))
|
|
|
|
|
|
|
|
|
|
|
|
class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView):
|
|
|
|
"""
|
|
|
|
Display the detail of a team.
|
|
|
|
"""
|
|
|
|
model = Team
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
|
|
|
self.object = self.get_object()
|
2021-01-17 11:40:23 +00:00
|
|
|
# Ensure that the user is an admin or a volunteer or a member of the team
|
2020-12-27 10:49:54 +00:00
|
|
|
if user.registration.is_admin or user.registration.participates and \
|
2021-01-17 11:40:23 +00:00
|
|
|
user.registration.team and user.registration.team.pk == kwargs["pk"] \
|
|
|
|
or user.registration.is_volunteer \
|
2021-05-11 14:40:18 +00:00
|
|
|
and (self.object.participation.tournament in user.registration.interesting_tournaments
|
|
|
|
or self.object.participation.final
|
|
|
|
and Tournament.final_tournament() in user.registration.interesting_tournaments):
|
2020-12-27 10:49:54 +00:00
|
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
raise PermissionDenied
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
|
|
|
|
team = self.get_object()
|
|
|
|
context["title"] = _("Detail of team {trigram}").format(trigram=self.object.trigram)
|
|
|
|
context["request_validation_form"] = RequestValidationForm(self.request.POST or None)
|
|
|
|
context["validation_form"] = ValidateParticipationForm(self.request.POST or None)
|
2021-01-01 16:07:28 +00:00
|
|
|
# A team is complete when there are at least 4 members plus a coache that have sent their authorizations,
|
2020-12-28 22:59:21 +00:00
|
|
|
# their health sheet, they confirmed their email address and under-18 people sent their parental authorization.
|
2024-02-11 19:20:28 +00:00
|
|
|
context["can_validate"] = team.can_validate()
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
def get_form_class(self):
|
|
|
|
if not self.request.POST:
|
|
|
|
return RequestValidationForm
|
|
|
|
elif self.request.POST["_form_type"] == "RequestValidationForm":
|
|
|
|
return RequestValidationForm
|
|
|
|
elif self.request.POST["_form_type"] == "ValidateParticipationForm":
|
|
|
|
return ValidateParticipationForm
|
|
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
|
self.object = self.get_object()
|
|
|
|
if isinstance(form, RequestValidationForm):
|
|
|
|
return self.handle_request_validation(form)
|
|
|
|
elif isinstance(form, ValidateParticipationForm):
|
|
|
|
return self.handle_validate_participation(form)
|
|
|
|
|
|
|
|
def handle_request_validation(self, form):
|
|
|
|
"""
|
|
|
|
A team requests to be validated
|
|
|
|
"""
|
|
|
|
if not self.request.user.registration.participates:
|
|
|
|
form.add_error(None, _("You don't participate, so you can't request the validation of the team."))
|
|
|
|
return self.form_invalid(form)
|
|
|
|
if self.object.participation.valid is not None:
|
|
|
|
form.add_error(None, _("The validation of the team is already done or pending."))
|
|
|
|
return self.form_invalid(form)
|
|
|
|
if not self.get_context_data()["can_validate"]:
|
|
|
|
form.add_error(None, _("The team can't be validated: missing email address confirmations, "
|
2021-01-22 08:40:28 +00:00
|
|
|
"authorizations, people, motivation letter or the tournament is not set."))
|
2020-12-27 10:49:54 +00:00
|
|
|
return self.form_invalid(form)
|
|
|
|
|
|
|
|
self.object.participation.valid = False
|
|
|
|
self.object.participation.save()
|
|
|
|
|
2021-01-23 20:48:01 +00:00
|
|
|
mail_context = dict(team=self.object, domain=Site.objects.first().domain)
|
|
|
|
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
|
|
|
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
2021-02-07 15:51:09 +00:00
|
|
|
send_mail("[TFJM²] Validation d'équipe", mail_plain, settings.DEFAULT_FROM_EMAIL,
|
2021-01-23 20:48:01 +00:00
|
|
|
[self.object.participation.tournament.organizers_email], html_message=mail_html)
|
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
def handle_validate_participation(self, form):
|
|
|
|
"""
|
|
|
|
An admin validates the team (or not)
|
|
|
|
"""
|
2021-02-07 16:40:29 +00:00
|
|
|
if not self.request.user.registration.is_admin and \
|
2021-02-07 16:31:50 +00:00
|
|
|
(not self.object.participation.tournament
|
|
|
|
or self.request.user.registration not in self.object.participation.tournament.organizers.all()):
|
2021-01-23 20:48:01 +00:00
|
|
|
form.add_error(None, _("You are not an organizer of the tournament."))
|
2020-12-27 10:49:54 +00:00
|
|
|
return self.form_invalid(form)
|
|
|
|
elif self.object.participation.valid is not False:
|
|
|
|
form.add_error(None, _("This team has no pending validation."))
|
|
|
|
return self.form_invalid(form)
|
|
|
|
|
|
|
|
if "validate" in self.request.POST:
|
|
|
|
self.object.participation.valid = True
|
|
|
|
self.object.participation.save()
|
|
|
|
|
2024-02-24 08:56:57 +00:00
|
|
|
domain = Site.objects.first().domain
|
|
|
|
for registration in self.object.participants.all():
|
2024-02-26 22:49:57 +00:00
|
|
|
if registration.is_student and self.object.participation.tournament.price:
|
2024-02-24 08:56:57 +00:00
|
|
|
payment = Payment.objects.get(registrations=registration, final=False)
|
|
|
|
else:
|
|
|
|
payment = None
|
|
|
|
mail_context = dict(domain=domain, registration=registration, team=self.object, payment=payment,
|
|
|
|
message=form.cleaned_data["message"])
|
|
|
|
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context)
|
|
|
|
mail_html = render_to_string("participation/mails/team_validated.html", mail_context)
|
|
|
|
registration.user.email_user("[TFJM²] Équipe validée", mail_plain, html_message=mail_html)
|
2020-12-27 10:49:54 +00:00
|
|
|
elif "invalidate" in self.request.POST:
|
|
|
|
self.object.participation.valid = None
|
|
|
|
self.object.participation.save()
|
|
|
|
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
|
|
|
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context)
|
|
|
|
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context)
|
2021-01-18 14:52:09 +00:00
|
|
|
send_mail("[TFJM²] Équipe non validée", mail_plain, None, [self.object.email],
|
2020-12-27 10:49:54 +00:00
|
|
|
html_message=mail_html)
|
|
|
|
else:
|
|
|
|
form.add_error(None, _("You must specify if you validate the registration or not."))
|
|
|
|
return self.form_invalid(form)
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return self.request.path
|
|
|
|
|
|
|
|
|
|
|
|
class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
|
|
|
"""
|
|
|
|
Update the detail of a team
|
|
|
|
"""
|
|
|
|
model = Team
|
|
|
|
form_class = TeamForm
|
|
|
|
template_name = "participation/update_team.html"
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
|
|
|
if not user.is_authenticated:
|
|
|
|
return super().handle_no_permission()
|
|
|
|
if user.registration.is_admin or user.registration.participates and \
|
2021-01-17 11:40:23 +00:00
|
|
|
user.registration.team and user.registration.team.pk == kwargs["pk"] \
|
|
|
|
or user.registration.is_volunteer \
|
2021-05-11 14:40:18 +00:00
|
|
|
and (self.get_object().participation.tournament in user.registration.interesting_tournaments
|
|
|
|
or self.get_object().participation.final
|
|
|
|
and Tournament.final_tournament() in user.registration.interesting_tournaments):
|
2020-12-27 10:49:54 +00:00
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
raise PermissionDenied
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["participation_form"] = ParticipationForm(data=self.request.POST or None,
|
|
|
|
instance=self.object.participation)
|
2023-01-22 14:49:42 +00:00
|
|
|
if not self.request.user.registration.is_volunteer:
|
|
|
|
del context["participation_form"].fields['final']
|
2020-12-27 10:49:54 +00:00
|
|
|
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
|
|
|
|
return context
|
|
|
|
|
|
|
|
@transaction.atomic
|
|
|
|
def form_valid(self, form):
|
|
|
|
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
|
2023-01-22 14:49:42 +00:00
|
|
|
if not self.request.user.registration.is_volunteer:
|
|
|
|
del participation_form.fields['final']
|
2020-12-27 10:49:54 +00:00
|
|
|
if not participation_form.is_valid():
|
|
|
|
return self.form_invalid(form)
|
|
|
|
|
|
|
|
participation_form.save()
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
|
2021-01-22 08:40:28 +00:00
|
|
|
class TeamUploadMotivationLetterView(LoginRequiredMixin, UpdateView):
|
|
|
|
"""
|
|
|
|
A team can send its motivation letter.
|
|
|
|
"""
|
|
|
|
model = Team
|
|
|
|
form_class = MotivationLetterForm
|
|
|
|
template_name = "participation/upload_motivation_letter.html"
|
|
|
|
extra_context = dict(title=_("Upload motivation letter"))
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not self.request.user.is_authenticated or \
|
|
|
|
not self.request.user.registration.is_admin \
|
|
|
|
and self.request.user.registration.team != self.get_object():
|
|
|
|
return self.handle_no_permission()
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
@transaction.atomic
|
|
|
|
def form_valid(self, form):
|
|
|
|
old_instance = Team.objects.get(pk=self.object.pk)
|
|
|
|
if old_instance.motivation_letter:
|
|
|
|
old_instance.motivation_letter.delete()
|
|
|
|
old_instance.save()
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
|
|
|
|
class MotivationLetterView(LoginRequiredMixin, View):
|
|
|
|
"""
|
|
|
|
Display the sent motivation letter.
|
|
|
|
"""
|
2022-04-22 16:02:27 +00:00
|
|
|
|
2021-01-22 08:40:28 +00:00
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
filename = kwargs["filename"]
|
|
|
|
path = f"media/authorization/motivation_letters/{filename}"
|
|
|
|
if not os.path.exists(path):
|
|
|
|
raise Http404
|
|
|
|
team = Team.objects.get(motivation_letter__endswith=filename)
|
|
|
|
user = request.user
|
|
|
|
if not (user.registration in team.participants.all() or user.registration.is_admin
|
|
|
|
or user.registration.is_volunteer
|
|
|
|
and team.participation.tournament in user.registration.organized_tournaments.all()):
|
|
|
|
raise PermissionDenied
|
|
|
|
# Guess mime type of the file
|
|
|
|
mime = Magic(mime=True)
|
|
|
|
mime_type = mime.from_file(path)
|
|
|
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
|
|
|
# Replace file name
|
|
|
|
true_file_name = _("Motivation letter of {team}.{ext}").format(team=str(team), ext=ext)
|
|
|
|
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
|
|
|
|
|
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
|
|
|
|
"""
|
|
|
|
Get as a ZIP archive all the authorizations that are sent
|
|
|
|
"""
|
|
|
|
model = Team
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
|
|
|
if not user.is_authenticated:
|
|
|
|
return super().handle_no_permission()
|
2021-03-28 18:09:29 +00:00
|
|
|
if user.registration.is_admin or user.registration.is_volunteer \
|
2023-04-09 09:32:26 +00:00
|
|
|
and (user.registration in self.get_object().participation.tournament.organizers
|
2021-05-11 14:40:18 +00:00
|
|
|
or self.get_object().participation.final
|
2023-04-09 09:32:26 +00:00
|
|
|
and user.registration in Tournament.final_tournament().organizers):
|
2020-12-27 10:49:54 +00:00
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
raise PermissionDenied
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
team = self.get_object()
|
2021-01-22 08:40:28 +00:00
|
|
|
magic = Magic(mime=True)
|
2020-12-27 10:49:54 +00:00
|
|
|
output = BytesIO()
|
|
|
|
zf = ZipFile(output, "w")
|
2021-01-17 16:28:59 +00:00
|
|
|
for participant in team.participants.all():
|
|
|
|
if participant.photo_authorization:
|
|
|
|
mime_type = magic.from_file("media/" + participant.photo_authorization.name)
|
|
|
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
|
|
|
zf.write("media/" + participant.photo_authorization.name,
|
|
|
|
_("Photo authorization of {participant}.{ext}").format(participant=str(participant), ext=ext))
|
|
|
|
|
|
|
|
if isinstance(participant, StudentRegistration) and participant.parental_authorization:
|
|
|
|
mime_type = magic.from_file("media/" + participant.parental_authorization.name)
|
|
|
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
|
|
|
zf.write("media/" + participant.parental_authorization.name,
|
|
|
|
_("Parental authorization of {participant}.{ext}")
|
|
|
|
.format(participant=str(participant), ext=ext))
|
|
|
|
|
2021-01-23 13:27:21 +00:00
|
|
|
if isinstance(participant, StudentRegistration) and participant.health_sheet:
|
2021-01-17 16:28:59 +00:00
|
|
|
mime_type = magic.from_file("media/" + participant.health_sheet.name)
|
|
|
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
|
|
|
zf.write("media/" + participant.health_sheet.name,
|
|
|
|
_("Health sheet of {participant}.{ext}").format(participant=str(participant), ext=ext))
|
2021-01-22 08:40:28 +00:00
|
|
|
|
2023-02-19 23:38:57 +00:00
|
|
|
if isinstance(participant, StudentRegistration) and participant.vaccine_sheet:
|
|
|
|
mime_type = magic.from_file("media/" + participant.vaccine_sheet.name)
|
|
|
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
|
|
|
zf.write("media/" + participant.vaccine_sheet.name,
|
|
|
|
_("Vaccine sheet of {participant}.{ext}").format(participant=str(participant), ext=ext))
|
|
|
|
|
2021-01-22 08:40:28 +00:00
|
|
|
if team.motivation_letter:
|
|
|
|
mime_type = magic.from_file("media/" + team.motivation_letter.name)
|
|
|
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
|
|
|
zf.write("media/" + team.motivation_letter.name,
|
|
|
|
_("Motivation letter of {team}.{ext}").format(team=str(team), ext=ext))
|
2020-12-27 10:49:54 +00:00
|
|
|
zf.close()
|
|
|
|
response = HttpResponse(content_type="application/zip")
|
|
|
|
response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
|
|
|
|
.format(filename=_("Photo authorizations of team {trigram}.zip").format(trigram=team.trigram))
|
|
|
|
response.write(output.getvalue())
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
class TeamLeaveView(LoginRequiredMixin, TemplateView):
|
|
|
|
"""
|
|
|
|
A team member leaves a team
|
|
|
|
"""
|
|
|
|
template_name = "participation/team_leave.html"
|
|
|
|
extra_context = dict(title=_("Leave team"))
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
if not request.user.registration.participates or not request.user.registration.team:
|
|
|
|
raise PermissionDenied(_("You are not in a team."))
|
|
|
|
if request.user.registration.team.participation.valid:
|
|
|
|
raise PermissionDenied(_("The team is already validated or the validation is pending."))
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
@transaction.atomic()
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
When the team is left, the user is unsubscribed from the team mailing list
|
|
|
|
and kicked from the team room.
|
|
|
|
"""
|
|
|
|
team = request.user.registration.team
|
|
|
|
request.user.registration.team = None
|
|
|
|
request.user.registration.save()
|
|
|
|
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False)
|
2021-01-18 20:30:26 +00:00
|
|
|
if team.students.count() + team.coaches.count() == 0:
|
2020-12-27 10:49:54 +00:00
|
|
|
team.delete()
|
|
|
|
return redirect(reverse_lazy("index"))
|
|
|
|
|
|
|
|
|
|
|
|
class MyParticipationDetailView(LoginRequiredMixin, RedirectView):
|
|
|
|
"""
|
|
|
|
Redirects to the detail view of the participation of the team.
|
|
|
|
"""
|
2022-04-22 16:02:27 +00:00
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
|
|
user = self.request.user
|
|
|
|
registration = user.registration
|
|
|
|
if registration.participates:
|
|
|
|
if registration.team:
|
|
|
|
return reverse_lazy("participation:participation_detail", args=(registration.team.participation.id,))
|
|
|
|
raise PermissionDenied(_("You are not in a team."))
|
|
|
|
raise PermissionDenied(_("You don't participate, so you don't have any team."))
|
|
|
|
|
|
|
|
|
|
|
|
class ParticipationDetailView(LoginRequiredMixin, DetailView):
|
|
|
|
"""
|
2021-01-12 14:42:32 +00:00
|
|
|
Display detail about the participation of a team, and manage the solution submission.
|
2020-12-27 10:49:54 +00:00
|
|
|
"""
|
|
|
|
model = Participation
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
|
|
|
if not user.is_authenticated:
|
|
|
|
return super().handle_no_permission()
|
|
|
|
if not self.get_object().valid:
|
|
|
|
raise PermissionDenied(_("The team is not validated yet."))
|
|
|
|
if user.registration.is_admin or user.registration.participates \
|
|
|
|
and user.registration.team.participation \
|
2021-01-17 11:40:23 +00:00
|
|
|
and user.registration.team.participation.pk == kwargs["pk"] \
|
|
|
|
or user.registration.is_volunteer \
|
2021-05-11 14:40:18 +00:00
|
|
|
and (self.get_object().tournament in user.registration.interesting_tournaments
|
|
|
|
or self.get_object().final
|
|
|
|
and Tournament.final_tournament() in user.registration.interesting_tournaments):
|
2020-12-27 10:49:54 +00:00
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
raise PermissionDenied
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
|
|
|
|
context["title"] = lambda: _("Participation of team {trigram}").format(trigram=self.object.team.trigram)
|
|
|
|
|
|
|
|
return context
|
2020-12-30 11:13:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TournamentListView(SingleTableView):
|
2020-12-31 11:13:42 +00:00
|
|
|
"""
|
|
|
|
Display the list of all tournaments.
|
|
|
|
"""
|
2020-12-30 11:13:05 +00:00
|
|
|
model = Tournament
|
|
|
|
table_class = TournamentTable
|
2020-12-31 11:13:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TournamentCreateView(AdminMixin, CreateView):
|
|
|
|
"""
|
|
|
|
Create a new tournament.
|
|
|
|
"""
|
|
|
|
model = Tournament
|
|
|
|
form_class = TournamentForm
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy("participation:tournament_detail", args=(self.object.pk,))
|
|
|
|
|
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
class TournamentUpdateView(VolunteerMixin, UpdateView):
|
2020-12-31 11:13:42 +00:00
|
|
|
"""
|
|
|
|
Update tournament detail.
|
|
|
|
"""
|
|
|
|
model = Tournament
|
|
|
|
form_class = TournamentForm
|
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated or not self.request.user.registration.is_admin \
|
|
|
|
and not (self.request.user.registration.is_volunteer
|
2021-01-17 15:23:48 +00:00
|
|
|
and self.request.user.registration.organized_tournaments.all()):
|
2021-01-17 11:40:23 +00:00
|
|
|
return self.handle_no_permission()
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
2020-12-31 11:13:42 +00:00
|
|
|
|
2023-04-11 20:41:52 +00:00
|
|
|
class TournamentDetailView(MultiTableMixin, DetailView):
|
2020-12-31 11:13:42 +00:00
|
|
|
"""
|
|
|
|
Display tournament detail.
|
|
|
|
"""
|
|
|
|
model = Tournament
|
2021-01-01 11:11:09 +00:00
|
|
|
|
2023-04-11 20:41:52 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
self.tables = [
|
|
|
|
ParticipationTable(self.get_object().participations.all()),
|
|
|
|
PoolTable(self.get_object().pools.order_by('id').all()),
|
|
|
|
]
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
2021-01-01 11:11:09 +00:00
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
2023-04-11 20:41:52 +00:00
|
|
|
tables = context['tables']
|
|
|
|
context["teams"] = tables[0]
|
|
|
|
context["pools"] = tables[1]
|
2021-01-14 18:23:32 +00:00
|
|
|
|
|
|
|
notes = dict()
|
|
|
|
for participation in self.object.participations.all():
|
2021-01-18 15:54:57 +00:00
|
|
|
note = sum(pool.average(participation)
|
2021-04-10 07:59:04 +00:00
|
|
|
for pool in self.object.pools.filter(participations=participation).all()
|
2021-04-10 09:43:31 +00:00
|
|
|
if pool.results_available
|
|
|
|
or (self.request.user.is_authenticated and self.request.user.registration.is_volunteer))
|
2021-01-18 15:54:57 +00:00
|
|
|
if note:
|
|
|
|
notes[participation] = note
|
2021-01-14 18:23:32 +00:00
|
|
|
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
|
2021-01-01 11:11:09 +00:00
|
|
|
return context
|
2021-01-12 16:24:46 +00:00
|
|
|
|
|
|
|
|
2024-02-23 21:58:23 +00:00
|
|
|
class TournamentPaymentsView(VolunteerMixin, SingleTableMixin, DetailView):
|
|
|
|
"""
|
|
|
|
Display the list of payments of a tournament.
|
|
|
|
"""
|
|
|
|
model = Tournament
|
|
|
|
table_class = PaymentTable
|
|
|
|
template_name = "participation/tournament_payments.html"
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["title"] = _("Payments of {tournament}").format(tournament=self.object)
|
|
|
|
return context
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated or not self.request.user.registration.is_admin \
|
|
|
|
and not (self.request.user.registration.is_volunteer
|
|
|
|
and self.get_object() in self.request.user.registration.organized_tournaments.all()):
|
|
|
|
return self.handle_no_permission()
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
def get_table_data(self):
|
|
|
|
return Payment.objects.filter(registrations__team__participation__tournament=self.get_object()) \
|
|
|
|
.annotate(team_id=F('registrations__team')).order_by('-valid', 'registrations__team__trigram').all()
|
|
|
|
|
|
|
|
|
2022-04-22 16:02:27 +00:00
|
|
|
class TournamentExportCSVView(VolunteerMixin, DetailView):
|
|
|
|
"""
|
|
|
|
Export team information in a CSV file.
|
|
|
|
"""
|
|
|
|
model = Tournament
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
tournament = self.get_object()
|
|
|
|
|
|
|
|
resp = HttpResponse(
|
|
|
|
content_type='text/csv',
|
|
|
|
headers={'Content-Disposition': f'attachment; filename="Tournoi de {tournament.name}.csv"'},
|
|
|
|
)
|
2023-04-07 09:32:11 +00:00
|
|
|
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Nom', 'Prénom', 'Email',
|
|
|
|
'Genre', 'Date de naissance'))
|
2022-04-22 16:02:27 +00:00
|
|
|
writer.writeheader()
|
|
|
|
|
|
|
|
for participation in tournament.participations.filter(valid=True).order_by('team__trigram').all():
|
|
|
|
for registration in participation.team.participants\
|
|
|
|
.order_by('coachregistration', 'user__last_name').all():
|
|
|
|
writer.writerow({
|
|
|
|
'Tournoi': tournament.name,
|
|
|
|
'Équipe': participation.team.name,
|
|
|
|
'Trigramme': participation.team.trigram,
|
|
|
|
'Nom': registration.user.last_name,
|
|
|
|
'Prénom': registration.user.first_name,
|
2023-04-07 09:32:11 +00:00
|
|
|
'Email': registration.user.email,
|
2022-04-22 16:02:27 +00:00
|
|
|
'Genre': registration.get_gender_display() if isinstance(registration, StudentRegistration)
|
|
|
|
else 'Encandrant⋅e',
|
|
|
|
'Date de naissance': registration.birth_date if isinstance(registration, StudentRegistration)
|
|
|
|
else 'Encandrant⋅e',
|
|
|
|
})
|
|
|
|
|
|
|
|
return resp
|
|
|
|
|
|
|
|
|
2021-01-12 16:24:46 +00:00
|
|
|
class SolutionUploadView(LoginRequiredMixin, FormView):
|
|
|
|
template_name = "participation/upload_solution.html"
|
|
|
|
form_class = SolutionForm
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
qs = Participation.objects.filter(pk=self.kwargs["pk"])
|
|
|
|
if not qs.exists():
|
|
|
|
raise Http404
|
|
|
|
self.participation = qs.get()
|
2021-01-17 11:40:23 +00:00
|
|
|
if not self.request.user.is_authenticated or not self.request.user.registration.is_admin \
|
|
|
|
and not (self.request.user.registration.participates
|
|
|
|
and self.request.user.registration.team == self.participation.team):
|
|
|
|
return self.handle_no_permission()
|
2021-01-12 16:24:46 +00:00
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
2021-05-09 08:40:45 +00:00
|
|
|
@transaction.atomic
|
2021-01-12 16:24:46 +00:00
|
|
|
def form_valid(self, form):
|
|
|
|
"""
|
|
|
|
When a solution is submitted, it replaces a previous solution if existing,
|
|
|
|
otherwise it creates a new solution.
|
|
|
|
It is discriminating whenever the team is selected for the final tournament or not.
|
|
|
|
"""
|
|
|
|
form_sol = form.instance
|
2021-01-18 23:11:52 +00:00
|
|
|
sol_qs = Solution.objects.filter(participation=self.participation,
|
|
|
|
problem=form_sol.problem,
|
|
|
|
final_solution=self.participation.final)
|
|
|
|
|
2021-04-03 20:15:03 +00:00
|
|
|
tournament = Tournament.final_tournament() if self.participation.final else self.participation.tournament
|
2021-01-18 23:11:52 +00:00
|
|
|
if timezone.now() > tournament.solution_limit and sol_qs.exists():
|
|
|
|
form.add_error(None, _("You can't upload a solution after the deadline."))
|
|
|
|
return self.form_invalid(form)
|
|
|
|
|
2021-01-12 16:24:46 +00:00
|
|
|
# Drop previous solution if existing
|
2021-01-18 23:11:52 +00:00
|
|
|
for sol in sol_qs.all():
|
2021-01-14 16:26:08 +00:00
|
|
|
sol.file.delete()
|
2021-01-22 08:40:28 +00:00
|
|
|
sol.save()
|
2021-01-12 16:51:55 +00:00
|
|
|
sol.delete()
|
2021-01-12 16:24:46 +00:00
|
|
|
form_sol.participation = self.participation
|
2021-05-09 08:40:45 +00:00
|
|
|
form_sol.final_solution = self.participation.final
|
2021-01-12 16:24:46 +00:00
|
|
|
form_sol.save()
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy("participation:participation_detail", args=(self.participation.pk,))
|
2021-01-13 16:00:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
class PoolCreateView(AdminMixin, CreateView):
|
|
|
|
model = Pool
|
|
|
|
form_class = PoolForm
|
|
|
|
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
class PoolDetailView(LoginRequiredMixin, DetailView):
|
2021-01-13 16:00:50 +00:00
|
|
|
model = Pool
|
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
if request.user.registration.is_admin or request.user.registration.participates \
|
|
|
|
and request.user.registration.team \
|
|
|
|
and request.user.registration.team.participation in self.get_object().participations.all() \
|
|
|
|
or request.user.registration.is_volunteer \
|
|
|
|
and self.get_object().tournament in request.user.registration.interesting_tournaments:
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 18:23:32 +00:00
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
|
2021-04-15 21:03:51 +00:00
|
|
|
context["passages"] = PassageTable(self.object.passages.order_by('id').all())
|
2021-01-14 18:33:56 +00:00
|
|
|
|
2021-04-10 07:59:04 +00:00
|
|
|
if self.object.results_available or self.request.user.registration.is_volunteer:
|
|
|
|
# Hide notes before the end of the turn
|
|
|
|
notes = dict()
|
|
|
|
for participation in self.object.participations.all():
|
|
|
|
note = self.object.average(participation)
|
|
|
|
if note:
|
|
|
|
notes[participation] = note
|
|
|
|
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
|
2021-01-14 18:23:32 +00:00
|
|
|
|
|
|
|
return context
|
|
|
|
|
2021-01-13 16:00:50 +00:00
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
class PoolUpdateView(VolunteerMixin, UpdateView):
|
2021-01-13 16:00:50 +00:00
|
|
|
model = Pool
|
|
|
|
form_class = PoolForm
|
2021-01-14 13:44:12 +00:00
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
|
|
and (self.get_object().tournament in request.user.registration.organized_tournaments.all()
|
|
|
|
or request.user.registration in self.get_object().juries.all()):
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 13:44:12 +00:00
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
|
2021-01-14 13:44:12 +00:00
|
|
|
model = Pool
|
|
|
|
form_class = PoolTeamsForm
|
2021-01-14 14:59:11 +00:00
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
|
|
and (self.get_object().tournament in request.user.registration.organized_tournaments.all()
|
|
|
|
or request.user.registration in self.get_object().juries.all()):
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 14:59:11 +00:00
|
|
|
|
2023-05-19 12:44:31 +00:00
|
|
|
class PoolDownloadView(VolunteerMixin, DetailView):
|
|
|
|
"""
|
|
|
|
Download all solutions or syntheses as a ZIP archive.
|
|
|
|
"""
|
|
|
|
model = Pool
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
|
|
and (self.get_object().tournament in request.user.registration.organized_tournaments.all()
|
|
|
|
or request.user.registration in self.get_object().juries.all()):
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
pool = self.get_object()
|
|
|
|
|
|
|
|
is_solution = 'solutions' in request.path
|
|
|
|
|
|
|
|
output = BytesIO()
|
|
|
|
zf = ZipFile(output, "w")
|
|
|
|
for s in (pool.solutions if is_solution else Synthesis.objects.filter(passage__pool=pool).all()):
|
|
|
|
zf.write("media/" + s.file.name, f"{s}.pdf")
|
|
|
|
|
|
|
|
zf.close()
|
|
|
|
response = HttpResponse(content_type="application/zip")
|
|
|
|
filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \
|
|
|
|
if is_solution else _("Syntheses for pool {pool} of tournament {tournament}.zip")
|
|
|
|
filename = filename.format(pool=pool.get_letter_display() + str(pool.round), tournament=pool.tournament.name)
|
|
|
|
response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
|
|
|
|
.format(filename=filename)
|
|
|
|
response.write(output.getvalue())
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
2023-04-05 14:54:16 +00:00
|
|
|
class PoolAddJurysView(VolunteerMixin, FormView, DetailView):
|
2023-04-05 15:52:46 +00:00
|
|
|
"""
|
|
|
|
This view lets organizers set jurys for a pool, without multiplying clicks.
|
|
|
|
"""
|
2023-04-05 14:54:16 +00:00
|
|
|
model = Pool
|
|
|
|
form_class = AddJuryForm
|
|
|
|
template_name = 'participation/pool_add_jurys.html'
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context['title'] = _("Jurys of {pool}").format(pool=self.object)
|
|
|
|
return context
|
|
|
|
|
|
|
|
@transaction.atomic
|
|
|
|
def form_valid(self, form):
|
|
|
|
self.object = self.get_object()
|
|
|
|
|
2023-04-05 15:52:46 +00:00
|
|
|
# Save the user object first
|
2023-04-05 14:54:16 +00:00
|
|
|
form.save()
|
|
|
|
user = form.instance
|
2023-04-05 15:52:46 +00:00
|
|
|
# Create associated registration object to the new user
|
2023-04-05 14:54:16 +00:00
|
|
|
reg = VolunteerRegistration.objects.create(
|
|
|
|
user=user,
|
|
|
|
professional_activity="Juré⋅e du tournoi " + self.object.tournament.name,
|
|
|
|
)
|
2023-04-05 15:52:46 +00:00
|
|
|
# Add the user in the jury
|
2023-04-05 14:54:16 +00:00
|
|
|
self.object.juries.add(reg)
|
|
|
|
self.object.save()
|
|
|
|
|
|
|
|
reg.send_email_validation_link()
|
|
|
|
|
2023-04-05 15:52:46 +00:00
|
|
|
# Generate new password for the user
|
2023-04-05 14:54:16 +00:00
|
|
|
password = get_random_string(16)
|
|
|
|
user.set_password(password)
|
|
|
|
user.save()
|
|
|
|
|
2023-04-05 15:52:46 +00:00
|
|
|
# Send welcome mail
|
2023-04-05 14:54:16 +00:00
|
|
|
subject = "[TFJM²] " + str(_("New TFJM² jury account"))
|
|
|
|
site = Site.objects.first()
|
|
|
|
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
|
|
|
|
inviter=self.request.user,
|
|
|
|
password=password,
|
|
|
|
domain=site.domain))
|
|
|
|
html = render_to_string('registration/mails/add_organizer.html', dict(user=user,
|
|
|
|
inviter=self.request.user,
|
|
|
|
password=password,
|
|
|
|
domain=site.domain))
|
|
|
|
user.email_user(subject, message, html_message=html)
|
|
|
|
|
2023-04-05 15:52:46 +00:00
|
|
|
# Add notification
|
|
|
|
messages.success(self.request, _("The jury {name} has been successfully added!")
|
2023-04-05 14:54:16 +00:00
|
|
|
.format(name=f"{user.first_name} {user.last_name}"))
|
|
|
|
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
def form_invalid(self, form):
|
2023-04-05 15:52:46 +00:00
|
|
|
# This is useful since we have a FormView + a DetailView
|
2023-04-05 14:54:16 +00:00
|
|
|
self.object = self.get_object()
|
|
|
|
return super().form_invalid(form)
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy('participation:pool_add_jurys', args=(self.kwargs['pk'],))
|
|
|
|
|
|
|
|
|
2022-05-15 10:23:17 +00:00
|
|
|
class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
|
|
|
|
model = Pool
|
|
|
|
form_class = UploadNotesForm
|
|
|
|
template_name = 'participation/upload_notes.html'
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
self.object = self.get_object()
|
|
|
|
|
2023-04-10 08:05:14 +00:00
|
|
|
if request.user.is_authenticated and \
|
|
|
|
(request.user.registration.is_admin or request.user.registration.is_volunteer
|
|
|
|
and (self.object.tournament in request.user.registration.organized_tournaments.all()
|
|
|
|
or request.user.registration in self.object.juries.all())):
|
2022-05-15 10:23:17 +00:00
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
|
|
|
@transaction.atomic
|
|
|
|
def form_valid(self, form):
|
|
|
|
pool = self.get_object()
|
|
|
|
parsed_notes = form.cleaned_data['parsed_notes']
|
|
|
|
|
2023-04-10 15:38:58 +00:00
|
|
|
for vr in parsed_notes.keys():
|
2022-05-15 10:23:17 +00:00
|
|
|
if vr not in pool.juries.all():
|
|
|
|
form.add_error('file', _("The following user is not registered as a jury:") + " " + str(vr))
|
|
|
|
|
2023-04-10 15:38:58 +00:00
|
|
|
if form.errors:
|
|
|
|
return self.form_invalid(form)
|
|
|
|
|
|
|
|
for vr, notes in parsed_notes.items():
|
2023-04-07 11:16:49 +00:00
|
|
|
# There is an observer note for 4-teams pools
|
|
|
|
notes_count = 7 if pool.passages.count() == 4 else 6
|
2022-05-15 10:23:17 +00:00
|
|
|
for i, passage in enumerate(pool.passages.all()):
|
2022-05-15 14:16:41 +00:00
|
|
|
note = Note.objects.get_or_create(jury=vr, passage=passage)[0]
|
2023-04-07 11:16:49 +00:00
|
|
|
passage_notes = notes[notes_count * i:notes_count * (i + 1)]
|
2022-05-15 10:23:17 +00:00
|
|
|
note.set_all(*passage_notes)
|
|
|
|
note.save()
|
|
|
|
|
|
|
|
messages.success(self.request, _("Notes were successfully uploaded."))
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],))
|
|
|
|
|
|
|
|
|
2023-04-07 19:47:06 +00:00
|
|
|
class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|
|
|
"""
|
|
|
|
Generate an ODS sheet to fill the notes of the pool.
|
|
|
|
"""
|
|
|
|
model = Pool
|
|
|
|
|
|
|
|
def render_to_response(self, context, **response_kwargs): # noqa: C901
|
|
|
|
pool_size = self.object.passages.count()
|
|
|
|
passage_width = 7 if pool_size == 4 else 6
|
|
|
|
line_length = pool_size * passage_width
|
|
|
|
|
|
|
|
def getcol(number: int) -> str:
|
|
|
|
"""
|
|
|
|
Translates the given number to the nth column name
|
|
|
|
"""
|
|
|
|
if number == 0:
|
|
|
|
return ''
|
|
|
|
return getcol((number - 1) // 26) + chr(65 + (number - 1) % 26)
|
|
|
|
|
|
|
|
doc = OpenDocumentSpreadsheet()
|
|
|
|
|
|
|
|
# Define styles
|
|
|
|
style = Style(name="Contenu", family="table-cell")
|
|
|
|
style.addElement(TableCellProperties(border="0.75pt solid #000000"))
|
|
|
|
doc.styles.addElement(style)
|
|
|
|
|
|
|
|
style_left = Style(name="Contenu gauche", family="table-cell")
|
|
|
|
style_left.addElement(TableCellProperties(border="0.75pt solid #000000", borderleft="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_left)
|
|
|
|
|
|
|
|
style_right = Style(name="Contenu droite", family="table-cell")
|
|
|
|
style_right.addElement(TableCellProperties(border="0.75pt solid #000000", borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_right)
|
|
|
|
|
|
|
|
style_top = Style(name="Contenu haut", family="table-cell")
|
|
|
|
style_top.addElement(TableCellProperties(border="0.75pt solid #000000", bordertop="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_top)
|
|
|
|
|
|
|
|
style_topright = Style(name="Contenu haut droite", family="table-cell")
|
|
|
|
style_topright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderright="2pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_topright)
|
|
|
|
|
|
|
|
style_topleftright = Style(name="Contenu haut gauche droite", family="table-cell")
|
|
|
|
style_topleftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_topleftright)
|
|
|
|
|
|
|
|
style_leftright = Style(name="Contenu haut gauche droite", family="table-cell")
|
|
|
|
style_leftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_leftright)
|
|
|
|
|
|
|
|
style_botleft = Style(name="Contenu bas gauche", family="table-cell")
|
|
|
|
style_botleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_botleft)
|
|
|
|
|
|
|
|
style_bot = Style(name="Contenu bas", family="table-cell")
|
|
|
|
style_bot.addElement(TableCellProperties(border="0.75pt solid #000000", borderbottom="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_bot)
|
|
|
|
|
|
|
|
style_botright = Style(name="Contenu bas droite", family="table-cell")
|
|
|
|
style_botright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(style_botright)
|
|
|
|
|
|
|
|
title_style = Style(name="Titre", family="table-cell")
|
|
|
|
title_style.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style.addElement(TableCellProperties(border="0.75pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style)
|
|
|
|
|
|
|
|
title_style_left = Style(name="Titre gauche", family="table-cell")
|
|
|
|
title_style_left.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_left.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_left)
|
|
|
|
|
|
|
|
title_style_right = Style(name="Titre droite", family="table-cell")
|
|
|
|
title_style_right.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_right.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_right)
|
|
|
|
|
|
|
|
title_style_leftright = Style(name="Titre gauche droite", family="table-cell")
|
|
|
|
title_style_leftright.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_leftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_leftright)
|
|
|
|
|
|
|
|
title_style_top = Style(name="Titre haut", family="table-cell")
|
|
|
|
title_style_top.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_top.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_top)
|
|
|
|
|
|
|
|
title_style_topbot = Style(name="Titre haut bas", family="table-cell")
|
|
|
|
title_style_topbot.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_topbot.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_topbot)
|
|
|
|
|
|
|
|
title_style_topleft = Style(name="Titre haut gauche", family="table-cell")
|
|
|
|
title_style_topleft.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_topleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_topleft)
|
|
|
|
|
|
|
|
title_style_topbotleft = Style(name="Titre haut bas gauche", family="table-cell")
|
|
|
|
title_style_topbotleft.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_topbotleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_topbotleft)
|
|
|
|
|
|
|
|
title_style_topright = Style(name="Titre haut droite", family="table-cell")
|
|
|
|
title_style_topright.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_topright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_topright)
|
|
|
|
|
|
|
|
title_style_topbotright = Style(name="Titre haut bas droite", family="table-cell")
|
|
|
|
title_style_topbotright.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_topbotright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_topbotright)
|
|
|
|
|
|
|
|
title_style_topleftright = Style(name="Titre haut gauche droite", family="table-cell")
|
|
|
|
title_style_topleftright.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_topleftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
bordertop="2pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_topleftright)
|
|
|
|
|
|
|
|
title_style_bot = Style(name="Titre bas", family="table-cell")
|
|
|
|
title_style_bot.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_bot.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_bot)
|
|
|
|
|
|
|
|
title_style_botleft = Style(name="Titre bas gauche", family="table-cell")
|
|
|
|
title_style_botleft.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_botleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000",
|
|
|
|
borderleft="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_botleft)
|
|
|
|
|
|
|
|
title_style_botright = Style(name="Titre bas droite", family="table-cell")
|
|
|
|
title_style_botright.addElement(TextProperties(fontweight="bold"))
|
|
|
|
title_style_botright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
|
|
borderbottom="2pt solid #000000",
|
|
|
|
borderright="2pt solid #000000"))
|
|
|
|
doc.styles.addElement(title_style_botright)
|
|
|
|
|
|
|
|
first_col_style = Style(name="co1", family="table-column")
|
|
|
|
first_col_style.addElement(TableColumnProperties(columnwidth="9cm", breakbefore="auto"))
|
|
|
|
doc.automaticstyles.addElement(first_col_style)
|
|
|
|
|
|
|
|
col_style = Style(name="co2", family="table-column")
|
|
|
|
col_style.addElement(TableColumnProperties(columnwidth="2.6cm", breakbefore="auto"))
|
|
|
|
doc.automaticstyles.addElement(col_style)
|
|
|
|
|
|
|
|
obs_col_style = Style(name="co3", family="table-column")
|
|
|
|
obs_col_style.addElement(TableColumnProperties(columnwidth="5.2cm", breakbefore="auto"))
|
|
|
|
doc.automaticstyles.addElement(obs_col_style)
|
|
|
|
|
|
|
|
table = Table(name=f"Poule {self.object.get_letter_display()}{self.object.round}")
|
|
|
|
doc.spreadsheet.addElement(table)
|
|
|
|
|
|
|
|
table.addElement(TableColumn(stylename=first_col_style))
|
|
|
|
|
|
|
|
for i in range(line_length):
|
|
|
|
table.addElement(TableColumn(stylename=obs_col_style if pool_size == 4
|
|
|
|
and i % passage_width == passage_width - 1 else col_style))
|
|
|
|
|
|
|
|
# Add line for the problems for different passages
|
|
|
|
header_pb = TableRow()
|
|
|
|
table.addElement(header_pb)
|
|
|
|
problems_tc = TableCell(valuetype="string", stylename=title_style_topleft)
|
|
|
|
problems_tc.addElement(P(text="Problème"))
|
|
|
|
header_pb.addElement(problems_tc)
|
|
|
|
for passage in self.object.passages.all():
|
|
|
|
tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
|
|
|
tc.addElement(P(text=f"Problème {passage.solution_number}"))
|
|
|
|
tc.setAttribute('numbercolumnsspanned', "7" if pool_size == 4 else "6")
|
|
|
|
tc.setAttribute("formula", f"of:=[.B{8 + self.object.juries.count() + passage.position}]")
|
|
|
|
header_pb.addElement(tc)
|
|
|
|
header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=6 if pool_size == 4 else 5))
|
|
|
|
|
|
|
|
# Add roles on the second line of the table
|
|
|
|
header_role = TableRow()
|
|
|
|
table.addElement(header_role)
|
|
|
|
role_tc = TableCell(valuetype="string", stylename=title_style_left)
|
|
|
|
role_tc.addElement(P(text="Rôle"))
|
|
|
|
header_role.addElement(role_tc)
|
|
|
|
for i in range(pool_size):
|
|
|
|
defender_tc = TableCell(valuetype="string", stylename=title_style_left)
|
|
|
|
defender_tc.addElement(P(text="Défenseur⋅se"))
|
|
|
|
defender_tc.setAttribute('numbercolumnsspanned', "2")
|
|
|
|
header_role.addElement(defender_tc)
|
|
|
|
header_role.addElement(CoveredTableCell())
|
|
|
|
|
|
|
|
opponent_tc = TableCell(valuetype="string", stylename=title_style)
|
|
|
|
opponent_tc.addElement(P(text="Opposant⋅e"))
|
|
|
|
opponent_tc.setAttribute('numbercolumnsspanned', "2")
|
|
|
|
header_role.addElement(opponent_tc)
|
|
|
|
header_role.addElement(CoveredTableCell())
|
|
|
|
|
|
|
|
reporter_tc = TableCell(valuetype="string",
|
|
|
|
stylename=title_style_right if pool_size != 4 else title_style)
|
|
|
|
reporter_tc.addElement(P(text="Rapporteur⋅e"))
|
|
|
|
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
|
|
|
header_role.addElement(reporter_tc)
|
|
|
|
header_role.addElement(CoveredTableCell())
|
|
|
|
|
|
|
|
if pool_size == 4:
|
|
|
|
observer_tc = TableCell(valuetype="string", stylename=title_style_right)
|
|
|
|
observer_tc.addElement(P(text="Intervention exceptionnelle"))
|
|
|
|
header_role.addElement(observer_tc)
|
|
|
|
|
|
|
|
# Add maximum notes on the third line
|
|
|
|
header_notes = TableRow()
|
|
|
|
table.addElement(header_notes)
|
|
|
|
jury_tc = TableCell(valuetype="string", value="Juré⋅e", stylename=title_style_botleft)
|
|
|
|
jury_tc.addElement(P(text="Juré⋅e"))
|
|
|
|
header_notes.addElement(jury_tc)
|
|
|
|
|
|
|
|
for i in range(pool_size):
|
|
|
|
defender_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
|
|
|
defender_w_tc.addElement(P(text="Écrit (/20)"))
|
|
|
|
header_notes.addElement(defender_w_tc)
|
|
|
|
|
|
|
|
defender_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
|
|
defender_o_tc.addElement(P(text="Oral (/16)"))
|
|
|
|
header_notes.addElement(defender_o_tc)
|
|
|
|
|
|
|
|
opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
|
|
opponent_w_tc.addElement(P(text="Écrit (/9)"))
|
|
|
|
header_notes.addElement(opponent_w_tc)
|
|
|
|
|
|
|
|
opponent_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
|
|
opponent_o_tc.addElement(P(text="Oral (/10)"))
|
|
|
|
header_notes.addElement(opponent_o_tc)
|
|
|
|
|
|
|
|
reporter_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
|
|
reporter_w_tc.addElement(P(text="Écrit (/9)"))
|
|
|
|
header_notes.addElement(reporter_w_tc)
|
|
|
|
|
|
|
|
reporter_o_tc = TableCell(valuetype="string",
|
|
|
|
stylename=title_style_botright if pool_size != 4 else title_style_bot)
|
|
|
|
reporter_o_tc.addElement(P(text="Oral (/10)"))
|
|
|
|
header_notes.addElement(reporter_o_tc)
|
|
|
|
|
|
|
|
if pool_size == 4:
|
|
|
|
observer_tc = TableCell(valuetype="string",
|
|
|
|
stylename=title_style_botright)
|
|
|
|
observer_tc.addElement(P(text="Oral (± 4)"))
|
|
|
|
header_notes.addElement(observer_tc)
|
|
|
|
|
|
|
|
# Add a notation line for each jury
|
|
|
|
for jury in self.object.juries.all():
|
|
|
|
jury_row = TableRow()
|
|
|
|
table.addElement(jury_row)
|
|
|
|
|
|
|
|
name_tc = TableCell(valuetype="string", stylename=style_leftright)
|
|
|
|
name_tc.addElement(P(text=f"{jury.user.first_name} {jury.user.last_name}"))
|
|
|
|
jury_row.addElement(name_tc)
|
|
|
|
|
|
|
|
for passage in self.object.passages.all():
|
|
|
|
notes = Note.objects.get(jury=jury, passage=passage)
|
|
|
|
for j, note in enumerate(notes.get_all()):
|
|
|
|
note_tc = TableCell(valuetype="float", value=note,
|
|
|
|
stylename=style_right if j == passage_width - 1 else style)
|
|
|
|
note_tc.addElement(P(text=str(note)))
|
|
|
|
jury_row.addElement(note_tc)
|
|
|
|
|
|
|
|
jury_size = self.object.juries.count()
|
|
|
|
min_row = 4
|
|
|
|
max_row = 4 + jury_size - 1
|
|
|
|
min_column = 2
|
|
|
|
|
|
|
|
# Add line for averages
|
|
|
|
average_row = TableRow()
|
|
|
|
table.addElement(average_row)
|
|
|
|
average_tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
|
|
|
average_tc.addElement(P(text="Moyenne"))
|
|
|
|
average_row.addElement(average_tc)
|
|
|
|
for i, passage in enumerate(self.object.passages.all()):
|
|
|
|
for j, note in enumerate(passage.averages):
|
|
|
|
tc = TableCell(valuetype="float", value=note,
|
|
|
|
stylename=style_topright if j == passage_width - 1 else style_top)
|
|
|
|
tc.addElement(P(text=str(note)))
|
|
|
|
column = getcol(min_column + i * passage_width + j)
|
|
|
|
tc.setAttribute("formula", f"of:=AVERAGEIF([.${getcol(min_column + i * passage_width)}${min_row}"
|
|
|
|
f":${getcol(min_column + i * passage_width)}{max_row}]; \">0\"; "
|
|
|
|
f"[.{column}${min_row}:{column}{max_row}])")
|
|
|
|
average_row.addElement(tc)
|
|
|
|
|
|
|
|
# Add coefficients for each note on the next line
|
|
|
|
coeff_row = TableRow()
|
|
|
|
table.addElement(coeff_row)
|
|
|
|
coeff_tc = TableCell(valuetype="string", stylename=title_style_leftright)
|
|
|
|
coeff_tc.addElement(P(text="Coefficient"))
|
|
|
|
coeff_row.addElement(coeff_tc)
|
|
|
|
for passage in self.object.passages.all():
|
|
|
|
defender_w_tc = TableCell(valuetype="float", value=1, stylename=style_left)
|
|
|
|
defender_w_tc.addElement(P(text="1"))
|
|
|
|
coeff_row.addElement(defender_w_tc)
|
|
|
|
|
|
|
|
defender_o_tc = TableCell(valuetype="float", value=2 - 0.5 * passage.defender_penalties, stylename=style)
|
|
|
|
defender_o_tc.addElement(P(text=str(2 - 0.5 * passage.defender_penalties)))
|
|
|
|
coeff_row.addElement(defender_o_tc)
|
|
|
|
|
|
|
|
opponent_w_tc = TableCell(valuetype="float", value=1, stylename=style)
|
|
|
|
opponent_w_tc.addElement(P(text="1"))
|
|
|
|
coeff_row.addElement(opponent_w_tc)
|
|
|
|
|
|
|
|
opponent_o_tc = TableCell(valuetype="float", value=2, stylename=style)
|
|
|
|
opponent_o_tc.addElement(P(text="2"))
|
|
|
|
coeff_row.addElement(opponent_o_tc)
|
|
|
|
|
|
|
|
reporter_w_tc = TableCell(valuetype="float", value=1, stylename=style)
|
|
|
|
reporter_w_tc.addElement(P(text="1"))
|
|
|
|
coeff_row.addElement(reporter_w_tc)
|
|
|
|
|
|
|
|
reporter_o_tc = TableCell(valuetype="float", value=1,
|
|
|
|
stylename=style_right if pool_size != 4 else style)
|
|
|
|
reporter_o_tc.addElement(P(text="1"))
|
|
|
|
coeff_row.addElement(reporter_o_tc)
|
|
|
|
|
|
|
|
if pool_size == 4:
|
|
|
|
observer_tc = TableCell(valuetype="float", value=1, stylename=style_right)
|
|
|
|
observer_tc.addElement(P(text="1"))
|
|
|
|
coeff_row.addElement(observer_tc)
|
|
|
|
|
|
|
|
# Add the subtotal on the next line
|
|
|
|
subtotal_row = TableRow()
|
|
|
|
table.addElement(subtotal_row)
|
|
|
|
subtotal_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
|
|
|
subtotal_tc.addElement(P(text="Sous-total"))
|
|
|
|
subtotal_row.addElement(subtotal_tc)
|
|
|
|
for i, passage in enumerate(self.object.passages.all()):
|
|
|
|
def_w_col = getcol(min_column + passage_width * i)
|
|
|
|
def_o_col = getcol(min_column + passage_width * i + 1)
|
|
|
|
defender_tc = TableCell(valuetype="float", value=passage.average_defender, stylename=style_botleft)
|
|
|
|
defender_tc.addElement(P(text=str(passage.average_defender)))
|
|
|
|
defender_tc.setAttribute('numbercolumnsspanned', "2")
|
|
|
|
defender_tc.setAttribute("formula", f"of:=[.{def_w_col}{max_row + 1}] * [.{def_w_col}{max_row + 2}]"
|
|
|
|
f" + [.{def_o_col}{max_row + 1}] * [.{def_o_col}{max_row + 2}]")
|
|
|
|
subtotal_row.addElement(defender_tc)
|
|
|
|
subtotal_row.addElement(CoveredTableCell())
|
|
|
|
|
|
|
|
opp_w_col = getcol(min_column + passage_width * i + 2)
|
|
|
|
opp_o_col = getcol(min_column + passage_width * i + 3)
|
|
|
|
opponent_tc = TableCell(valuetype="float", value=passage.average_opponent, stylename=style_bot)
|
|
|
|
opponent_tc.addElement(P(text=str(passage.average_opponent)))
|
|
|
|
opponent_tc.setAttribute('numbercolumnsspanned', "2")
|
|
|
|
opponent_tc.setAttribute("formula", f"of:=[.{opp_w_col}{max_row + 1}] * [.{opp_w_col}{max_row + 2}]"
|
|
|
|
f" + [.{opp_o_col}{max_row + 1}] * [.{opp_o_col}{max_row + 2}]")
|
|
|
|
subtotal_row.addElement(opponent_tc)
|
|
|
|
subtotal_row.addElement(CoveredTableCell())
|
|
|
|
|
|
|
|
rep_w_col = getcol(min_column + passage_width * i + 4)
|
|
|
|
rep_o_col = getcol(min_column + passage_width * i + 5)
|
|
|
|
reporter_tc = TableCell(valuetype="float", value=passage.average_reporter,
|
|
|
|
stylename=style_botright if pool_size != 4 else style_bot)
|
|
|
|
reporter_tc.addElement(P(text=str(passage.average_reporter)))
|
|
|
|
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
|
|
|
reporter_tc.setAttribute("formula", f"of:=[.{rep_w_col}{max_row + 1}] * [.{rep_w_col}{max_row + 2}]"
|
|
|
|
f" + [.{rep_o_col}{max_row + 1}] * [.{rep_o_col}{max_row + 2}]")
|
|
|
|
subtotal_row.addElement(reporter_tc)
|
|
|
|
subtotal_row.addElement(CoveredTableCell())
|
|
|
|
|
|
|
|
if pool_size == 4:
|
|
|
|
obs_col = getcol(min_column + passage_width * i + 6)
|
|
|
|
observer_tc = TableCell(valuetype="float", value=passage.average_observer,
|
|
|
|
stylename=style_botright)
|
|
|
|
observer_tc.addElement(P(text=str(passage.average_observer)))
|
|
|
|
observer_tc.setAttribute("formula", f"of:=[.{obs_col}{max_row + 1}] * [.{obs_col}{max_row + 2}]")
|
|
|
|
subtotal_row.addElement(observer_tc)
|
|
|
|
|
|
|
|
table.addElement(TableRow())
|
|
|
|
|
|
|
|
# Compute the total scores in a new table
|
|
|
|
scores_header = TableRow()
|
|
|
|
table.addElement(scores_header)
|
|
|
|
team_tc = TableCell(valuetype="string", stylename=title_style_topbotleft)
|
|
|
|
team_tc.addElement(P(text="Équipe"))
|
|
|
|
scores_header.addElement(team_tc)
|
|
|
|
problem_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
|
|
|
problem_tc.addElement(P(text="Problème"))
|
|
|
|
scores_header.addElement(problem_tc)
|
|
|
|
total_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
|
|
|
total_tc.addElement(P(text="Total"))
|
|
|
|
scores_header.addElement(total_tc)
|
|
|
|
rank_tc = TableCell(valuetype="string", stylename=title_style_topbotright)
|
|
|
|
rank_tc.addElement(P(text="Rang"))
|
|
|
|
scores_header.addElement(rank_tc)
|
|
|
|
|
|
|
|
# For each line of the matrix P, the ith team is defender on the passage number Pi0,
|
|
|
|
# opponent on the passage number Pi1, reporter on the passage number Pi2
|
|
|
|
# and eventually observer on the passage number Pi3.
|
|
|
|
passage_matrix = []
|
|
|
|
match pool_size:
|
|
|
|
case 3:
|
|
|
|
passage_matrix = [
|
|
|
|
[0, 2, 1],
|
|
|
|
[1, 0, 2],
|
|
|
|
[2, 1, 0],
|
|
|
|
]
|
|
|
|
case 4:
|
|
|
|
passage_matrix = [
|
|
|
|
[0, 3, 2, 1],
|
|
|
|
[1, 0, 3, 2],
|
|
|
|
[2, 1, 0, 3],
|
|
|
|
[3, 2, 1, 0],
|
|
|
|
]
|
|
|
|
case 5:
|
|
|
|
passage_matrix = [
|
|
|
|
[0, 2, 3],
|
|
|
|
[1, 4, 2],
|
|
|
|
[2, 0, 4],
|
|
|
|
[3, 1, 0],
|
|
|
|
[4, 3, 1],
|
|
|
|
]
|
|
|
|
|
|
|
|
sorted_participations = sorted(self.object.participations.all(), key=lambda p: -self.object.average(p))
|
|
|
|
for passage in self.object.passages.all():
|
|
|
|
team_row = TableRow()
|
|
|
|
table.addElement(team_row)
|
|
|
|
|
|
|
|
team_tc = TableCell(valuetype="string",
|
|
|
|
stylename=style_botleft if passage.position == pool_size else style_left)
|
|
|
|
team_tc.addElement(P(text=f"{passage.defender.team.name} ({passage.defender.team.trigram})"))
|
|
|
|
team_row.addElement(team_tc)
|
|
|
|
|
|
|
|
problem_tc = TableCell(valuetype="string",
|
|
|
|
stylename=style_bot if passage.position == pool_size else style)
|
|
|
|
problem_tc.addElement(P(text=f"Problème {passage.solution_number}"))
|
|
|
|
team_row.addElement(problem_tc)
|
|
|
|
|
|
|
|
passage_line = passage_matrix[passage.position - 1]
|
|
|
|
score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender),
|
|
|
|
stylename=style_bot if passage.position == pool_size else style)
|
|
|
|
score_tc.addElement(P(text=self.object.average(passage.defender)))
|
|
|
|
formula = "of:="
|
|
|
|
formula += getcol(min_column + passage_line[0] * passage_width) + str(max_row + 3) # Defender
|
|
|
|
formula += " + " + getcol(min_column + passage_line[1] * passage_width + 2) + str(max_row + 3) # Opponent
|
|
|
|
formula += " + " + getcol(min_column + passage_line[2] * passage_width + 4) + str(max_row + 3) # Reporter
|
|
|
|
if pool_size == 4:
|
|
|
|
# Observer
|
|
|
|
formula += " + " + getcol(min_column + passage_line[3] * passage_width + 6) + str(max_row + 3)
|
|
|
|
score_tc.setAttribute("formula", formula)
|
|
|
|
team_row.addElement(score_tc)
|
|
|
|
|
|
|
|
score_col = 'C'
|
|
|
|
rank_tc = TableCell(valuetype="float", value=sorted_participations.index(passage.defender) + 1,
|
|
|
|
stylename=style_botright if passage.position == pool_size else style_right)
|
|
|
|
rank_tc.addElement(P(text=str(sorted_participations.index(passage.defender) + 1)))
|
|
|
|
rank_tc.setAttribute("formula", f"of:=RANK([.{score_col}{max_row + 5 + passage.position}]; "
|
|
|
|
f"[.{score_col}${max_row + 6}]:[.{score_col}${max_row + 5 + pool_size}])")
|
|
|
|
team_row.addElement(rank_tc)
|
|
|
|
|
|
|
|
table.addElement(TableRow())
|
|
|
|
|
|
|
|
# Add small instructions
|
|
|
|
instructions_tr = TableRow()
|
|
|
|
table.addElement(instructions_tr)
|
|
|
|
instructions_tc = TableCell()
|
|
|
|
instructions_tc.addElement(P(text="Merci de ne pas toucher aux noms des juré⋅es.\n"
|
|
|
|
"Si nécessaire, faites les modifications sur le site\n"
|
|
|
|
"et récupérez le nouveau template.\n"
|
|
|
|
"N'entrez que des notes entières.\n"
|
|
|
|
"Ne retirez pas de 0 : toute ligne incomplète sera ignorée.\n"
|
|
|
|
"Dans le cadre de poules à 5, laissez des 0 en face des\n"
|
|
|
|
"juré⋅es qui ne sont pas dans le passage souhaité,\n"
|
|
|
|
"et remplissez uniquement les notes nécessaires dans le tableau.\n"
|
|
|
|
"Les moyennes calculées ignorent les 0, donc pas d'inquiétude."))
|
|
|
|
instructions_tr.addElement(instructions_tc)
|
|
|
|
|
|
|
|
# Save the sheet in a temporary file and send it in the response
|
|
|
|
doc.save('/tmp/notes.ods')
|
|
|
|
|
|
|
|
return FileResponse(streaming_content=open("/tmp/notes.ods", "rb"),
|
|
|
|
content_type="application/vnd.oasis.opendocument.spreadsheet",
|
|
|
|
filename=f"Feuille de notes - {self.object.tournament.name} "
|
|
|
|
f"- Poule {self.object.get_letter_display()}{self.object.round}.ods")
|
|
|
|
|
|
|
|
|
2023-04-06 21:38:03 +00:00
|
|
|
class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
|
|
|
"""
|
|
|
|
Generate a PDF from a LaTeX template for the notation papers.
|
|
|
|
"""
|
|
|
|
model = Pool
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
2023-04-06 21:58:59 +00:00
|
|
|
|
|
|
|
passages = self.object.passages.all()
|
|
|
|
if passages.count() == 5:
|
|
|
|
page = self.request.GET.get('page', '1')
|
|
|
|
if not page.isnumeric() or page not in ['1', '2']:
|
|
|
|
page = '1'
|
|
|
|
passages = passages.filter(id__in=[passages[0].id, passages[2].id, passages[4].id]
|
|
|
|
if page == '1' else [passages[1].id, passages[3].id])
|
|
|
|
context['page'] = page
|
|
|
|
|
|
|
|
context['passages'] = passages
|
|
|
|
context['esp'] = passages.count() * '&'
|
2023-04-06 21:38:03 +00:00
|
|
|
context['is_jury'] = self.request.user.registration in self.object.juries.all() \
|
|
|
|
and 'blank' not in self.request.GET
|
|
|
|
context['tfjm_number'] = timezone.now().year - 2010
|
|
|
|
return context
|
|
|
|
|
|
|
|
def render_to_response(self, context, **response_kwargs):
|
|
|
|
tex = render_to_string(self.template_name, context=context, request=self.request)
|
|
|
|
temp_dir = mkdtemp()
|
|
|
|
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
|
|
|
f.write(tex)
|
|
|
|
process = subprocess.Popen(["pdflatex", "-interaction=nonstopmode", f"-output-directory={temp_dir}",
|
|
|
|
os.path.join(temp_dir, "texput.tex"), ])
|
|
|
|
process.wait()
|
|
|
|
return FileResponse(streaming_content=open(os.path.join(temp_dir, "texput.pdf"), "rb"),
|
|
|
|
content_type="application/pdf",
|
|
|
|
filename=self.template_name.split("/")[-1][:-3] + "pdf")
|
|
|
|
|
|
|
|
|
|
|
|
class ScaleNotationSheetTemplateView(NotationSheetTemplateView):
|
|
|
|
template_name = 'participation/tex/bareme.tex'
|
|
|
|
|
|
|
|
|
|
|
|
class FinalNotationSheetTemplateView(NotationSheetTemplateView):
|
|
|
|
template_name = 'participation/tex/finale.tex'
|
|
|
|
|
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
class PassageCreateView(VolunteerMixin, CreateView):
|
2021-01-14 14:59:11 +00:00
|
|
|
model = Passage
|
|
|
|
form_class = PassageForm
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
2021-01-17 11:40:23 +00:00
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 14:59:11 +00:00
|
|
|
qs = Pool.objects.filter(pk=self.kwargs["pk"])
|
|
|
|
if not qs.exists():
|
|
|
|
raise Http404
|
|
|
|
self.pool = qs.get()
|
2021-01-17 11:40:23 +00:00
|
|
|
|
|
|
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
|
|
and (self.pool.tournament in request.user.registration.organized_tournaments.all()
|
|
|
|
or request.user.registration in self.pool.juries.all()):
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
return self.handle_no_permission()
|
2021-01-14 14:59:11 +00:00
|
|
|
|
|
|
|
def get_form(self, form_class=None):
|
|
|
|
form = super().get_form(form_class)
|
|
|
|
form.instance.pool = self.pool
|
2021-01-18 23:13:22 +00:00
|
|
|
form.fields["defender"].queryset = self.pool.participations.all()
|
2021-01-14 14:59:11 +00:00
|
|
|
form.fields["opponent"].queryset = self.pool.participations.all()
|
|
|
|
form.fields["reporter"].queryset = self.pool.participations.all()
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
class PassageDetailView(LoginRequiredMixin, DetailView):
|
2021-01-14 14:59:11 +00:00
|
|
|
model = Passage
|
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
|
|
and (self.get_object().pool.tournament in request.user.registration.organized_tournaments.all()
|
|
|
|
or request.user.registration in self.get_object().pool.juries.all()) \
|
|
|
|
or request.user.registration.participates and request.user.registration.team \
|
|
|
|
and request.user.registration.team.participation in [self.get_object().defender,
|
|
|
|
self.get_object().opponent,
|
|
|
|
self.get_object().reporter]:
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 17:21:22 +00:00
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
if self.request.user.registration in self.object.pool.juries.all():
|
2022-04-29 16:53:34 +00:00
|
|
|
context["my_note"] = Note.objects.get_or_create(passage=self.object, jury=self.request.user.registration)[0]
|
2021-01-14 17:43:53 +00:00
|
|
|
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
|
2021-04-04 11:30:02 +00:00
|
|
|
elif self.request.user.registration.is_admin:
|
|
|
|
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
|
2023-04-07 10:10:25 +00:00
|
|
|
if 'notes' in context and not self.object.observer:
|
|
|
|
# Only display the observer column for 4-teams pools
|
|
|
|
context['notes']._sequence.pop()
|
2021-01-14 17:21:22 +00:00
|
|
|
return context
|
|
|
|
|
2021-01-14 14:59:11 +00:00
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
class PassageUpdateView(VolunteerMixin, UpdateView):
|
2021-01-14 14:59:11 +00:00
|
|
|
model = Passage
|
|
|
|
form_class = PassageForm
|
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
|
|
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
|
|
and (self.get_object().pool.tournament in request.user.registration.organized_tournaments.all()
|
|
|
|
or request.user.registration in self.get_object().pool.juries.all()):
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
|
|
|
|
class SynthesisUploadView(LoginRequiredMixin, FormView):
|
|
|
|
template_name = "participation/upload_synthesis.html"
|
|
|
|
form_class = SynthesisForm
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
2021-01-17 11:40:23 +00:00
|
|
|
if not request.user.is_authenticated or not request.user.registration.participates:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
qs = Passage.objects.filter(pk=self.kwargs["pk"])
|
|
|
|
if not qs.exists():
|
|
|
|
raise Http404
|
|
|
|
self.participation = self.request.user.registration.team.participation
|
|
|
|
self.passage = qs.get()
|
2021-01-17 11:40:23 +00:00
|
|
|
|
2021-04-04 16:13:30 +00:00
|
|
|
if self.participation not in [self.passage.opponent, self.passage.reporter]:
|
2021-01-17 11:40:23 +00:00
|
|
|
return self.handle_no_permission()
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
|
"""
|
|
|
|
When a solution is submitted, it replaces a previous solution if existing,
|
|
|
|
otherwise it creates a new solution.
|
|
|
|
It is discriminating whenever the team is selected for the final tournament or not.
|
|
|
|
"""
|
|
|
|
form_syn = form.instance
|
2021-04-04 16:13:30 +00:00
|
|
|
form_syn.type = 1 if self.participation == self.passage.opponent else 2
|
2021-01-18 23:11:52 +00:00
|
|
|
syn_qs = Synthesis.objects.filter(participation=self.participation,
|
|
|
|
passage=self.passage,
|
|
|
|
type=form_syn.type).all()
|
|
|
|
|
|
|
|
deadline = self.passage.pool.tournament.syntheses_first_phase_limit if self.passage.pool.round == 1 \
|
|
|
|
else self.passage.pool.tournament.syntheses_second_phase_limit
|
|
|
|
if syn_qs.exists() and timezone.now() > deadline:
|
|
|
|
form.add_error(None, _("You can't upload a synthesis after the deadline."))
|
|
|
|
return self.form_invalid(form)
|
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
# Drop previous solution if existing
|
2021-01-18 23:11:52 +00:00
|
|
|
for syn in syn_qs.all():
|
2021-01-14 16:26:08 +00:00
|
|
|
syn.file.delete()
|
2021-01-22 08:40:28 +00:00
|
|
|
syn.save()
|
2021-01-14 16:26:08 +00:00
|
|
|
syn.delete()
|
|
|
|
form_syn.participation = self.participation
|
|
|
|
form_syn.passage = self.passage
|
|
|
|
form_syn.save()
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
|
2021-01-14 17:21:22 +00:00
|
|
|
|
|
|
|
|
2021-01-17 11:40:23 +00:00
|
|
|
class NoteUpdateView(VolunteerMixin, UpdateView):
|
2021-01-14 17:21:22 +00:00
|
|
|
model = Note
|
|
|
|
form_class = NoteForm
|
2021-01-17 11:40:23 +00:00
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
return self.handle_no_permission()
|
|
|
|
|
|
|
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
|
|
and self.get_object().jury == request.user.registration:
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
return self.handle_no_permission()
|
2023-04-07 10:10:25 +00:00
|
|
|
|
|
|
|
def get_form(self, form_class=None):
|
|
|
|
form = super().get_form(form_class)
|
|
|
|
if not self.object.passage.observer:
|
|
|
|
# Set the note of the observer only for 4-teams pools
|
|
|
|
del form.fields['observer_oral']
|
|
|
|
return form
|