plateforme-tfjm2/participation/views.py

2124 lines
103 KiB
Python
Raw Permalink Normal View History

2020-12-27 10:49:54 +00:00
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import asyncio
from concurrent.futures import ThreadPoolExecutor
import csv
from hashlib import sha1
2020-12-27 10:49:54 +00:00
from io import BytesIO
import os
import subprocess
from tempfile import mkdtemp
from typing import Any, Dict
2020-12-27 10:49:54 +00:00
from zipfile import ZipFile
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
from django.db.models import F
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
from django.utils import timezone, translation
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.timezone import localtime
2020-12-27 10:49:54 +00:00
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
from django.views.generic.detail import SingleObjectMixin
2020-12-27 10:49:54 +00:00
from django.views.generic.edit import FormMixin, ProcessFormView
from django_tables2 import MultiTableMixin, SingleTableMixin, SingleTableView
import gspread
2020-12-27 10:49:54 +00:00
from magic import Magic
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
from registration.models import Payment, VolunteerRegistration
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
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
PoolForm, RequestValidationForm, SolutionForm, TeamForm, TournamentForm, UploadNotesForm, \
ValidateParticipationForm, WrittenReviewForm
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
2021-01-17 15:23:48 +00:00
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
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"))
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
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,
# their health sheet, they confirmed their email address and under-18 people sent their parental authorization.
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, "
"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()
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)
send_mail(f"[{settings.APP_NAME}] {_('Team validation')}", mail_plain, settings.DEFAULT_FROM_EMAIL,
[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()):
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()
domain = Site.objects.first().domain
for registration in self.object.participants.all():
if settings.PAYMENT_MANAGEMENT and \
registration.is_student and self.object.participation.tournament.price:
payment = Payment.objects.get(registrations=registration, final=False)
else:
payment = None
mail_context_plain = dict(domain=domain, registration=registration, team=self.object, payment=payment,
message=form.cleaned_data["message"])
mail_context_html = dict(domain=domain, registration=registration, team=self.object, payment=payment,
message=form.cleaned_data["message"].replace('\n', '<br>'))
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain)
mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html)
registration.user.email_user(f"[{settings.APP_NAME}] {_('Team validated')}", 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_plain = dict(team=self.object, message=form.cleaned_data["message"])
mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>'))
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain)
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html)
send_mail(f"[{settings.APP_NAME}] {_('Team not validated')}", mail_plain,
None, [self.object.email], html_message=mail_html)
2020-12-27 10:49:54 +00:00
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)
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)
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)
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
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)
class TeamAuthorizationsView(LoginRequiredMixin, View):
2020-12-27 10:49:54 +00:00
"""
Get as a ZIP archive all the authorizations that are sent
"""
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
if 'team_id' in kwargs:
team = Team.objects.get(pk=kwargs["team_id"])
tournament = team.participation.tournament
else:
team = None
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
if user.registration.is_admin or user.registration.is_volunteer \
and (user.registration in tournament.organizers.all()
or (team is not None and team.participation.final
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):
if 'team_id' in kwargs:
team = Team.objects.get(pk=kwargs["team_id"])
tournament = team.participation.tournament
teams = [team]
filename = _("Authorizations of team {trigram}.zip").format(trigram=team.trigram)
else:
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
teams = [p.team for p in tournament.participations.filter(valid=True)]
filename = _("Authorizations of {tournament}.zip").format(tournament=tournament.name)
magic = Magic(mime=True)
2020-12-27 10:49:54 +00:00
output = BytesIO()
zf = ZipFile(output, "w")
for team in teams:
team_prefix = f"{team.trigram}/" if len(teams) > 1 else ""
for participant in team.participants.all():
user_prefix = f"{team_prefix}{participant.user.first_name} {participant.user.last_name}/"
if 'team_id' in kwargs or not tournament.final:
# Don't include the photo authorization and the parental authorization of the regional tournament
# in the final authorizations
if participant.photo_authorization \
and participant.photo_authorization.storage.exists(participant.photo_authorization.path):
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,
user_prefix + _("Photo authorization of {participant}.{ext}")
.format(participant=str(participant), ext=ext))
if participant.is_student and participant.parental_authorization \
and participant.parental_authorization.storage.exists(
participant.parental_authorization.path):
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,
user_prefix + _("Parental authorization of {participant}.{ext}")
.format(participant=str(participant), ext=ext))
if participant.is_student and participant.health_sheet \
and participant.health_sheet.storage.exists(participant.health_sheet.path):
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,
user_prefix + _("Health sheet of {participant}.{ext}")
.format(participant=str(participant), ext=ext))
if participant.is_student and participant.vaccine_sheet \
and participant.vaccine_sheet.storage.exists(participant.vaccine_sheet.path):
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,
user_prefix + _("Vaccine sheet of {participant}.{ext}")
.format(participant=str(participant), ext=ext))
if 'team_id' in kwargs or tournament.final:
# Don't include final authorizations in the regional authorizations
if participant.photo_authorization_final \
and participant.photo_authorization_final.storage.exists(
participant.photo_authorization_final.path):
mime_type = magic.from_file("media/" + participant.photo_authorization_final.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + participant.photo_authorization_final.name,
user_prefix + _("Photo authorization of {participant} (final).{ext}")
.format(participant=str(participant), ext=ext))
if participant.is_student and participant.parental_authorization_final \
and participant.parental_authorization_final.storage.exists(
participant.parental_authorization_final.path):
mime_type = magic.from_file("media/" + participant.parental_authorization_final.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + participant.parental_authorization_final.name,
user_prefix + _("Parental authorization of {participant} (final).{ext}")
.format(participant=str(participant), ext=ext))
if team.motivation_letter and team.motivation_letter.storage.exists(team.motivation_letter.path):
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,
team_prefix + _("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"] = f"attachment; filename=\"{filename}\""
2020-12-27 10:49:54 +00:00
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)
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
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
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
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
sorted_notes = sorted(notes.items(), key=lambda x: x[1], reverse=True)
context["notes"] = sorted_notes
context["available_notes_1"] = all(pool.results_available for pool in self.object.pools.filter(round=1).all())
context["available_notes_2"] = all(pool.results_available for pool in self.object.pools.filter(round=2).all())
context["available_notes_3"] = all(pool.results_available for pool in self.object.pools.filter(round=3).all())
2021-01-14 18:23:32 +00:00
if settings.HAS_FINAL and not self.object.final and notes and context["available_notes_2"] \
and not self.request.user.is_anonymous and self.request.user.registration.is_volunteer:
context["team_selectable_for_final"] = next(participation for participation, _note in sorted_notes
if not participation.final)
2021-01-01 11:11:09 +00:00
return context
2021-01-12 16:24:46 +00:00
def get_tables(self):
return [
ParticipationTable(self.object.participations.all()),
PoolTable(self.object.pools.all()),
]
2021-01-12 16:24:46 +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):
if self.object.final:
payments = Payment.objects.filter(final=True)
else:
payments = Payment.objects.filter(registrations__team__participation__tournament=self.get_object())
return payments.annotate(team_id=F('registrations__team')).order_by('-valid', 'registrations__team__trigram') \
.distinct().all()
2022-04-22 16:02:27 +00:00
class TournamentExportCSVView(VolunteerMixin, DetailView):
"""
Export all team informations in a CSV file.
2022-04-22 16:02:27 +00:00
"""
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"'},
)
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Sélectionnée',
'Nom', 'Prénom', 'Email', 'Type', 'Genre', 'Date de naissance',
'Adresse', 'Code postal', 'Ville', 'Pays', 'Téléphone',
'Classe', 'Établissement',
'Nom responsable légal⋅e', 'Téléphone responsable légal⋅e',
'Email responsable légal⋅e',
'Problèmes de santé', 'Contraintes de logement'))
2022-04-22 16:02:27 +00:00
writer.writeheader()
participations = tournament.participations
if 'all' not in request.GET:
participations = participations.filter(valid=True)
for participation in participations.order_by('-valid', 'team__trigram').all():
for registration in participation.team.participants \
2022-04-22 16:02:27 +00:00
.order_by('coachregistration', 'user__last_name').all():
writer.writerow({
'Tournoi': tournament.name,
'Équipe': participation.team.name,
'Trigramme': participation.team.trigram,
'Sélectionnée': ("oui" if participation.valid else
"en attente" if participation.valid is False else "non"),
2022-04-22 16:02:27 +00:00
'Nom': registration.user.last_name,
'Prénom': registration.user.first_name,
'Email': registration.user.email,
'Type': registration.type.capitalize(),
'Genre': registration.get_gender_display() if registration.is_student else '',
'Date de naissance': registration.birth_date if registration.is_student else '',
'Adresse': registration.address,
'Code postal': registration.zip_code,
'Ville': registration.city,
'Pays': registration.country,
'Téléphone': registration.phone_number,
'Classe': registration.get_student_class_display() if registration.is_student
else registration.last_degree,
'Établissement': registration.school if registration.is_student
else registration.professional_activity,
'Nom responsable légal⋅e': registration.responsible_name if registration.is_student else '',
'Téléphone responsable légal⋅e': registration.responsible_phone if registration.is_student else '',
'Email responsable légal⋅e': registration.responsible_email if registration.is_student else '',
'Problèmes de santé': registration.health_issues,
'Contraintes de logement': registration.housing_constraints,
2022-04-22 16:02:27 +00:00
})
return resp
class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView):
"""
Publish notes of a tournament for a given round.
"""
model = Tournament
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
tournament = self.get_object()
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
if int(kwargs["round"]) not in (1, 2):
raise Http404
tournament = Tournament.objects.get(pk=kwargs["pk"])
tournament.pools.filter(round=kwargs["round"]).update(results_available='hide' not in request.GET)
if 'hide' not in request.GET:
messages.success(request, _("Notes published!"))
else:
messages.success(request, _("Notes hidden!"))
return super().get(request, *args, **kwargs)
def get_redirect_url(self, *args, **kwargs):
return reverse_lazy("participation:tournament_detail", args=(kwargs['pk'],))
class TournamentHarmonizeView(VolunteerMixin, DetailView):
"""
Harmonize the notes of a tournament.
"""
model = Tournament
template_name = "participation/tournament_harmonize.html"
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
tournament = self.get_object()
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1):
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tournament = self.get_object()
context['round'] = self.kwargs['round']
context['pools'] = tournament.pools.filter(round=context["round"]).all()
context['title'] = _("Harmonize notes of {tournament} - Day {round}") \
.format(tournament=tournament, round=context["round"])
notes = dict()
for participation in self.object.participations.filter(valid=True).all():
note = sum(pool.average(participation) for pool in context['pools'])
tweak = sum(tweak.diff for tweak in participation.tweaks.filter(pool__in=context['pools']).all())
notes[participation] = {'note': note, 'tweak': tweak}
context["notes"] = sorted(notes.items(), key=lambda x: x[1]['note'], reverse=True)
return context
class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
model = Tournament
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
tournament = self.get_object()
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \
or self.kwargs['action'] not in ('add', 'remove') \
or self.kwargs['trigram'] not in [p.team.trigram
for p in tournament.participations.filter(valid=True).all()]:
raise Http404
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
tournament = self.get_object()
participation = tournament.participations.filter(valid=True).get(team__trigram=kwargs['trigram'])
pool = tournament.pools.get(round=kwargs['round'], participations=participation)
tweak_qs = Tweak.objects.filter(participation=participation, pool=pool)
old_diff = tweak_qs.first().diff if tweak_qs.exists() else 0
new_diff = old_diff + (1 if kwargs['action'] == 'add' else -1)
if new_diff == 0:
tweak_qs.delete()
else:
tweak_qs.update_or_create(defaults={'diff': new_diff},
create_defaults={'diff': new_diff, 'participation': participation, 'pool': pool})
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet("Classement final")
column = 3 if kwargs['round'] == 1 else 5 if kwargs['round'] == 2 else 8
row = worksheet.find(f"{participation.team.name} ({participation.team.trigram})", in_column=1).row
worksheet.update_cell(row, column, new_diff)
return redirect(reverse_lazy("participation:tournament_harmonize", args=(tournament.pk, kwargs['round'],)))
class SelectTeamFinalView(VolunteerMixin, DetailView):
model = Tournament
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
tournament = self.get_object()
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
participation_qs = tournament.participations.filter(pk=self.kwargs["participation_id"])
if not participation_qs.exists():
raise Http404
self.participation = participation_qs.get()
return super().dispatch(request, *args, **kwargs)
@transaction.atomic
def get(self, request, *args, **kwargs):
tournament = self.get_object()
self.participation.final = True
self.participation.save()
for regional_sol in self.participation.solutions.filter(final_solution=False).all():
final_sol, _created = Solution.objects.get_or_create(participation=self.participation, final_solution=True,
problem=regional_sol.problem)
final_sol: Solution
with open(regional_sol.file.path, 'rb') as f:
final_sol.file.save(regional_sol.file.name, f)
for registration in self.participation.team.participants.all():
registration.send_email_final_selection()
return redirect(reverse_lazy("participation:tournament_detail", args=(tournament.pk,)))
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
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
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
for sol in sol_qs.all():
2021-01-14 16:26:08 +00:00
sol.file.delete()
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)
context["passages"] = PassageTable(self.object.passages.order_by('position').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():
# For a 5-teams pool, notes are separated in 2 different pool objects, so we fetch them all
all_pools = self.object.tournament.pools.filter(round=self.object.round,
letter=self.object.letter).all()
note = sum(pool.average(participation) for pool in all_pools)
2021-04-10 07:59:04 +00:00
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():
2021-01-17 11:40:23 +00:00
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def form_valid(self, form):
ret = super().form_valid(form)
# Update Google Sheets juries lines
if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
self.object.update_juries_lines_spreadsheet()
return ret
2021-01-14 13:44:12 +00:00
class SolutionsDownloadView(VolunteerMixin, View):
"""
Download all solutions or written reviews as a ZIP archive.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
reg = request.user.registration
if reg.is_admin:
return super().dispatch(request, *args, **kwargs)
if 'team_id' in kwargs:
team = Team.objects.get(pk=kwargs["team_id"])
tournament = team.participation.tournament
if reg.participates and reg.team == team \
or reg.is_volunteer and (reg in tournament.organizers.all() or team.participation.final
and reg in Tournament.final_tournament().organizers):
return super().dispatch(request, *args, **kwargs)
elif 'tournament_id' in kwargs:
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
if reg.is_volunteer \
and (tournament in reg.organized_tournaments.all()
or reg.pools_presided.filter(tournament=tournament).exists()):
return super().dispatch(request, *args, **kwargs)
else:
pool = Pool.objects.get(pk=kwargs["pool_id"])
tournament = pool.tournament
if reg.is_volunteer \
and (reg in tournament.organizers.all()
or reg in pool.juries.all()
or reg.pools_presided.filter(tournament=tournament).exists()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get(self, request, *args, **kwargs):
is_solution = 'solutions' in request.path
if 'team_id' in kwargs:
team = Team.objects.get(pk=kwargs["team_id"])
solutions = Solution.objects.filter(participation=team.participation).all()
written_reviews = WrittenReview.objects.filter(participation=team.participation).all()
filename = _("Solutions of team {trigram}.zip") if is_solution \
else _("Written reviews of team {trigram}.zip")
filename = filename.format(trigram=team.trigram)
def prefix(s: Solution | WrittenReview) -> str:
return ""
elif 'tournament_id' in kwargs:
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
sort_by = request.GET.get('sort_by', 'team').lower()
if sort_by == 'pool':
pools = Pool.objects.filter(tournament=tournament).all()
solutions = []
for pool in pools:
for sol in pool.solutions:
sol.pool = pool
solutions.append(sol)
written_reviews = WrittenReview.objects.filter(passage__pool__tournament=tournament).all()
filename = _("Solutions of {tournament}.zip") if is_solution \
else _("Written reviews of {tournament}.zip")
filename = filename.format(tournament=tournament.name)
def prefix(s: Solution | WrittenReview) -> str:
pool = s.pool if is_solution else s.passage.pool
p = f"Poule {pool.short_name}/"
if not is_solution:
p += f"Passage {s.passage.position}/"
return p
else:
if not tournament.final:
solutions = Solution.objects.filter(participation__tournament=tournament).all()
else:
solutions = Solution.objects.filter(final_solution=True).all()
written_reviews = WrittenReview.objects.filter(passage__pool__tournament=tournament).all()
filename = _("Solutions of {tournament}.zip") if is_solution \
else _("Written reviews of {tournament}.zip")
filename = filename.format(tournament=tournament.name)
def prefix(s: Solution | WrittenReview) -> str:
return f"{s.participation.team.trigram}/" if sort_by == "team" else f"Problème {s.problem}/"
else:
pool = Pool.objects.get(pk=kwargs["pool_id"])
solutions = pool.solutions
written_reviews = WrittenReview.objects.filter(passage__pool=pool).all()
filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \
if is_solution else _("Written reviews for pool {pool} of tournament {tournament}.zip")
filename = filename.format(pool=pool.short_name,
tournament=pool.tournament.name)
def prefix(s: Solution | WrittenReview) -> str:
return ""
output = BytesIO()
zf = ZipFile(output, "w")
for s in (solutions if is_solution else written_reviews):
if s.file.storage.exists(s.file.path):
zf.write("media/" + s.file.name, prefix(s) + f"{s}.pdf")
zf.close()
response = HttpResponse(content_type="application/zip")
response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
.format(filename=filename)
response.write(output.getvalue())
return response
class PoolJuryView(VolunteerMixin, FormView, DetailView):
"""
This view lets organizers set jurys for a pool, without multiplying clicks.
"""
model = Pool
form_class = AddJuryForm
template_name = 'participation/pool_jury.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
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 == self.object.jury_president)):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Jury of pool {pool} for {tournament} with teams {teams}") \
.format(pool=f"{self.object.short_name}",
tournament=self.object.tournament.name,
teams=", ".join(participation.team.trigram for participation in self.object.participations.all()))
return context
@transaction.atomic
def form_valid(self, form):
self.object = self.get_object()
user = form.instance
if user.id:
# The user already exists, so we don't recreate it
user.refresh_from_db()
reg = user.registration
if reg in self.object.juries.all():
messages.warning(self.request, _("The jury {name} is already in the pool!")
.format(name=f"{user.first_name} {user.last_name}"))
return self.form_invalid(form)
else:
# Save the user object first
form.save()
# Create associated registration object to the new user
reg = VolunteerRegistration.objects.create(
user=user,
professional_activity="Juré⋅e du tournoi " + self.object.tournament.name,
)
reg.send_email_validation_link()
# Generate new password for the user
password = get_random_string(16)
user.set_password(password)
user.save()
# Send welcome mail
subject = f"[{settings.APP_NAME}] " + str(_("New 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)
# Add the user in the jury
self.object.juries.add(reg)
self.object.save()
# Update Google Sheets juries lines
if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
self.object.update_juries_lines_spreadsheet()
# Add notification
messages.success(self.request, _("The jury {name} has been successfully added!")
.format(name=f"{user.first_name} {user.last_name}"))
return super().form_valid(form)
def form_invalid(self, form):
# This is useful since we have a FormView + a DetailView
self.object = self.get_object()
return super().form_invalid(form)
def get_success_url(self):
return reverse_lazy('participation:pool_jury', args=(self.kwargs['pk'],))
class PoolRemoveJuryView(VolunteerMixin, DetailView):
model = Pool
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
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 == self.object.jury_president)):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get(self, request, *args, **kwargs):
pool = self.get_object()
if not pool.juries.filter(pk=kwargs['jury_id']).exists():
raise Http404
jury = pool.juries.get(pk=kwargs['jury_id'])
pool.juries.remove(jury)
pool.save()
Note.objects.filter(jury=jury, passage__pool=pool).delete()
messages.success(request, _("The jury {name} has been successfully removed!")
.format(name=f"{jury.user.first_name} {jury.user.last_name}"))
return redirect(reverse_lazy('participation:pool_jury', args=(pool.pk,)))
class PoolPresideJuryView(VolunteerMixin, DetailView):
model = Pool
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
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 == self.object.jury_president)):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get(self, request, *args, **kwargs):
pool = self.get_object()
if not pool.juries.filter(pk=kwargs['jury_id']).exists():
raise Http404
jury = pool.juries.get(pk=kwargs['jury_id'])
pool.jury_president = jury
pool.save()
messages.success(request, _("The jury {name} has been successfully promoted president!")
.format(name=f"{jury.user.first_name} {jury.user.last_name}"))
return redirect(reverse_lazy('participation:pool_jury', args=(pool.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()
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 == self.object.jury_president)):
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']
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))
if form.errors:
return self.form_invalid(form)
for vr, notes in parsed_notes.items():
notes_count = 6 + (2 if pool.participations.count() >= 4 and settings.HAS_OBSERVER else 0)
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]
passage_notes = notes[notes_count * i:notes_count * (i + 1)]
note.set_all(*list(map(int, passage_notes)))
2022-05-15 10:23:17 +00:00
note.save()
if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
pool.update_spreadsheet()
2022-05-15 10:23:17 +00:00
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'],))
class PoolNotesTemplateView(VolunteerMixin, DetailView):
"""
Generate an ODS sheet to fill the notes of the pool.
"""
model = Pool
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
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 == self.object.jury_president)):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def render_to_response(self, context, **response_kwargs): # noqa: C901
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
pool_size = self.object.passages.count()
has_observer = self.object.participations.count() >= 4 and settings.HAS_OBSERVER
passage_width = 6 + (2 if has_observer else 0)
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)
jury_id_style = Style(name="co_jury_id", family="table-column")
jury_id_style.addElement(TableColumnProperties(columnwidth="1cm", breakbefore="auto"))
doc.automaticstyles.addElement(jury_id_style)
col_style = Style(name="co2", family="table-column")
col_style.addElement(TableColumnProperties(columnwidth="2.6cm", breakbefore="auto"))
doc.automaticstyles.addElement(col_style)
table = Table(name=f"Poule {self.object.short_name}")
doc.spreadsheet.addElement(table)
table.addElement(TableColumn(stylename=first_col_style))
table.addElement(TableColumn(stylename=jury_id_style))
for i in range(line_length):
table.addElement(TableColumn(stylename=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=_("Problem")))
problems_tc.setAttribute('numbercolumnsspanned', "2")
header_pb.addElement(problems_tc)
header_pb.addElement(CoveredTableCell())
for passage in self.object.passages.all():
tc = TableCell(valuetype="string", stylename=title_style_topleftright)
tc.addElement(P(text=_("Problem #{problem}").format(problem=passage.solution_number)))
tc.setAttribute('numbercolumnsspanned', str(passage_width))
header_pb.addElement(tc)
header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=passage_width - 1))
# 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=_("Role")))
role_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(role_tc)
header_role.addElement(CoveredTableCell())
for i in range(pool_size):
reporter_tc = TableCell(valuetype="string", stylename=title_style_left)
reporter_tc.addElement(P(text=_("Reporter")))
reporter_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(reporter_tc)
header_role.addElement(CoveredTableCell())
opponent_tc = TableCell(valuetype="string", stylename=title_style)
opponent_tc.addElement(P(text=_("Opponent")))
opponent_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(opponent_tc)
header_role.addElement(CoveredTableCell())
reviewer_tc = TableCell(valuetype="string",
stylename=title_style if has_observer else title_style_right)
reviewer_tc.addElement(P(text=_("Reviewer")))
reviewer_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(reviewer_tc)
header_role.addElement(CoveredTableCell())
if has_observer:
observer_tc = TableCell(valuetype="string", stylename=title_style_right)
observer_tc.addElement(P(text=_("Observer")))
observer_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(observer_tc)
header_role.addElement(CoveredTableCell())
# Add maximum notes on the third line
header_notes = TableRow()
table.addElement(header_notes)
jury_tc = TableCell(valuetype="string", value=_("Juree"), stylename=title_style_botleft)
jury_tc.addElement(P(text=_("Juree")))
jury_tc.setAttribute('numbercolumnsspanned', "2")
header_notes.addElement(jury_tc)
header_notes.addElement(CoveredTableCell())
for i in range(pool_size):
reporter_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
reporter_w_tc.addElement(P(text=f"{_('Writing')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(reporter_w_tc)
reporter_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
reporter_o_tc.addElement(P(text=f"{_('Oral')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(reporter_o_tc)
opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
opponent_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
header_notes.addElement(opponent_w_tc)
opponent_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
opponent_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
header_notes.addElement(opponent_o_tc)
reviewer_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
reviewer_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
header_notes.addElement(reviewer_w_tc)
reviewer_o_tc = TableCell(valuetype="string",
stylename=title_style_bot if has_observer else title_style_botright)
reviewer_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
header_notes.addElement(reviewer_o_tc)
if has_observer:
observer_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
observer_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
header_notes.addElement(observer_w_tc)
observer_o_tc = TableCell(valuetype="string", stylename=title_style_botright)
observer_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
header_notes.addElement(observer_o_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)
jury_id_tc = TableCell(valuetype="float", value=jury.pk, stylename=style_leftright)
jury_id_tc.addElement(P(text=str(jury.pk)))
jury_row.addElement(jury_id_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 = 3
# 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=_("Average")))
average_tc.setAttribute('numbercolumnsspanned', "2")
average_row.addElement(average_tc)
average_row.addElement(CoveredTableCell())
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_tc.setAttribute('numbercolumnsspanned', "2")
coeff_row.addElement(coeff_tc)
coeff_row.addElement(CoveredTableCell())
for passage in self.object.passages.all():
reporter_w_tc = TableCell(valuetype="float", value=passage.coeff_reporter_writing, stylename=style_left)
reporter_w_tc.addElement(P(text=str(passage.coeff_reporter_writing)))
coeff_row.addElement(reporter_w_tc)
reporter_o_tc = TableCell(valuetype="float", value=passage.coeff_reporter_oral, stylename=style)
reporter_o_tc.addElement(P(text=str(passage.coeff_reporter_oral)))
coeff_row.addElement(reporter_o_tc)
opponent_w_tc = TableCell(valuetype="float", value=passage.coeff_opponent_writing, stylename=style)
opponent_w_tc.addElement(P(text=str(passage.coeff_opponent_writing)))
coeff_row.addElement(opponent_w_tc)
opponent_o_tc = TableCell(valuetype="float", value=passage.coeff_opponent_oral, stylename=style)
opponent_o_tc.addElement(P(text=str(passage.coeff_opponent_oral)))
coeff_row.addElement(opponent_o_tc)
reviewer_w_tc = TableCell(valuetype="float", value=passage.coeff_reviewer_writing, stylename=style)
reviewer_w_tc.addElement(P(text=str(passage.coeff_reviewer_writing)))
coeff_row.addElement(reviewer_w_tc)
reviewer_o_tc = TableCell(valuetype="float", value=passage.coeff_reviewer_oral,
stylename=style if has_observer else style_right)
reviewer_o_tc.addElement(P(text=str(passage.coeff_reviewer_oral)))
coeff_row.addElement(reviewer_o_tc)
if has_observer:
observer_w_tc = TableCell(valuetype="float", value=passage.coeff_observer_writing, stylename=style)
observer_w_tc.addElement(P(text=str(passage.coeff_observer_writing)))
coeff_row.addElement(observer_w_tc)
observer_o_tc = TableCell(valuetype="float", value=passage.coeff_observer_oral, stylename=style_right)
observer_o_tc.addElement(P(text=str(passage.coeff_observer_oral)))
coeff_row.addElement(observer_o_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=_("Subtotal")))
subtotal_tc.setAttribute('numbercolumnsspanned', "2")
subtotal_row.addElement(subtotal_tc)
subtotal_row.addElement(CoveredTableCell())
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)
reporter_tc = TableCell(valuetype="float", value=passage.average_reporter, stylename=style_botleft)
reporter_tc.addElement(P(text=str(passage.average_reporter)))
reporter_tc.setAttribute('numbercolumnsspanned', "2")
reporter_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(reporter_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)
reviewer_tc = TableCell(valuetype="float", value=passage.average_reviewer,
stylename=style_bot if has_observer else style_botright)
reviewer_tc.addElement(P(text=str(passage.average_reviewer)))
reviewer_tc.setAttribute('numbercolumnsspanned', "2")
reviewer_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(reviewer_tc)
subtotal_row.addElement(CoveredTableCell())
if has_observer:
obs_w_col = getcol(min_column + passage_width * i + 6)
obs_o_col = getcol(min_column + passage_width * i + 7)
observer_tc = TableCell(valuetype="float", value=passage.average_observer, stylename=style_botright)
observer_tc.addElement(P(text=str(passage.average_observer)))
observer_tc.setAttribute('numbercolumnsspanned', "2")
observer_tc.setAttribute("formula", f"of:=[.{obs_w_col}{max_row + 1}] * [.{obs_w_col}{max_row + 2}]"
f" + [.{obs_o_col}{max_row + 1}] * [.{obs_o_col}{max_row + 2}]")
subtotal_row.addElement(observer_tc)
subtotal_row.addElement(CoveredTableCell())
table.addElement(TableRow())
if self.object.participations.count() == 5:
# 5-teams pools are separated in two different objects.
# So, displaying the ranking may don't make any sens. We don't display it for this reason.
scores_row = TableRow()
table.addElement(scores_row)
score_tc = TableCell(valuetype="string")
score_tc.addElement(P(text="Le classement d'une poule à 5 n'est pas disponible sur le tableur, "
"puisque les notes de l'autre salle sont manquantes.\n"
"Merci de vous fier au site, ou bien au Google Sheets."))
scores_row.addElement(score_tc)
else:
# 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=_("Team")))
team_tc.setAttribute('numbercolumnsspanned', "2")
scores_header.addElement(team_tc)
problem_tc = TableCell(valuetype="string", stylename=title_style_topbot)
problem_tc.addElement(P(text=_("Problem")))
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=_("Rank")))
scores_header.addElement(rank_tc)
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.reporter.team.name} ({passage.reporter.team.trigram})"))
team_tc.setAttribute('numbercolumnsspanned', "2")
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=_("Problem #{problem}").format(problem=passage.solution_number)))
problem_tc.setAttribute("formula", f"of:=[.B{3 + passage_width * (passage.position - 1)}]")
team_row.addElement(problem_tc)
reporter_pos = passage.position - 1
opponent_pos = self.object.passages.get(opponent=passage.reporter).position - 1
reviewer_pos = self.object.passages.get(reviewer=passage.reporter).position - 1
observer_pos = self.object.passages.get(observer=passage.reporter).position - 1 \
if has_observer else None
score_tc = TableCell(valuetype="float", value=self.object.average(passage.reporter),
stylename=style_bot if passage.position == pool_size else style)
score_tc.addElement(P(text=self.object.average(passage.reporter)))
formula = "of:="
formula += getcol(min_column + reporter_pos * passage_width) + str(max_row + 3) # Reporter
formula += " + " + getcol(min_column + opponent_pos * passage_width + 2) + str(max_row + 3) # Opponent
formula += " + " + getcol(min_column + reviewer_pos * passage_width + 4) + str(max_row + 3) # Reviewer
if has_observer:
# Observer
formula += " + " + getcol(min_column + observer_pos * 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.reporter) + 1,
stylename=style_botright if passage.position == pool_size else style_right)
rank_tc.addElement(P(text=str(sorted_participations.index(passage.reporter) + 1)))
rank_tc.setAttribute("formula", f"of:=RANK([.{score_col}{max_row + 5 + passage.position}]; "
f"[.{score_col}${max_row + 6}]:"
f"[.{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"{_('Notation sheet')} - {self.object.tournament.name} "
f"- {_('Pool')} {self.object.short_name}.ods")
class NotationSheetTemplateView(VolunteerMixin, DetailView):
"""
Generate a PDF from a LaTeX template for the notation papers.
"""
model = Pool
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
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 == self.object.jury_president)):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
passages = self.object.passages.all()
context['passages'] = passages
context['esp'] = passages.count() * '&'
if self.request.user.registration in self.object.juries.all() and 'blank' not in self.request.GET:
context['jury'] = self.request.user.registration
context['tfjm_number'] = timezone.now().year - settings.FIRST_EDITION + 1
return context
def render_to_response(self, context, **response_kwargs):
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
template_name = self.get_template_names()[0]
tex = render_to_string(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=template_name.split("/")[-1][:-3] + "pdf")
class ScaleNotationSheetTemplateView(NotationSheetTemplateView):
def get_template_names(self):
return [f"participation/tex/scale_{settings.TFJM_APP.lower()}.tex"]
class FinalNotationSheetTemplateView(NotationSheetTemplateView):
template_name = "participation/tex/final_sheet.tex"
class NotationSheetsArchiveView(VolunteerMixin, DetailView):
@property
def model(self):
return Pool if 'pool_id' in self.kwargs else Tournament
@property
def pk_url_kwarg(self):
return 'pool_id' if 'pool_id' in self.kwargs else 'tournament_id'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
reg = request.user.registration
if 'pool_id' in kwargs:
pool = self.get_object()
tournament = pool.tournament
if reg.is_admin or reg.is_volunteer \
and (tournament in reg.organized_tournaments.all() or reg in pool.juries.all()):
return super().dispatch(request, *args, **kwargs)
else:
tournament = self.get_object()
if reg.is_admin or reg.is_volunteer and tournament in reg.organized_tournaments.all():
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get(self, request, *args, **kwargs):
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
if 'pool_id' in kwargs:
pool = self.get_object()
tournament = pool.tournament
pools = [pool]
filename = _("Notation sheets of pool {pool} of {tournament}.zip") \
.format(pool=pool.short_name, tournament=tournament.name)
else:
tournament = self.get_object()
pools = tournament.pools.all()
filename = _("Notation sheets of {tournament}.zip").format(tournament=tournament.name)
output = BytesIO()
with ZipFile(output, "w") as zf:
for pool in pools:
prefix = f"{pool.short_name}/" if len(pools) > 1 else ""
for template_name in [f"scale_{settings.TFJM_APP.lower()}", "final_sheet"]:
juries = list(pool.juries.all()) + [None]
for jury in juries:
if jury is not None and template_name.startswith("scale"):
continue
context = {'jury': jury, 'pool': pool,
'tfjm_number': timezone.now().year - settings.FIRST_EDITION + 1}
passages = pool.passages.all()
context['passages'] = passages
context['esp'] = passages.count() * '&'
tex = render_to_string(f"participation/tex/{template_name}.tex",
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()
sheet_name = f"Barème pour la poule {pool.short_name}" if template_name.startswith("scale") \
else (f"Feuille de notation pour la poule {pool.short_name}"
f" - {str(jury) if jury else 'Vierge'}")
zf.write(os.path.join(temp_dir, "texput.pdf"),
f"{prefix}{sheet_name}.pdf")
response = HttpResponse(content_type="application/zip")
response["Content-Disposition"] = f"attachment; filename=\"{filename}\""
response.write(output.getvalue())
return response
@method_decorator(csrf_exempt, name='dispatch')
class GSheetNotificationsView(View):
async def post(self, request, *args, **kwargs):
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
return HttpResponse(status=404)
tournament = await Tournament.objects.prefetch_related('participation_set', 'pools').aget(pk=kwargs['pk'])
now = localtime(timezone.now())
expected_channel_id = sha1(f"{tournament.name}-{now.date()}-{request.site.domain}".encode()) \
.hexdigest()
if request.headers['X-Goog-Channel-ID'] != expected_channel_id:
raise ValueError(f"Invalid channel ID: {request.headers['X-Goog-Channel-ID']}")
if request.headers['X-Goog-Resource-State'] != 'update' \
or 'content' not in request.headers['X-Goog-Changed'].split(','):
return HttpResponse(status=204)
# Run the parsing in dedicated executors since it takes time
executor = ThreadPoolExecutor()
async for pool in tournament.pools.prefetch_related('participations', 'passages__notes', 'juries').all():
asyncio.get_event_loop().run_in_executor(executor, pool.parse_spreadsheet)
asyncio.get_event_loop().run_in_executor(executor, tournament.parse_tweaks_spreadsheets)
return HttpResponse(status=204)
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()
reg = request.user.registration
passage = self.get_object()
if reg.is_admin or reg.is_volunteer \
and (self.get_object().pool.tournament in reg.organized_tournaments.all()
or reg in passage.pool.juries.all()
or reg.pools_presided.filter(tournament=passage.pool.tournament).exists()) \
or reg.participates and reg.team \
and reg.team.participation in [passage.reporter, passage.opponent, passage.reviewer, passage.observer]:
2021-01-17 11:40:23 +00:00
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)
reg = self.request.user.registration
passage = self.object
if reg in passage.pool.juries.all():
context["my_note"] = Note.objects.get_or_create(passage=passage, jury=self.request.user.registration)[0]
if reg.is_volunteer:
notes = passage.notes.all()
if not reg.is_admin \
and (reg != passage.pool.jury_president
or reg not in passage.pool.tournament.organizers.all()):
notes = [note for note in notes if note.has_any_note() or note.jury == reg]
context["notes"] = NoteTable(notes)
if 'notes' in context and not self.request.user.registration.is_admin:
context['notes']._sequence.remove('update')
context['notes'].columns['reporter_writing'].column.verbose_name += f" ({passage.reporter.team.trigram})"
context['notes'].columns['reporter_oral'].column.verbose_name += f" ({passage.reporter.team.trigram})"
context['notes'].columns['opponent_writing'].column.verbose_name += f" ({passage.opponent.team.trigram})"
context['notes'].columns['opponent_oral'].column.verbose_name += f" ({passage.opponent.team.trigram})"
context['notes'].columns['reviewer_writing'].column.verbose_name += f" ({passage.reviewer.team.trigram})"
context['notes'].columns['reviewer_oral'].column.verbose_name += f" ({passage.reviewer.team.trigram})"
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 WrittenReviewUploadView(LoginRequiredMixin, FormView):
template_name = "participation/upload_written_review.html"
form_class = WrittenReviewForm
2021-01-14 16:26:08 +00:00
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
if self.participation \
and self.participation not in [self.passage.opponent, self.passage.reviewer, self.passage.observer]:
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
form_syn.type = 1 if self.participation == self.passage.opponent \
else 2 if self.participation == self.passage.reviewer else 3
syn_qs = WrittenReview.objects.filter(participation=self.participation,
passage=self.passage,
type=form_syn.type).all()
deadline = self.passage.pool.tournament.reviews_first_phase_limit if self.passage.pool.round == 1 \
else self.passage.pool.tournament.reviews_second_phase_limit
if syn_qs.exists() and timezone.now() > deadline:
form.add_error(None, _("You can't upload a written review after the deadline."))
return self.form_invalid(form)
2021-01-14 16:26:08 +00:00
# Drop previous solution if existing
for syn in syn_qs.all():
2021-01-14 16:26:08 +00:00
syn.file.delete()
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()
reg = request.user.registration
note = self.get_object()
if reg.is_admin or reg.is_volunteer and (note.jury == reg or note.passage.pool.jury_president == reg):
2021-01-17 11:40:23 +00:00
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields['reporter_writing'].label += f" ({self.object.passage.reporter.team.trigram})"
form.fields['reporter_oral'].label += f" ({self.object.passage.reporter.team.trigram})"
form.fields['opponent_writing'].label += f" ({self.object.passage.opponent.team.trigram})"
form.fields['opponent_oral'].label += f" ({self.object.passage.opponent.team.trigram})"
form.fields['reviewer_writing'].label += f" ({self.object.passage.reviewer.team.trigram})"
form.fields['reviewer_oral'].label += f" ({self.object.passage.reviewer.team.trigram})"
form.fields['observer_writing'].label += f" ({self.object.passage.observer.team.trigram})"
form.fields['observer_oral'].label += f" ({self.object.passage.observer.team.trigram})"
return form
def form_valid(self, form):
ret = super().form_valid(form)
self.object.update_spreadsheet()
return ret