# Copyright (C) 2020 by Animath # SPDX-License-Identifier: GPL-3.0-or-later import os import subprocess from tempfile import mkdtemp from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core.exceptions import PermissionDenied, ValidationError from django.db import transaction from django.db.models import Q from django.http import FileResponse, Http404 from django.shortcuts import redirect, resolve_url from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.http import urlsafe_base64_decode from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View from django_tables2 import SingleTableView from magic import Magic from participation.models import Passage, Solution, Synthesis, Tournament from tfjm.tokens import email_validation_token from tfjm.views import AdminMixin, UserMixin, VolunteerMixin from .forms import AddOrganizerForm, AdminRegistrationForm, CoachRegistrationForm, HealthSheetForm, \ ParentalAuthorizationForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \ VolunteerRegistrationForm from .models import ParticipantRegistration, Payment, Registration, StudentRegistration from .tables import RegistrationTable class SignupView(CreateView): """ Signup, as a participant or a coach. """ model = User form_class = SignupForm template_name = "registration/sign-up.html" extra_context = dict(title=_("Sign up")) def get_context_data(self, **kwargs): context = super().get_context_data() context["student_registration_form"] = StudentRegistrationForm(self.request.POST or None) context["coach_registration_form"] = CoachRegistrationForm(self.request.POST or None) del context["student_registration_form"].fields["team"] del context["student_registration_form"].fields["email_confirmed"] del context["coach_registration_form"].fields["team"] del context["coach_registration_form"].fields["email_confirmed"] return context @transaction.atomic def form_valid(self, form): role = form.cleaned_data["role"] if role == "participant": registration_form = StudentRegistrationForm(self.request.POST) else: registration_form = CoachRegistrationForm(self.request.POST) del registration_form.fields["team"] del registration_form.fields["email_confirmed"] if not registration_form.is_valid(): return self.form_invalid(form) ret = super().form_valid(form) registration = registration_form.instance registration.user = form.instance registration.save() registration.send_email_validation_link() return ret def get_success_url(self): return reverse_lazy("registration:email_validation_sent") class AddOrganizerView(VolunteerMixin, CreateView): model = User form_class = AddOrganizerForm template_name = "registration/add_organizer.html" extra_context = dict(title=_("Add organizer")) def get_context_data(self, **kwargs): context = super().get_context_data() context["volunteer_registration_form"] = VolunteerRegistrationForm(self.request.POST or None) context["admin_registration_form"] = AdminRegistrationForm(self.request.POST or None) del context["volunteer_registration_form"].fields["email_confirmed"] del context["admin_registration_form"].fields["email_confirmed"] if not self.request.user.registration.is_admin: del context["form"].fields["type"] del context["admin_registration_form"] return context @transaction.atomic def form_valid(self, form): role = form.cleaned_data["type"] if role == "admin": registration_form = AdminRegistrationForm(self.request.POST) else: registration_form = VolunteerRegistrationForm(self.request.POST) del registration_form.fields["email_confirmed"] if not registration_form.is_valid(): return self.form_invalid(form) ret = super().form_valid(form) registration = registration_form.instance registration.user = form.instance registration.save() registration.send_email_validation_link() password = get_random_string(16) form.instance.set_password(password) form.instance.save() subject = "[TFJM²] " + str(_("New TFJM² organizer account")) site = Site.objects.first() message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user, inviter=self.request.user, password=password, domain=site.domain)) html = render_to_string('registration/mails/add_organizer.html', dict(user=registration.user, inviter=self.request.user, password=password, domain=site.domain)) registration.user.email_user(subject, message, html_message=html) if registration.is_admin: registration.user.is_superuser = True registration.user.save() return ret def get_success_url(self): return reverse_lazy("registration:email_validation_sent") class UserValidateView(TemplateView): """ A view to validate the email address. """ title = _("Email validation") template_name = 'registration/email_validation_complete.html' extra_context = dict(title=_("Validate email")) def get(self, *args, **kwargs): """ With a given token and user id (in params), validate the email address. """ assert 'uidb64' in kwargs and 'token' in kwargs self.validlink = False user = self.get_user(kwargs['uidb64']) token = kwargs['token'] # Validate the token if user is not None and email_validation_token.check_token(user, token): self.validlink = True user.registration.email_confirmed = True user.registration.save() return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400) def get_user(self, uidb64): """ Get user from the base64-encoded string. """ try: # urlsafe_base64_decode() decodes to bytestring uid = urlsafe_base64_decode(uidb64).decode() user = User.objects.get(pk=uid) except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError): user = None return user def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['user_object'] = self.get_user(self.kwargs["uidb64"]) context['login_url'] = resolve_url(settings.LOGIN_URL) if self.validlink: context['validlink'] = True else: context.update({ 'title': _('Email validation unsuccessful'), 'validlink': False, }) return context class UserValidationEmailSentView(TemplateView): """ Display the information that the validation link has been sent. """ template_name = 'registration/email_validation_email_sent.html' extra_context = dict(title=_('Email validation email sent')) class UserResendValidationEmailView(LoginRequiredMixin, DetailView): """ Rensend the email validation link. """ model = User extra_context = dict(title=_("Resend email validation link")) def get(self, request, *args, **kwargs): user = self.get_object() user.registration.send_email_validation_link() return redirect('registration:email_validation_sent') class MyAccountDetailView(LoginRequiredMixin, RedirectView): """ Redirect to our own profile detail page. """ def get_redirect_url(self, *args, **kwargs): return reverse_lazy("registration:user_detail", args=(self.request.user.pk,)) class UserDetailView(UserMixin, DetailView): """ Display the detail about a user. """ model = User context_object_name = "user_object" template_name = "registration/user_detail.html" def dispatch(self, request, *args, **kwargs): me = request.user if not me.is_authenticated: return self.handle_no_permission() user = self.get_object() if user == me or me.registration.is_admin or me.registration.is_volunteer \ and user.registration.participates and user.registration.team \ and user.registration.team.participation.tournament in user.registration.organized_tournaments.all() \ or user.registration.is_volunteer and me.registration.is_volunteer \ and me.registration.interesting_tournaments.intersection(user.registration.intersting_tournaments): return super().dispatch(request, *args, **kwargs) raise PermissionDenied def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = _("Detail of user {user}").format(user=str(self.object.registration)) return context class UserListView(AdminMixin, SingleTableView): """ Display the list of all registered users. """ model = Registration table_class = RegistrationTable template_name = "registration/user_list.html" class UserUpdateView(UserMixin, UpdateView): """ Update the detail about a user and its registration. """ model = User form_class = UserForm template_name = "registration/update_user.html" def dispatch(self, request, *args, **kwargs): if not self.request.user.is_authenticated or \ not self.request.user.registration.is_admin and self.request.user != self.get_object(): return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.get_object() context["title"] = _("Update user {user}").format(user=str(self.object.registration)) context["registration_form"] = user.registration.form_class(data=self.request.POST or None, instance=self.object.registration) if not self.request.user.registration.is_admin: if "team" in context["registration_form"].fields: del context["registration_form"].fields["team"] del context["registration_form"].fields["email_confirmed"] return context @transaction.atomic def form_valid(self, form): user = form.instance registration_form = user.registration.form_class(data=self.request.POST or None, instance=self.object.registration) if not self.request.user.registration.is_admin: if "team" in registration_form.fields: del registration_form.fields["team"] del registration_form.fields["email_confirmed"] if not registration_form.is_valid(): return self.form_invalid(form) registration_form.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("registration:user_detail", args=(self.object.pk,)) class UserUploadPhotoAuthorizationView(UserMixin, UpdateView): """ A participant can send its photo authorization. """ model = StudentRegistration form_class = PhotoAuthorizationForm template_name = "registration/upload_photo_authorization.html" extra_context = dict(title=_("Upload photo authorization")) def dispatch(self, request, *args, **kwargs): if not self.request.user.is_authenticated or \ not self.request.user.registration.is_admin and self.request.user != self.get_object().user: return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) @transaction.atomic def form_valid(self, form): old_instance = StudentRegistration.objects.get(pk=self.object.pk) if old_instance.photo_authorization: old_instance.photo_authorization.delete() old_instance.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("registration:user_detail", args=(self.object.user.pk,)) class UserUploadHealthSheetView(UserMixin, UpdateView): """ A participant can send its health sheet. """ model = StudentRegistration form_class = HealthSheetForm template_name = "registration/upload_health_sheet.html" extra_context = dict(title=_("Upload health sheet")) def dispatch(self, request, *args, **kwargs): if not self.request.user.is_authenticated or \ not self.request.user.registration.is_admin and self.request.user != self.get_object().user: return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) @transaction.atomic def form_valid(self, form): old_instance = StudentRegistration.objects.get(pk=self.object.pk) if old_instance.health_sheet: old_instance.health_sheet.delete() old_instance.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("registration:user_detail", args=(self.object.user.pk,)) class UserUploadParentalAuthorizationView(UserMixin, UpdateView): """ A participant can send its parental authorization. """ model = StudentRegistration form_class = ParentalAuthorizationForm template_name = "registration/upload_parental_authorization.html" extra_context = dict(title=_("Upload parental authorization")) def dispatch(self, request, *args, **kwargs): if not self.request.user.is_authenticated or \ not self.request.user.registration.is_admin and self.request.user != self.get_object().user: return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) @transaction.atomic def form_valid(self, form): old_instance = StudentRegistration.objects.get(pk=self.object.pk) if old_instance.parental_authorization: old_instance.parental_authorization.delete() old_instance.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("registration:user_detail", args=(self.object.user.pk,)) class AuthorizationTemplateView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if "registration_id" in self.request.GET: registration = Registration.objects.get(pk=self.request.GET.get("registration_id")) # Don't get unwanted information if registration.user == self.request.user \ or self.request.user.is_authenticated and self.request.user.registration.is_admin: context["registration"] = registration if "tournament_id" in self.request.GET: context["tournament"] = Tournament.objects.get(pk=self.request.GET.get("tournament_id")) else: raise ValueError("Merci d'indiquer un tournoi.") return context def render_to_response(self, context, **response_kwargs): tex = render_to_string(self.template_name, context=context, request=self.request) temp_dir = mkdtemp() with open(os.path.join(temp_dir, "texput.tex"), "w") as f: f.write(tex) process = subprocess.Popen(["pdflatex", "-interaction=nonstopmode", f"-output-directory={temp_dir}", os.path.join(temp_dir, "texput.tex"), ]) process.wait() return FileResponse(open(os.path.join(temp_dir, "texput.pdf"), "rb"), content_type="application/pdf", filename=self.template_name.split("/")[-1][:-3] + "pdf") class AdultPhotoAuthorizationTemplateView(AuthorizationTemplateView): template_name = "registration/tex/Autorisation_droit_image_majeur.tex" class ChildPhotoAuthorizationTemplateView(AuthorizationTemplateView): template_name = "registration/tex/Autorisation_droit_image_mineur.tex" class ParentalAuthorizationTemplateView(AuthorizationTemplateView): template_name = "registration/tex/Autorisation_parentale.tex" class InstructionsTemplateView(AuthorizationTemplateView): template_name = "registration/tex/Instructions.tex" class PaymentUpdateView(LoginRequiredMixin, UpdateView): model = Payment form_class = PaymentForm def dispatch(self, request, *args, **kwargs): if not self.request.user.is_authenticated or \ not self.request.user.registration.is_admin \ and (self.request.user != self.get_object().registration.user or self.get_object().valid is not False): return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) def get_form(self, form_class=None): form = super().get_form(form_class) if not self.request.user.registration.is_admin: del form.fields["type"].widget.choices[-1] del form.fields["valid"] return form def form_valid(self, form): if not self.request.user.registration.is_admin: form.instance.valid = None return super().form_valid(form) class PhotoAuthorizationView(LoginRequiredMixin, View): """ Display the sent photo authorization. """ def get(self, request, *args, **kwargs): filename = kwargs["filename"] path = f"media/authorization/photo/{filename}" if not os.path.exists(path): raise Http404 student = ParticipantRegistration.objects.get(photo_authorization__endswith=filename) user = request.user if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team and student.team.participation.tournament in user.registration.organized_tournaments.all()): raise PermissionDenied # Guess mime type of the file mime = Magic(mime=True) mime_type = mime.from_file(path) ext = mime_type.split("/")[1].replace("jpeg", "jpg") # Replace file name true_file_name = _("Photo authorization of {student}.{ext}").format(student=str(student), ext=ext) return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name) class HealthSheetView(LoginRequiredMixin, View): """ Display the sent health sheet. """ def get(self, request, *args, **kwargs): filename = kwargs["filename"] path = f"media/authorization/health/{filename}" if not os.path.exists(path): raise Http404 student = StudentRegistration.objects.get(health_sheet__endswith=filename) user = request.user if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team and student.team.participation.tournament in user.registration.organized_tournaments.all()): raise PermissionDenied # Guess mime type of the file mime = Magic(mime=True) mime_type = mime.from_file(path) ext = mime_type.split("/")[1].replace("jpeg", "jpg") # Replace file name true_file_name = _("Health sheet of {student}.{ext}").format(student=str(student), ext=ext) return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name) class ParentalAuthorizationView(LoginRequiredMixin, View): """ Display the sent parental authorization. """ def get(self, request, *args, **kwargs): filename = kwargs["filename"] path = f"media/authorization/parental/{filename}" if not os.path.exists(path): raise Http404 student = StudentRegistration.objects.get(parental_authorization__endswith=filename) user = request.user if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team and student.team.participation.tournament in user.registration.organized_tournaments.all()): raise PermissionDenied # Guess mime type of the file mime = Magic(mime=True) mime_type = mime.from_file(path) ext = mime_type.split("/")[1].replace("jpeg", "jpg") # Replace file name true_file_name = _("Parental authorization of {student}.{ext}").format(student=str(student), ext=ext) return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name) class ScholarshipView(LoginRequiredMixin, View): """ Display the sent scholarship paper. """ def get(self, request, *args, **kwargs): filename = kwargs["filename"] path = f"media/authorization/scholarship/{filename}" if not os.path.exists(path): raise Http404 payment = Payment.objects.get(scholarship_file__endswith=filename) user = request.user if not (payment.registration.user == user or user.registration.is_admin): raise PermissionDenied # Guess mime type of the file mime = Magic(mime=True) mime_type = mime.from_file(path) ext = mime_type.split("/")[1].replace("jpeg", "jpg") # Replace file name true_file_name = _("Scholarship attestation of {user}.{ext}").format(user=str(user.registration), ext=ext) return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name) class SolutionView(LoginRequiredMixin, View): """ Display the sent solution. """ def get(self, request, *args, **kwargs): filename = kwargs["filename"] path = f"media/solutions/{filename}" if not os.path.exists(path): raise Http404 solution = Solution.objects.get(file__endswith=filename) user = request.user passage_participant_qs = Passage.objects.filter(Q(defender=user.registration.team.participation) | Q(opponent=user.registration.team.participation) | Q(reporter=user.registration.team.participation), defender=solution.participation, solution_number=solution.problem) if not (user.registration.is_admin or user.registration.is_volunteer and Passage.objects.filter(Q(pool__juries=user.registration) | Q(pool__tournament__in=user.registration.organized_tournaments.all()), defender=solution.participation, solution_number=solution.problem).exists() or user.registration.participates and user.registration.team and (solution.participation.team == user.registration.team or any(passage.pool.round == 1 or timezone.now() >= passage.pool.tournament.solutions_available_second_phase for passage in passage_participant_qs.all()))): raise PermissionDenied # Guess mime type of the file mime = Magic(mime=True) mime_type = mime.from_file(path) ext = mime_type.split("/")[1].replace("jpeg", "jpg") # Replace file name true_file_name = str(solution) + f".{ext}" return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name) class SynthesisView(LoginRequiredMixin, View): """ Display the sent synthesis. """ def get(self, request, *args, **kwargs): filename = kwargs["filename"] path = f"media/syntheses/{filename}" if not os.path.exists(path): raise Http404 synthesis = Synthesis.objects.get(file__endswith=filename) user = request.user if not (user.registration.is_admin or user.registration.is_volunteer and (user.registration in synthesis.passage.pool.juries.all() or user.registration in synthesis.passage.pool.tournament.organizers.all()) or user.registration.participates and user.registration.team == synthesis.participation.team): raise PermissionDenied # Guess mime type of the file mime = Magic(mime=True) mime_type = mime.from_file(path) ext = mime_type.split("/")[1].replace("jpeg", "jpg") # Replace file name true_file_name = str(synthesis) + f".{ext}" return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name) class UserImpersonateView(LoginRequiredMixin, RedirectView): """ An administrator can log in through this page as someone else, and act as this other person. """ def dispatch(self, request, *args, **kwargs): if self.request.user.registration.is_admin: if not User.objects.filter(pk=kwargs["pk"]).exists(): raise Http404 session = request.session session["admin"] = request.user.pk session["_fake_user_id"] = kwargs["pk"] return super().dispatch(request, *args, **kwargs) def get_redirect_url(self, *args, **kwargs): return reverse_lazy("registration:user_detail", args=(kwargs["pk"],)) class ResetAdminView(LoginRequiredMixin, View): """ Return to admin view, clear the session field that let an administrator to log in as someone else. """ def dispatch(self, request, *args, **kwargs): user = request.user if not user.is_authenticated: return self.handle_no_permission() if "_fake_user_id" in request.session: del request.session["_fake_user_id"] return redirect(request.GET.get("path", reverse_lazy("index")))