import random from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Q from django.http import FileResponse, Http404 from django.shortcuts import redirect, resolve_url from django.urls import reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.http import urlsafe_base64_decode from django.utils.translation import gettext_lazy as _ from django.views import View from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import CreateView, UpdateView, DetailView, FormView, TemplateView from django_tables2 import SingleTableView from tournament.forms import TeamForm, JoinTeam from tournament.models import Team, Tournament, Pool from tournament.views import AdminMixin, TeamMixin, OrgaMixin from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis from .tables import UserTable from .tokens import email_validation_token class CreateUserView(CreateView): """ Signup form view. """ model = TFJMUser form_class = SignUpForm template_name = "registration/signup.html" # When errors are reported from the signup view, don't send passwords to admins @method_decorator(sensitive_post_parameters('password1', 'password2',)) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) def form_valid(self, form): form.instance.send_email_validation_link() return super().form_valid(form) def get_success_url(self): return reverse_lazy('member: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 = {"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.email_confirmed = True user.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 = TFJMUser.objects.get(pk=uid) except (TypeError, ValueError, OverflowError, TFJMUser.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 = {"title": _('Email validation email sent')} class UserResendValidationEmailView(LoginRequiredMixin, DetailView): """ Rensend the email validation link. """ model = TFJMUser extra_context = {"title": _("Resend email validation link")} def get(self, request, *args, **kwargs): user = self.get_object() user.profile.send_email_validation_link() url = 'member:user_detail' if user.profile.registration_valid else 'member:future_user_detail' return redirect(url, user.id) class MyAccountView(LoginRequiredMixin, UpdateView): """ Update our personal data. """ model = TFJMUser template_name = "member/my_account.html" def get_form_class(self): # The used form can change according to the role of the user. return AdminUserForm if self.request.user.organizes else TFJMUserForm \ if self.request.user.role == "3participant" else CoachUserForm def get_object(self, queryset=None): return self.request.user def get_success_url(self): return reverse_lazy('member:my_account') class UserDetailView(LoginRequiredMixin, DetailView): """ View the personal information of a given user. Only organizers can see this page, since there are personal data. """ model = TFJMUser form_class = TFJMUserForm context_object_name = "tfjmuser" def dispatch(self, request, *args, **kwargs): if isinstance(request.user, AnonymousUser): raise PermissionDenied self.object = self.get_object() if not request.user.admin \ and (self.object.team is not None and request.user not in self.object.team.tournament.organizers.all())\ and (self.object.team is not None and self.object.team.selected_for_final and request.user not in Tournament.get_final().organizers.all())\ and self.request.user != self.object: raise PermissionDenied return super().dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): """ An administrator can log in through this page as someone else, and act as this other person. """ if "view_as" in request.POST and self.request.user.admin: session = request.session session["admin"] = request.user.pk obj = self.get_object() session["_fake_user_id"] = obj.pk return redirect(request.path) return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = str(self.object) return context class AddTeamView(LoginRequiredMixin, CreateView): """ Register a new team. Users can choose the name, the trigram and a preferred tournament. """ model = Team form_class = TeamForm def form_valid(self, form): if self.request.user.organizes: form.add_error('name', _("You can't organize and participate at the same time.")) return self.form_invalid(form) if self.request.user.team: form.add_error('name', _("You are already in a team.")) return self.form_invalid(form) # Generate a random access code team = form.instance alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789" code = "" for i in range(6): code += random.choice(alphabet) team.access_code = code team.validation_status = "0invalid" team.save() team.refresh_from_db() self.request.user.team = team self.request.user.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("member:my_team") class JoinTeamView(LoginRequiredMixin, FormView): """ Join a team with a given access code. """ model = Team form_class = JoinTeam template_name = "tournament/team_form.html" def form_valid(self, form): team = form.cleaned_data["team"] if self.request.user.organizes: form.add_error('access_code', _("You can't organize and participate at the same time.")) return self.form_invalid(form) if self.request.user.team: form.add_error('access_code', _("You are already in a team.")) return self.form_invalid(form) if self.request.user.role == '2coach' and len(team.coaches) == 3: form.add_error('access_code', _("This team is full of coachs.")) return self.form_invalid(form) if self.request.user.role == '3participant' and len(team.participants) == 6: form.add_error('access_code', _("This team is full of participants.")) return self.form_invalid(form) if not team.invalid: form.add_error('access_code', _("This team is already validated or waiting for validation.")) self.request.user.team = team self.request.user.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("member:my_team") class MyTeamView(TeamMixin, View): """ Redirect to the page of the information of our personal team. """ def get(self, request, *args, **kwargs): return redirect("tournament:team_detail", pk=request.user.team.pk) class DocumentView(AccessMixin, View): """ View a PDF document, if we have the right. - Everyone can see the documents that concern itself. - An administrator can see anything. - An organizer can see documents that are related to its tournament. - A jury can see solutions and syntheses that are evaluated in their pools. """ def get(self, request, *args, **kwargs): try: doc = Document.objects.get(file=self.kwargs["file"]) except Document.DoesNotExist: raise Http404(_("No %(verbose_name)s found matching the query") % {'verbose_name': Document._meta.verbose_name}) if request.user.is_authenticated: grant = request.user.admin if isinstance(doc, Solution) or isinstance(doc, Synthesis): grant = grant or doc.team == request.user.team or request.user in doc.tournament.organizers.all() elif isinstance(doc, MotivationLetter): grant = grant or doc.team == request.user.team or request.user in doc.team.tournament.organizers.all() grant = grant or doc.team.selected_for_final and request.user in Tournament.get_final().organizers.all() if isinstance(doc, Solution): for pool in doc.pools.all(): if request.user in pool.juries.all(): grant = True break if pool.round == 2 and timezone.now() < doc.tournament.date_solutions_2: continue if self.request.user.team in pool.teams.all(): grant = True elif isinstance(doc, Synthesis): for pool in request.user.pools.all(): # If the user is a jury in the pool if doc.team in pool.teams.all() and doc.final == pool.tournament.final: grant = True break else: pool = Pool.objects.filter(extra_access_token=self.request.session["extra_access_token"]) if pool.exists(): pool = pool.get() if isinstance(doc, Solution): grant = doc in pool.solutions.all() elif isinstance(doc, Synthesis): grant = doc.team in pool.teams.all() and doc.final == pool.tournament.final else: grant = False else: grant = False if not grant: raise PermissionDenied return FileResponse(doc.file, content_type="application/pdf", filename=str(doc) + ".pdf") class ProfileListView(AdminMixin, SingleTableView): """ List all registered profiles. """ model = TFJMUser queryset = TFJMUser.objects.order_by("role", "last_name", "first_name") table_class = UserTable template_name = "member/profile_list.html" extra_context = dict(title=_("All profiles"), type="all") class OrphanedProfileListView(AdminMixin, SingleTableView): """ List all orphaned profiles, ie. participants that have no team. """ model = TFJMUser queryset = TFJMUser.objects.filter((Q(role="2coach") | Q(role="3participant")) & Q(team__isnull=True))\ .order_by("role", "last_name", "first_name") table_class = UserTable template_name = "member/profile_list.html" extra_context = dict(title=_("Orphaned profiles"), type="orphaned") class OrganizersListView(OrgaMixin, SingleTableView): """ List all organizers. """ model = TFJMUser queryset = TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer"))\ .order_by("role", "last_name", "first_name") table_class = UserTable template_name = "member/profile_list.html" extra_context = dict(title=_("Organizers"), type="organizers") class ResetAdminView(AdminMixin, 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): if "_fake_user_id" in request.session: del request.session["_fake_user_id"] return redirect(request.GET["path"])