plateforme-tfjm2/apps/participation/views.py

485 lines
19 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, ParticipationForm, PoolForm, RequestValidationForm, SolutionForm, TeamForm,\
TournamentForm, ValidateParticipationForm
from .models import Participation, Team, Tournament, Solution, Pool
from .tables import TeamTable, TournamentTable, ParticipationTable, PoolTable
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())
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.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(AdminMixin, DetailView):
model = Pool
class PoolUpdateView(AdminMixin, UpdateView):
model = Pool
form_class = PoolForm