# 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