# Copyright (C) 2020 by Animath # SPDX-License-Identifier: GPL-3.0-or-later from io import BytesIO from zipfile import ZipFile from corres2math.lists import get_sympa_client from corres2math.matrix import Matrix from corres2math.views import AdminMixin 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 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, DeleteView, 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 .forms import JoinTeamForm, ParticipationForm, PhaseForm, QuestionForm, \ ReceiveParticipationForm, RequestValidationForm, SendParticipationForm, TeamForm, \ UploadVideoForm, ValidateParticipationForm from .models import Participation, Phase, Question, Team, Video from .tables import CalendarTable, TeamTable 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()}:correspondances-maths.fr", f"@{user.registration.matrix_username}:correspondances-maths.fr") return ret def get_success_url(self): return reverse_lazy("participation:team_detail", args=(self.object.pk,)) 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()}:correspondances-maths.fr", f"@{user.registration.matrix_username}:correspondances-maths.fr") 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 = ('participation__problem', '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 3 members that have sent their photo authorization # and confirmed their email address context["can_validate"] = team.students.count() >= 3 and \ all(r.email_confirmed for r in team.students.all()) and \ all(r.photo_authorization for r in team.students.all()) and \ team.participation.problem 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, " "photo 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) get_sympa_client().subscribe(self.object.email, f"probleme-{self.object.participation.problem}", False, f"Equipe {self.object.name}") 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) def get_success_url(self): return reverse_lazy("participation:team_detail", args=(self.object.pk,)) 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.students.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()}:correspondances-maths.fr", f"@{request.user.registration.matrix_username}:correspondances-maths.fr", "É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 video 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) context["current_phase"] = Phase.current_phase() return context class SetParticipationReceiveParticipationView(AdminMixin, UpdateView): """ Define the solution that a team will receive. """ model = Participation form_class = ReceiveParticipationForm template_name = "participation/receive_participation_form.html" def get_success_url(self): return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],)) class SetParticipationSendParticipationView(AdminMixin, UpdateView): """ Define the team where the solution will be sent. """ model = Participation form_class = SendParticipationForm template_name = "participation/send_participation_form.html" def get_success_url(self): return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],)) class CreateQuestionView(LoginRequiredMixin, CreateView): """ Ask a question to another team. """ participation: Participation model = Question form_class = QuestionForm extra_context = dict(title=_("Create question")) def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() self.participation = Participation.objects.get(pk=kwargs["pk"]) if request.user.registration.is_admin or \ request.user.registration.participates and \ self.participation.valid and \ request.user.registration.team.pk == self.participation.team_id: return super().dispatch(request, *args, **kwargs) raise PermissionDenied def form_valid(self, form): form.instance.participation = self.participation return super().form_valid(form) def get_success_url(self): return reverse_lazy("participation:participation_detail", args=(self.participation.pk,)) class UpdateQuestionView(LoginRequiredMixin, UpdateView): """ Edit a question. """ model = Question form_class = QuestionForm def dispatch(self, request, *args, **kwargs): self.object = self.get_object() if not request.user.is_authenticated: return self.handle_no_permission() if request.user.registration.is_admin or \ request.user.registration.participates and \ self.object.participation.valid and \ request.user.registration.team.pk == self.object.participation.team_id: return super().dispatch(request, *args, **kwargs) raise PermissionDenied def get_success_url(self): return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,)) class DeleteQuestionView(LoginRequiredMixin, DeleteView): """ Remove a question. """ model = Question extra_context = dict(title=_("Delete question")) def dispatch(self, request, *args, **kwargs): self.object = self.get_object() if not request.user.is_authenticated: return self.handle_no_permission() if request.user.registration.is_admin or \ request.user.registration.participates and \ self.object.participation.valid and \ request.user.registration.team.pk == self.object.participation.team_id: return super().dispatch(request, *args, **kwargs) raise PermissionDenied def get_success_url(self): return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,)) class UploadVideoView(LoginRequiredMixin, UpdateView): """ Upload a solution video for a team. """ model = Video form_class = UploadVideoForm template_name = "participation/upload_video.html" extra_context = dict(title=_("Upload video")) 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.participation.pk == self.get_object().participation.pk: return super().dispatch(request, *args, **kwargs) raise PermissionDenied def get_success_url(self): return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,)) class CalendarView(SingleTableView): """ Display the calendar of the action. """ table_class = CalendarTable model = Phase extra_context = dict(title=_("Calendar")) class PhaseUpdateView(AdminMixin, UpdateView): """ Update a phase of the calendar, if we have sufficient rights. """ model = Phase form_class = PhaseForm extra_context = dict(title=_("Calendar update")) def get_success_url(self): return reverse_lazy("participation:calendar")