585 lines
23 KiB
Python
585 lines
23 KiB
Python
# Copyright (C) 2020 by Animath
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from io import BytesIO
|
|
from zipfile import ZipFile
|
|
|
|
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.http import HttpResponse, Http404
|
|
from django.shortcuts import redirect
|
|
from django.template.loader import render_to_string
|
|
from django.urls import reverse_lazy
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView
|
|
from django.views.generic.edit import FormMixin, ProcessFormView
|
|
from django_tables2 import SingleTableView
|
|
from magic import Magic
|
|
from registration.models import AdminRegistration
|
|
from tfjm.lists import get_sympa_client
|
|
from tfjm.matrix import Matrix
|
|
from tfjm.views import AdminMixin
|
|
|
|
from .forms import JoinTeamForm, NoteForm, ParticipationForm, PassageForm, PoolForm, PoolTeamsForm, \
|
|
RequestValidationForm, TeamForm, TournamentForm, ValidateParticipationForm, SolutionForm, SynthesisForm
|
|
from .models import Note, Participation, Passage, Pool, Team, Tournament, Solution, Synthesis
|
|
from .tables import NoteTable, PassageTable, PoolTable, TeamTable, TournamentTable, ParticipationTable
|
|
|
|
|
|
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, and finally
|
|
a Matrix room is created and the user is invited in this room.
|
|
"""
|
|
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}")
|
|
|
|
# Invite the user in the team Matrix room
|
|
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
|
|
f"@{user.registration.matrix_username}:tfjm.org")
|
|
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/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 user joins a team, the user is automatically subscribed to
|
|
the team mailing list,the user is invited in the team Matrix room.
|
|
"""
|
|
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}")
|
|
|
|
# Invite the user in the team Matrix room
|
|
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
|
|
f"@{user.registration.matrix_username}:tfjm.org")
|
|
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
|
|
ordering = ('trigram',)
|
|
|
|
|
|
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()
|
|
# Ensure that the user is an admin or a member of the team
|
|
if user.registration.is_admin or user.registration.participates and \
|
|
user.registration.team and user.registration.team.pk == kwargs["pk"]:
|
|
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)
|
|
# 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.students.count() >= 4 and team.coachs.exists() and \
|
|
all(r.email_confirmed for r in team.students.all()) and \
|
|
all(r.photo_authorization for r in team.participants.all()) and \
|
|
all(r.health_sheet for r in team.participants.all()) and \
|
|
all(r.parental_authorization for r in team.students.all() if r.under_18)
|
|
|
|
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 or the chosen problem is not set."))
|
|
return self.form_invalid(form)
|
|
|
|
self.object.participation.valid = False
|
|
self.object.participation.save()
|
|
|
|
for admin in AdminRegistration.objects.all():
|
|
mail_context = dict(user=admin.user, 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)
|
|
admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html)
|
|
return super().form_valid(form)
|
|
|
|
def handle_validate_participation(self, form):
|
|
"""
|
|
An admin validates the team (or not)
|
|
"""
|
|
if not self.request.user.registration.is_admin:
|
|
form.add_error(None, _("You are not an administrator."))
|
|
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()
|
|
mail_context = dict(team=self.object, 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)
|
|
send_mail("[Corres2math] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html)
|
|
|
|
get_sympa_client().subscribe(self.object.email, "equipes", False, f"Equipe {self.object.name}")
|
|
get_sympa_client().unsubscribe(self.object.email, "equipes-non-valides", False)
|
|
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)
|
|
send_mail("[Corres2math] Équipe non validée", mail_plain, None, [self.object.email],
|
|
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 \
|
|
user.registration.team and \
|
|
user.registration.team.pk == kwargs["pk"]:
|
|
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)
|
|
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 participation_form.is_valid():
|
|
return self.form_invalid(form)
|
|
|
|
participation_form.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
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()
|
|
if user.registration.is_admin or user.registration.participates and user.registration.team.pk == kwargs["pk"]:
|
|
return super().dispatch(request, *args, **kwargs)
|
|
raise PermissionDenied
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
team = self.get_object()
|
|
output = BytesIO()
|
|
zf = ZipFile(output, "w")
|
|
for student in team.participants.all():
|
|
magic = Magic(mime=True)
|
|
mime_type = magic.from_file("media/" + student.photo_authorization.name)
|
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
|
zf.write("media/" + student.photo_authorization.name,
|
|
_("Photo authorization of {student}.{ext}").format(student=str(student), ext=ext))
|
|
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)
|
|
Matrix.kick(f"#equipe-{team.trigram.lower()}:tfjm.org",
|
|
f"@{request.user.registration.matrix_username}:tfjm.org",
|
|
"Équipe quittée")
|
|
if team.students.count() + team.coachs.count() == 0:
|
|
team.delete()
|
|
return redirect(reverse_lazy("index"))
|
|
|
|
|
|
class MyParticipationDetailView(LoginRequiredMixin, RedirectView):
|
|
"""
|
|
Redirects to the detail view of the participation of the team.
|
|
"""
|
|
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):
|
|
"""
|
|
Display detail about the participation of a team, and manage the solution submission.
|
|
"""
|
|
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 \
|
|
and user.registration.team.participation.pk == kwargs["pk"]:
|
|
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
|
|
|
|
|
|
class TournamentListView(SingleTableView):
|
|
"""
|
|
Display the list of all tournaments.
|
|
"""
|
|
model = Tournament
|
|
table_class = TournamentTable
|
|
|
|
|
|
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,))
|
|
|
|
|
|
class TournamentUpdateView(AdminMixin, UpdateView):
|
|
"""
|
|
Update tournament detail.
|
|
"""
|
|
model = Tournament
|
|
form_class = TournamentForm
|
|
|
|
|
|
class TournamentDetailView(DetailView):
|
|
"""
|
|
Display tournament detail.
|
|
"""
|
|
model = Tournament
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["teams"] = ParticipationTable(self.object.participations.all())
|
|
context["pools"] = PoolTable(self.object.pools.all())
|
|
|
|
notes = dict()
|
|
for participation in self.object.participations.all():
|
|
notes[participation] = sum(pool.average(participation)
|
|
for pool in self.object.pools.filter(participations=participation).all())
|
|
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
return context
|
|
|
|
|
|
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()
|
|
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_sol = form.instance
|
|
# Drop previous solution if existing
|
|
for sol in Solution.objects.filter(participation=self.participation,
|
|
problem=form_sol.problem,
|
|
final_solution=self.participation.final).all():
|
|
sol.file.delete()
|
|
sol.delete()
|
|
form_sol.participation = self.participation
|
|
form_sol.final = self.participation.final
|
|
form_sol.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("participation:participation_detail", args=(self.participation.pk,))
|
|
|
|
|
|
class PoolCreateView(AdminMixin, CreateView):
|
|
model = Pool
|
|
form_class = PoolForm
|
|
|
|
|
|
class PoolDetailView(LoginRequiredMixin, DetailView):
|
|
model = Pool
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["passages"] = PassageTable(self.object.passages.all())
|
|
|
|
notes = dict()
|
|
for participation in self.object.participations.all():
|
|
notes[participation] = self.object.average(participation)
|
|
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
return context
|
|
|
|
|
|
class PoolUpdateView(AdminMixin, UpdateView):
|
|
model = Pool
|
|
form_class = PoolForm
|
|
|
|
|
|
class PoolUpdateTeamsView(AdminMixin, UpdateView):
|
|
model = Pool
|
|
form_class = PoolTeamsForm
|
|
|
|
|
|
class PassageCreateView(AdminMixin, CreateView):
|
|
model = Passage
|
|
form_class = PassageForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
qs = Pool.objects.filter(pk=self.kwargs["pk"])
|
|
if not qs.exists():
|
|
raise Http404
|
|
self.pool = qs.get()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.instance.pool = self.pool
|
|
form.fields["defender"].queryset = self.pool.participations.all()
|
|
form.fields["opponent"].queryset = self.pool.participations.all()
|
|
form.fields["reporter"].queryset = self.pool.participations.all()
|
|
return form
|
|
|
|
|
|
class PassageDetailView(LoginRequiredMixin, DetailView):
|
|
model = Passage
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
if self.request.user.registration in self.object.pool.juries.all():
|
|
context["my_note"] = Note.objects.get(passage=self.object, jury=self.request.user.registration)
|
|
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
|
|
return context
|
|
|
|
|
|
class PassageUpdateView(AdminMixin, UpdateView):
|
|
model = Passage
|
|
form_class = PassageForm
|
|
|
|
|
|
class SynthesisUploadView(LoginRequiredMixin, FormView):
|
|
template_name = "participation/upload_synthesis.html"
|
|
form_class = SynthesisForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
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()
|
|
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
|
|
# Drop previous solution if existing
|
|
for syn in Synthesis.objects.filter(participation=self.participation,
|
|
passage=self.passage,
|
|
type=form_syn.type).all():
|
|
syn.file.delete()
|
|
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,))
|
|
|
|
|
|
class NoteUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = Note
|
|
form_class = NoteForm
|