plateforme-corres2math/apps/participation/views.py

547 lines
22 KiB
Python
Raw Normal View History

2020-12-26 20:26:26 +00:00
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from io import BytesIO
from zipfile import ZipFile
2020-10-09 11:58:42 +00:00
from corres2math.lists import get_sympa_client
from corres2math.matrix import Matrix
2020-10-20 11:14:02 +00:00
from corres2math.views import AdminMixin
2020-09-22 18:41:42 +00:00
from django.contrib.auth.mixins import LoginRequiredMixin
2020-12-21 17:02:37 +00:00
from django.contrib.sites.models import Site
2020-09-24 08:21:50 +00:00
from django.core.exceptions import PermissionDenied
2020-10-15 18:39:34 +00:00
from django.core.mail import send_mail
2020-09-23 21:20:44 +00:00
from django.db import transaction
2020-10-09 11:58:42 +00:00
from django.http import HttpResponse
2020-10-29 14:09:24 +00:00
from django.shortcuts import redirect
from django.template.loader import render_to_string
2020-09-23 21:20:44 +00:00
from django.urls import reverse_lazy
2020-09-22 18:41:42 +00:00
from django.utils.translation import gettext_lazy as _
2020-10-31 21:18:04 +00:00
from django.views.generic import CreateView, DeleteView, DetailView, FormView, RedirectView, TemplateView, UpdateView
2020-10-11 16:44:42 +00:00
from django.views.generic.edit import FormMixin, ProcessFormView
2020-10-20 11:06:51 +00:00
from django_tables2 import SingleTableView
from magic import Magic
from registration.models import AdminRegistration
2020-09-21 13:41:55 +00:00
2020-10-31 17:11:37 +00:00
from .forms import JoinTeamForm, ParticipationForm, PhaseForm, QuestionForm, \
ReceiveParticipationForm, RequestValidationForm, SendParticipationForm, TeamForm, \
UploadVideoForm, ValidateParticipationForm
from .models import Participation, Phase, Question, Team, Video
2020-11-16 10:58:05 +00:00
from .tables import CalendarTable, TeamTable
2020-09-22 18:41:42 +00:00
class CreateTeamView(LoginRequiredMixin, CreateView):
2020-10-30 18:46:46 +00:00
"""
Display the page to create a team for new users.
"""
2020-09-22 18:41:42 +00:00
model = Team
form_class = TeamForm
extra_context = dict(title=_("Create team"))
template_name = "participation/create_team.html"
2020-09-27 14:35:31 +00:00
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
2020-09-23 21:20:44 +00:00
registration = user.registration
if not registration.participates:
2020-09-27 14:35:31 +00:00
raise PermissionDenied(_("You don't participate, so you can't create a team."))
2020-09-23 21:20:44 +00:00
elif registration.team:
2020-09-27 14:35:31 +00:00
raise PermissionDenied(_("You are already in a team."))
return super().dispatch(request, *args, **kwargs)
2020-09-23 21:20:44 +00:00
2020-09-27 14:35:31 +00:00
@transaction.atomic
def form_valid(self, form):
2020-10-30 18:46:46 +00:00
"""
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.
"""
2020-09-23 21:20:44 +00:00
ret = super().form_valid(form)
2020-10-30 18:46:46 +00:00
# The user joins the team
2020-09-27 14:35:31 +00:00
user = self.request.user
registration = user.registration
2020-09-23 21:20:44 +00:00
registration.team = form.instance
registration.save()
2020-10-30 18:46:46 +00:00
# Subscribe the user mail address to the team mailing list
2020-10-09 11:49:09 +00:00
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
f"{user.first_name} {user.last_name}")
2020-10-30 18:46:46 +00:00
# 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")
2020-09-23 21:20:44 +00:00
return ret
def get_success_url(self):
return reverse_lazy("participation:team_detail", args=(self.object.pk,))
2020-09-23 21:20:44 +00:00
2020-09-22 18:41:42 +00:00
class JoinTeamView(LoginRequiredMixin, FormView):
2020-10-30 18:46:46 +00:00
"""
Participants can join a team with the access code of the team.
"""
2020-09-22 18:41:42 +00:00
model = Team
form_class = JoinTeamForm
extra_context = dict(title=_("Join team"))
template_name = "participation/create_team.html"
2020-09-23 21:20:44 +00:00
2020-09-27 14:35:31 +00:00
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
2020-09-23 21:20:44 +00:00
registration = user.registration
if not registration.participates:
2020-09-27 14:35:31 +00:00
raise PermissionDenied(_("You don't participate, so you can't create a team."))
2020-09-23 21:20:44 +00:00
elif registration.team:
2020-09-27 14:35:31 +00:00
raise PermissionDenied(_("You are already in a team."))
return super().dispatch(request, *args, **kwargs)
2020-09-23 21:20:44 +00:00
2020-09-27 14:35:31 +00:00
@transaction.atomic
def form_valid(self, form):
2020-10-30 18:46:46 +00:00
"""
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
2020-09-23 21:20:44 +00:00
ret = super().form_valid(form)
2020-10-30 18:46:46 +00:00
# Join the team
2020-09-27 14:35:31 +00:00
user = self.request.user
registration = user.registration
2020-09-23 21:20:44 +00:00
registration.team = form.instance
registration.save()
2020-10-30 18:46:46 +00:00
# Subscribe to the team mailing list
2020-10-09 11:49:09 +00:00
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
f"{user.first_name} {user.last_name}")
2020-10-30 18:46:46 +00:00
# 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")
2020-09-23 21:20:44 +00:00
return ret
def get_success_url(self):
return reverse_lazy("participation:team_detail", args=(self.object.pk,))
2020-09-24 08:21:50 +00:00
2020-11-16 10:58:05 +00:00
class TeamListView(AdminMixin, SingleTableView):
"""
Display the whole list of teams
"""
model = Team
table_class = TeamTable
ordering = ('participation__problem', 'trigram',)
2020-09-24 08:21:50 +00:00
class MyTeamDetailView(LoginRequiredMixin, RedirectView):
2020-10-30 18:46:46 +00:00
"""
Redirect to the detail of the team in which the user is.
"""
2020-09-24 08:21:50 +00:00
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."))
2020-10-11 16:44:42 +00:00
class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView):
2020-10-30 18:46:46 +00:00
"""
Display the detail of a team.
"""
2020-09-24 08:21:50 +00:00
model = Team
2020-09-24 09:15:54 +00:00
2020-10-11 14:30:02 +00:00
def get(self, request, *args, **kwargs):
2020-09-27 14:35:31 +00:00
user = request.user
2020-10-11 16:44:42 +00:00
self.object = self.get_object()
2020-10-30 18:46:46 +00:00
# Ensure that the user is an admin or a member of the team
2020-11-02 17:09:24 +00:00
if user.registration.is_admin or user.registration.participates and \
user.registration.team and user.registration.team.pk == kwargs["pk"]:
2020-10-11 14:30:02 +00:00
return super().get(request, *args, **kwargs)
2020-09-27 14:35:31 +00:00
raise PermissionDenied
2020-10-11 14:30:02 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2020-10-11 16:44:42 +00:00
team = self.get_object()
2020-11-02 09:58:00 +00:00
context["title"] = _("Detail of team {trigram}").format(trigram=self.object.trigram)
2020-10-11 16:44:42 +00:00
context["request_validation_form"] = RequestValidationForm(self.request.POST or None)
context["validation_form"] = ValidateParticipationForm(self.request.POST or None)
2020-10-30 18:46:46 +00:00
# A team is complete when there are at least 3 members that have sent their photo authorization
# and confirmed their email address
2020-10-11 14:30:02 +00:00
context["can_validate"] = team.students.count() >= 3 and \
2020-10-11 14:49:31 +00:00
all(r.email_confirmed for r in team.students.all()) and \
2020-10-11 14:30:02 +00:00
all(r.photo_authorization for r in team.students.all()) and \
team.participation.problem
return context
2020-10-11 16:44:42 +00:00
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):
2020-11-02 17:25:32 +00:00
return self.handle_request_validation(form)
2020-10-11 16:44:42 +00:00
elif isinstance(form, ValidateParticipationForm):
2020-11-02 17:25:32 +00:00
return self.handle_validate_participation(form)
2020-10-11 16:44:42 +00:00
2020-11-02 17:25:32 +00:00
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():
2020-12-21 17:02:37 +00:00
mail_context = dict(user=admin.user, team=self.object, domain=Site.objects.first().domain)
2020-11-02 17:25:32 +00:00
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)
2020-11-03 13:26:55 +00:00
return super().form_valid(form)
2020-11-02 17:25:32 +00:00
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)
2020-11-14 21:18:35 +00:00
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}")
2020-11-02 17:25:32 +00:00
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)
2020-11-02 17:09:24 +00:00
return super().form_valid(form)
2020-10-11 16:44:42 +00:00
def get_success_url(self):
return self.request.path
2020-09-24 09:15:54 +00:00
class TeamUpdateView(LoginRequiredMixin, UpdateView):
2020-10-30 18:46:46 +00:00
"""
Update the detail of a team
"""
2020-09-24 09:15:54 +00:00
model = Team
form_class = TeamForm
template_name = "participation/update_team.html"
2020-09-27 14:35:31 +00:00
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
2020-11-02 17:19:53 +00:00
if user.registration.is_admin or user.registration.participates and \
user.registration.team and \
user.registration.team.pk == kwargs["pk"]:
2020-09-27 14:35:31 +00:00
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
2020-09-24 09:15:54 +00:00
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)
2020-11-02 10:18:06 +00:00
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
2020-09-24 09:15:54 +00:00
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,))
2020-09-27 12:32:05 +00:00
class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
2020-10-30 18:46:46 +00:00
"""
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")
2020-10-11 14:30:02 +00:00
response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
.format(filename=_("Photo authorizations of team {trigram}.zip").format(trigram=team.trigram))
response.write(output.getvalue())
return response
2020-10-20 14:08:42 +00:00
class TeamLeaveView(LoginRequiredMixin, TemplateView):
2020-10-30 18:46:46 +00:00
"""
A team member leaves a team
"""
2020-10-20 14:08:42 +00:00
template_name = "participation/team_leave.html"
2020-11-02 09:58:00 +00:00
extra_context = dict(title=_("Leave team"))
2020-10-20 14:08:42 +00:00
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
2020-11-03 13:43:51 +00:00
if not request.user.registration.participates or not request.user.registration.team:
2020-10-20 14:08:42 +00:00
raise PermissionDenied(_("You are not in a team."))
if request.user.registration.team.participation.valid:
2020-10-20 14:08:42 +00:00
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):
2020-10-30 18:46:46 +00:00
"""
When the team is left, the user is unsubscribed from the team mailing list
and kicked from the team room.
"""
2020-10-20 14:08:42 +00:00
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")
2020-10-20 14:08:42 +00:00
if team.students.count() + team.coachs.count() == 0:
team.delete()
return redirect(reverse_lazy("index"))
2020-09-27 12:32:05 +00:00
class MyParticipationDetailView(LoginRequiredMixin, RedirectView):
2020-10-30 18:46:46 +00:00
"""
Redirects to the detail view of the participation of the team.
"""
2020-09-27 12:32:05 +00:00
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):
2020-10-30 18:46:46 +00:00
"""
Display detail about the participation of a team, and manage the video submission.
"""
2020-09-27 12:32:05 +00:00
model = Participation
2020-09-27 14:35:31 +00:00
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."))
2020-10-11 14:30:02 +00:00
if user.registration.is_admin or user.registration.participates \
2020-11-02 17:09:24 +00:00
and user.registration.team.participation \
and user.registration.team.participation.pk == kwargs["pk"]:
2020-09-27 14:35:31 +00:00
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
2020-10-20 12:51:09 +00:00
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
2020-09-27 12:32:05 +00:00
2020-10-31 11:51:49 +00:00
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"],))
2020-10-31 11:51:49 +00:00
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"],))
2020-10-31 17:11:37 +00:00
class CreateQuestionView(LoginRequiredMixin, CreateView):
"""
Ask a question to another team.
"""
participation: Participation
model = Question
form_class = QuestionForm
2020-11-02 09:58:00 +00:00
extra_context = dict(title=_("Create question"))
2020-10-31 17:11:37 +00:00
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 \
2020-11-03 16:58:05 +00:00
self.participation.valid and \
2020-10-31 17:11:37 +00:00
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):
2020-10-31 17:57:00 +00:00
self.object = self.get_object()
2020-10-31 17:11:37 +00:00
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.registration.is_admin or \
request.user.registration.participates and \
2020-11-03 16:58:05 +00:00
self.object.participation.valid and \
2020-10-31 17:11:37 +00:00
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,))
2020-10-31 21:18:04 +00:00
class DeleteQuestionView(LoginRequiredMixin, DeleteView):
"""
Remove a question.
"""
model = Question
2020-11-02 09:58:00 +00:00
extra_context = dict(title=_("Delete question"))
2020-10-31 21:18:04 +00:00
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 \
2020-11-03 16:58:05 +00:00
self.object.participation.valid and \
2020-10-31 21:18:04 +00:00
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,))
2020-09-27 12:32:05 +00:00
class UploadVideoView(LoginRequiredMixin, UpdateView):
2020-10-30 18:46:46 +00:00
"""
Upload a solution video for a team.
"""
2020-09-27 12:32:05 +00:00
model = Video
form_class = UploadVideoForm
template_name = "participation/upload_video.html"
2020-11-02 09:58:00 +00:00
extra_context = dict(title=_("Upload video"))
2020-09-27 12:32:05 +00:00
2020-09-27 14:35:31 +00:00
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
2020-10-11 14:30:02 +00:00
if user.registration.is_admin or user.registration.participates \
2020-09-27 14:52:52 +00:00
and user.registration.team.participation.pk == self.get_object().participation.pk:
2020-09-27 14:35:31 +00:00
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
2020-09-27 12:32:05 +00:00
def get_success_url(self):
return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,))
2020-10-20 11:06:51 +00:00
class CalendarView(SingleTableView):
2020-10-30 18:46:46 +00:00
"""
Display the calendar of the action.
"""
2020-10-20 11:06:51 +00:00
table_class = CalendarTable
model = Phase
2020-11-02 09:58:00 +00:00
extra_context = dict(title=_("Calendar"))
2020-10-20 11:14:02 +00:00
class PhaseUpdateView(AdminMixin, UpdateView):
2020-10-30 18:46:46 +00:00
"""
Update a phase of the calendar, if we have sufficient rights.
"""
2020-10-20 11:14:02 +00:00
model = Phase
form_class = PhaseForm
2020-11-02 09:58:00 +00:00
extra_context = dict(title=_("Calendar update"))
2020-10-20 12:21:16 +00:00
def get_success_url(self):
return reverse_lazy("participation:calendar")