# 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 Http404, HttpResponse from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils import timezone 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, StudentRegistration from tfjm.lists import get_sympa_client from tfjm.matrix import Matrix from tfjm.views import AdminMixin, VolunteerMixin from .forms import JoinTeamForm, NoteForm, ParticipationForm, PassageForm, PoolForm, PoolTeamsForm, \ RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, ValidateParticipationForm from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable 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 volunteer 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"] \ or user.registration.is_volunteer \ and self.object.participation.tournament in user.registration.interesting_tournaments: 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.coaches.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("[TFJM²] 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("[TFJM²] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html) if self.object.participation.tournament.price == 0: for registration in self.object.participants.all(): registration.payment.type = "free" registration.payment.valid = True registration.payment.save() else: for coach in self.object.coaches.all(): coach.payment.type = "free" coach.payment.valid = True coach.payment.save() 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("[TFJM²] É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"] \ or user.registration.is_volunteer \ and self.object.participation.tournament in user.registration.interesting_tournaments: 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"] \ or user.registration.is_volunteer \ and self.object.participation.tournament in user.registration.interesting_tournaments: 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 participant in team.participants.all(): magic = Magic(mime=True) if participant.photo_authorization: mime_type = magic.from_file("media/" + participant.photo_authorization.name) ext = mime_type.split("/")[1].replace("jpeg", "jpg") zf.write("media/" + participant.photo_authorization.name, _("Photo authorization of {participant}.{ext}").format(participant=str(participant), ext=ext)) if isinstance(participant, StudentRegistration) and participant.parental_authorization: mime_type = magic.from_file("media/" + participant.parental_authorization.name) ext = mime_type.split("/")[1].replace("jpeg", "jpg") zf.write("media/" + participant.parental_authorization.name, _("Parental authorization of {participant}.{ext}") .format(participant=str(participant), ext=ext)) if participant.health_sheet: mime_type = magic.from_file("media/" + participant.health_sheet.name) ext = mime_type.split("/")[1].replace("jpeg", "jpg") zf.write("media/" + participant.health_sheet.name, _("Health sheet of {participant}.{ext}").format(participant=str(participant), ext=ext)) 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.coaches.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"] \ or user.registration.is_volunteer \ and self.object.tournament in user.registration.interesting_tournaments: 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(VolunteerMixin, UpdateView): """ Update tournament detail. """ model = Tournament form_class = TournamentForm 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.request.user.registration.organized_tournaments.all()): return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) 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(): note = sum(pool.average(participation) for pool in self.object.pools.filter(participations=participation).all()) if note: notes[participation] = note 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() 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() 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 sol_qs = Solution.objects.filter(participation=self.participation, problem=form_sol.problem, final_solution=self.participation.final) tournament = Tournament.final_tournament() if self.participation.final else self.participation.final 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) # Drop previous solution if existing for sol in sol_qs.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 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() 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(): note = self.object.average(participation) if note: notes[participation] = note context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True) return context class PoolUpdateView(VolunteerMixin, UpdateView): model = Pool form_class = PoolForm def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() if request.user.registration.is_admin or request.user.registration.is_volunteer \ and (self.get_object().tournament in request.user.registration.organized_tournaments.all() or request.user.registration in self.get_object().juries.all()): return super().dispatch(request, *args, **kwargs) return self.handle_no_permission() class PoolUpdateTeamsView(VolunteerMixin, UpdateView): model = Pool form_class = PoolTeamsForm def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() if request.user.registration.is_admin or request.user.registration.is_volunteer \ and (self.get_object().tournament in request.user.registration.organized_tournaments.all() or request.user.registration in self.get_object().juries.all()): return super().dispatch(request, *args, **kwargs) return self.handle_no_permission() class PassageCreateView(VolunteerMixin, CreateView): model = Passage form_class = PassageForm def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() qs = Pool.objects.filter(pk=self.kwargs["pk"]) if not qs.exists(): raise Http404 self.pool = qs.get() if request.user.registration.is_admin or request.user.registration.is_volunteer \ and (self.pool.tournament in request.user.registration.organized_tournaments.all() or request.user.registration in self.pool.juries.all()): return super().dispatch(request, *args, **kwargs) return self.handle_no_permission() def get_form(self, form_class=None): form = super().get_form(form_class) form.instance.pool = self.pool fSynthesisorm.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 dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() if request.user.registration.is_admin or request.user.registration.is_volunteer \ and (self.get_object().pool.tournament in request.user.registration.organized_tournaments.all() or request.user.registration in self.get_object().pool.juries.all()) \ or request.user.registration.participates and request.user.registration.team \ and request.user.registration.team.participation in [self.get_object().defender, self.get_object().opponent, self.get_object().reporter]: return super().dispatch(request, *args, **kwargs) return self.handle_no_permission() 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(VolunteerMixin, UpdateView): model = Passage form_class = PassageForm 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() class SynthesisUploadView(LoginRequiredMixin, FormView): template_name = "participation/upload_synthesis.html" form_class = SynthesisForm def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated or not request.user.registration.participates: return self.handle_no_permission() 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() if self.participation not in [self.passage.defender, self.passage.opponent, self.passage.reporter]: return self.handle_no_permission() 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 syn_qs = Synthesis.objects.filter(participation=self.participation, passage=self.passage, type=form_syn.type).all() deadline = self.passage.pool.tournament.syntheses_first_phase_limit if self.passage.pool.round == 1 \ else self.passage.pool.tournament.syntheses_second_phase_limit if syn_qs.exists() and timezone.now() > deadline: form.add_error(None, _("You can't upload a synthesis after the deadline.")) return self.form_invalid(form) # Drop previous solution if existing for syn in syn_qs.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(VolunteerMixin, UpdateView): model = Note form_class = NoteForm def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() if request.user.registration.is_admin or request.user.registration.is_volunteer \ and self.get_object().jury == request.user.registration: return super().dispatch(request, *args, **kwargs) return self.handle_no_permission()