import random import zipfile from datetime import timedelta from io import BytesIO from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.core.mail import send_mail from django.db.models import Q 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 import timezone from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, CreateView, UpdateView from django.views.generic.edit import BaseFormView from django_tables2.views import SingleTableView from member.models import TFJMUser, Solution, Synthesis from .forms import TournamentForm, OrganizerForm, SolutionForm, SynthesisForm, TeamForm, PoolForm from .models import Tournament, Team, Pool from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable, PoolTable class AdminMixin(LoginRequiredMixin): """ If a view extends this mixin, then the view will be only accessible to administrators. """ def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated or not request.user.admin: raise PermissionDenied return super().dispatch(request, *args, **kwargs) class OrgaMixin(LoginRequiredMixin): """ If a view extends this mixin, then the view will be only accessible to administrators or organizers. """ def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated or not request.user.organizes: raise PermissionDenied return super().dispatch(request, *args, **kwargs) class TeamMixin(LoginRequiredMixin): """ If a view extends this mixin, then the view will be only accessible to users that are registered in a team. """ def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated or not request.user.team: raise PermissionDenied return super().dispatch(request, *args, **kwargs) class TournamentListView(SingleTableView): """ Display the list of all tournaments, ordered by start date then name. """ model = Tournament table_class = TournamentTable extra_context = dict(title=_("Tournaments list"),) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) team_users = TFJMUser.objects.filter(Q(team__isnull=False) | Q(role="admin") | Q(role="organizer"))\ .order_by('-role') valid_team_users = team_users.filter( Q(team__validation_status="2valid") | Q(role="admin") | Q(role="organizer")) context["team_users_emails"] = [user.email for user in team_users] context["valid_team_users_emails"] = [user.email for user in valid_team_users] return context class TournamentCreateView(AdminMixin, CreateView): """ Create a tournament. Only accessible to admins. """ model = Tournament form_class = TournamentForm extra_context = dict(title=_("Add tournament"),) def get_success_url(self): return reverse_lazy('tournament:detail', args=(self.object.pk,)) class TournamentDetailView(DetailView): """ Display the detail of a tournament. Accessible to all, including not authenticated users. """ model = Tournament def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = _("Tournament of {name}").format(name=self.object.name) if self.object.final: team_users = TFJMUser.objects.filter(team__selected_for_final=True) valid_team_users = team_users else: team_users = TFJMUser.objects.filter( Q(team__tournament=self.object) | Q(organized_tournaments=self.object)).order_by('role') valid_team_users = team_users.filter( Q(team__validation_status="2valid") | Q(role="admin") | Q(organized_tournaments=self.object)) context["team_users_emails"] = [user.email for user in team_users] context["valid_team_users_emails"] = [user.email for user in valid_team_users] context["teams"] = TeamTable(self.object.teams.all()) return context class TournamentUpdateView(OrgaMixin, UpdateView): """ Update the data of a tournament. Reserved to admins and organizers of the tournament. """ def dispatch(self, request, *args, **kwargs): """ Restrict the view to organizers of tournaments, then process the request. """ if self.request.user.role == "1volunteer" and self.request.user not in self.get_object().organizers.all(): raise PermissionDenied return super().dispatch(request, *args, **kwargs) model = Tournament form_class = TournamentForm extra_context = dict(title=_("Update tournament"),) def get_success_url(self): return reverse_lazy('tournament:detail', args=(self.object.pk,)) class TeamDetailView(LoginRequiredMixin, DetailView): """ View the detail of a team. Restricted to this team, admins and organizers of its tournament. """ model = Team def dispatch(self, request, *args, **kwargs): """ Protect the page and process the request. """ if not request.user.is_authenticated or \ (not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all() and not (self.get_object().selected_for_final and request.user in Tournament.get_final().organizers.all()) and self.get_object() != request.user.team): raise PermissionDenied return super().dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): """ Process POST requests. Supported requests: - get the solutions of the team as a ZIP archive - a user leaves its team (if the composition is not validated yet) - the team requests the validation - Organizers can validate or invalidate the request - Admins can delete teams - Admins can select teams for the final tournament """ team = self.get_object() if "zip" in request.POST: solutions = team.solutions.all() out = BytesIO() zf = zipfile.ZipFile(out, "w") for solution in solutions: zf.write(solution.file.path, str(solution) + ".pdf") zf.close() resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename={}'\ .format(_("Solutions for team {team}.zip") .format(team=str(team)).replace(" ", "%20")) return resp elif "leave" in request.POST and request.user.participates: request.user.team = None request.user.save() if not team.users.exists(): team.delete() return redirect('tournament:detail', pk=team.tournament.pk) elif "request_validation" in request.POST and request.user.participates and team.can_validate: team.validation_status = "1waiting" team.save() team.tournament.send_mail_to_organizers("request_validation", "Demande de validation TFJM²", team=team) return redirect('tournament:team_detail', pk=team.pk) elif "validate" in request.POST and request.user.organizes: team.validation_status = "2valid" team.save() team.send_mail("validate_team", "Équipe validée TFJM²") return redirect('tournament:team_detail', pk=team.pk) elif "invalidate" in request.POST and request.user.organizes: team.validation_status = "0invalid" team.save() team.send_mail("unvalidate_team", "Équipe non validée TFJM²") return redirect('tournament:team_detail', pk=team.pk) elif "delete" in request.POST and request.user.organizes: team.delete() return redirect('tournament:detail', pk=team.tournament.pk) elif "select_final" in request.POST and request.user.admin and not team.selected_for_final and team.pools: # We copy all solutions for solutions for the final for solution in team.solutions.all(): alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789" id = "" for i in range(64): id += random.choice(alphabet) with solution.file.open("rb") as source: with open("/code/media/" + id, "wb") as dest: for chunk in source.chunks(): dest.write(chunk) new_sol = Solution( file=id, team=team, problem=solution.problem, final=True, ) new_sol.save() team.selected_for_final = True team.save() team.send_mail("select_for_final", "Sélection pour la finale, félicitations ! - TFJM²", final=Tournament.get_final()) return redirect('tournament:team_detail', pk=team.pk) return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = _("Information about team") context["ordered_solutions"] = self.object.solutions.order_by('problem').all() context["team_users_emails"] = [user.email for user in self.object.users.all()] return context class TeamUpdateView(LoginRequiredMixin, UpdateView): """ Update the information about a team. Team members, admins and organizers are allowed to do this. """ model = Team form_class = TeamForm extra_context = dict(title=_("Update team"),) def dispatch(self, request, *args, **kwargs): if not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all() \ and self.get_object() != self.request.user.team: raise PermissionDenied return super().dispatch(request, *args, **kwargs) class AddOrganizerView(AdminMixin, CreateView): """ Add a new organizer account. No password is created, the user should reset its password using the link sent by mail. Only name and email are requested. Only admins are granted to do this. """ model = TFJMUser form_class = OrganizerForm extra_context = dict(title=_("Add organizer"),) template_name = "tournament/add_organizer.html" def form_valid(self, form): user = form.instance msg = render_to_string("mail_templates/add_organizer.txt", context=dict(user=user)) msg_html = render_to_string("mail_templates/add_organizer.html", context=dict(user=user)) send_mail('Organisateur du TFJM² 2020', msg, 'contact@tfjm.org', [user.email], html_message=msg_html) return super().form_valid(form) def get_success_url(self): return reverse_lazy('index') class SolutionsView(TeamMixin, BaseFormView, SingleTableView): """ Upload and view solutions for a team. """ model = Solution table_class = SolutionTable form_class = SolutionForm template_name = "tournament/solutions_list.html" extra_context = dict(title=_("Solutions")) def post(self, request, *args, **kwargs): if "zip" in request.POST: solutions = request.user.team.solutions out = BytesIO() zf = zipfile.ZipFile(out, "w") for solution in solutions: zf.write(solution.file.path, str(solution) + ".pdf") zf.close() resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename={}'\ .format(_("Solutions for team {team}.zip") .format(team=str(request.user.team)).replace(" ", "%20")) return resp return super().post(request, *args, **kwargs) def get_context_data(self, **kwargs): self.object_list = self.get_queryset() context = super().get_context_data(**kwargs) context["now"] = timezone.now() context["real_deadline"] = self.request.user.team.future_tournament.date_solutions + timedelta(minutes=30) return context def get_queryset(self): qs = super().get_queryset().filter(team=self.request.user.team) return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'problem',) def form_valid(self, form): solution = form.instance solution.team = self.request.user.team solution.final = solution.team.selected_for_final if timezone.now() > solution.tournament.date_solutions + timedelta(minutes=30): form.add_error('file', _("You can't publish your solution anymore. Deadline: {date:%m-%d-%Y %H:%M}.") .format(date=timezone.localtime(solution.tournament.date_solutions))) return super().form_invalid(form) prev_sol = Solution.objects.filter(problem=solution.problem, team=solution.team, final=solution.final) for sol in prev_sol.all(): sol.delete() alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789" id = "" for i in range(64): id += random.choice(alphabet) solution.file.name = id solution.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("tournament:solutions") class SolutionsOrgaListView(OrgaMixin, SingleTableView): """ View all solutions sent by teams for the organized tournaments. Juries can view solutions of their pools. Organizers can download a ZIP archive for each organized tournament. """ model = Solution table_class = SolutionTable template_name = "tournament/solutions_orga_list.html" extra_context = dict(title=_("All solutions")) def post(self, request, *args, **kwargs): if "tournament_zip" in request.POST: tournament = Tournament.objects.get(pk=int(request.POST["tournament_zip"])) solutions = tournament.solutions if not request.user.admin and request.user not in tournament.organizers.all(): raise PermissionDenied out = BytesIO() zf = zipfile.ZipFile(out, "w") for solution in solutions: zf.write(solution.file.path, str(solution) + ".pdf") zf.close() resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename={}'\ .format(_("Solutions for tournament {tournament}.zip") .format(tournament=str(tournament)).replace(" ", "%20")) return resp return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["tournaments"] = \ Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments return context def get_queryset(self): qs = super().get_queryset() if not self.request.user.admin: if self.request.user in Tournament.get_final().organizers.all(): qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(pools__juries=self.request.user) | Q(final=True)) else: qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(pools__juries=self.request.user)) return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'problem',).distinct() class SynthesesView(TeamMixin, BaseFormView, SingleTableView): """ Upload and view syntheses for a team. """ model = Synthesis table_class = SynthesisTable form_class = SynthesisForm template_name = "tournament/syntheses_list.html" extra_context = dict(title=_("Syntheses")) def post(self, request, *args, **kwargs): if "zip" in request.POST: syntheses = request.user.team.syntheses out = BytesIO() zf = zipfile.ZipFile(out, "w") for synthesis in syntheses: zf.write(synthesis.file.path, str(synthesis) + ".pdf") zf.close() resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename={}'\ .format(_("Syntheses for team {team}.zip") .format(team=str(request.user.team)).replace(" ", "%20")) return resp return super().post(request, *args, **kwargs) def get_queryset(self): qs = super().get_queryset().filter(team=self.request.user.team) return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'round', 'source',) def get_context_data(self, **kwargs): self.object_list = self.get_queryset() context = super().get_context_data(**kwargs) context["now"] = timezone.now() context["real_deadline_1"] = self.request.user.team.future_tournament.date_syntheses + timedelta(minutes=30) context["real_deadline_2"] = self.request.user.team.future_tournament.date_syntheses_2 + timedelta(minutes=30) return context def form_valid(self, form): synthesis = form.instance synthesis.team = self.request.user.team synthesis.final = synthesis.team.selected_for_final if synthesis.round == '1' and timezone.now() > (synthesis.tournament.date_syntheses + timedelta(minutes=30)): form.add_error('file', _("You can't publish your synthesis anymore for the first round." " Deadline: {date:%m-%d-%Y %H:%M}.") .format(date=timezone.localtime(synthesis.tournament.date_syntheses))) return super().form_invalid(form) if synthesis.round == '2' and timezone.now() > synthesis.tournament.date_syntheses_2 + timedelta(minutes=30): form.add_error('file', _("You can't publish your synthesis anymore for the second round." " Deadline: {date:%m-%d-%Y %H:%M}.") .format(date=timezone.localtime(synthesis.tournament.date_syntheses_2))) return super().form_invalid(form) prev_syn = Synthesis.objects.filter(team=synthesis.team, round=synthesis.round, source=synthesis.source, final=synthesis.final) for syn in prev_syn.all(): syn.delete() alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789" id = "" for i in range(64): id += random.choice(alphabet) synthesis.file.name = id synthesis.save() return super().form_valid(form) def get_success_url(self): return reverse_lazy("tournament:syntheses") class SynthesesOrgaListView(OrgaMixin, SingleTableView): """ View all syntheses sent by teams for the organized tournaments. Juries can view syntheses of their pools. Organizers can download a ZIP archive for each organized tournament. """ model = Synthesis table_class = SynthesisTable template_name = "tournament/syntheses_orga_list.html" extra_context = dict(title=_("All syntheses")) def post(self, request, *args, **kwargs): if "tournament_zip" in request.POST: tournament = Tournament.objects.get(pk=request.POST["tournament_zip"]) syntheses = tournament.syntheses if not request.user.admin and request.user not in tournament.organizers.all(): raise PermissionDenied out = BytesIO() zf = zipfile.ZipFile(out, "w") for synthesis in syntheses: zf.write(synthesis.file.path, str(synthesis) + ".pdf") zf.close() resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename={}'\ .format(_("Syntheses for tournament {tournament}.zip") .format(tournament=str(tournament)).replace(" ", "%20")) return resp return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["tournaments"] = \ Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments return context def get_queryset(self): qs = super().get_queryset() if not self.request.user.admin: if self.request.user in Tournament.get_final().organizers.all(): qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(team__pools__juries=self.request.user) | Q(final=True)) else: qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(team__pools__juries=self.request.user)) return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'round', 'source',).distinct() class PoolListView(LoginRequiredMixin, SingleTableView): """ View the list of visible pools. Admins see all, juries see their own pools, organizers see the pools of their tournaments. """ model = Pool table_class = PoolTable extra_context = dict(title=_("Pools")) def get_queryset(self): qs = super().get_queryset() user = self.request.user if not user.admin and user.organizes: qs = qs.filter(Q(juries=user) | Q(teams__tournament__organizers=user)) elif user.participates: qs = qs.filter(teams=user.team) qs = qs.distinct().order_by('solutions__final', 'teams__tournament__date_start', 'teams__tournament__name', 'round',) return qs class PoolCreateView(AdminMixin, CreateView): """ Create a pool manually. This page should not be used: prefer send automatically data from the drawing bot. """ model = Pool form_class = PoolForm extra_context = dict(title=_("Create pool")) def get_success_url(self): return reverse_lazy("tournament:pools") class PoolDetailView(LoginRequiredMixin, DetailView): """ See the detail of a pool. Teams and juries can download here defended solutions of the pool. If this is the second round, teams can't download solutions of the other teams before the date when they should be available. Juries see also syntheses. They see of course solutions immediately. This is also true for organizers and admins. All can be downloaded as a ZIP archive. """ model = Pool extra_context = dict(title=_("Pool detail")) def get_queryset(self): qs = super().get_queryset() user = self.request.user if not user.admin and user.organizes: qs = qs.filter(Q(juries=user) | Q(teams__tournament__organizers=user)) elif user.participates: qs = qs.filter(teams=user.team) return qs.distinct() def post(self, request, *args, **kwargs): user = request.user pool = self.get_object() if "solutions_zip" in request.POST: if user.participates and pool.round == 2 and pool.tournament.date_solutions_2 > timezone.now(): raise PermissionDenied out = BytesIO() zf = zipfile.ZipFile(out, "w") for solution in pool.solutions.all(): zf.write(solution.file.path, str(solution) + ".pdf") zf.close() resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename={}' \ .format(_("Solutions of a pool for the round {round} of the tournament {tournament}.zip") .format(round=pool.round, tournament=str(pool.tournament)).replace(" ", "%20")) return resp elif "syntheses_zip" in request.POST and user.organizes: if user.participates and pool.round == 2 and pool.tournament.date_solutions_2 > timezone.now(): raise PermissionDenied out = BytesIO() zf = zipfile.ZipFile(out, "w") for synthesis in pool.syntheses.all(): zf.write(synthesis.file.path, str(synthesis) + ".pdf") zf.close() resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed") resp['Content-Disposition'] = 'attachment; filename={}' \ .format(_("Syntheses of a pool for the round {round} of the tournament {tournament}.zip") .format(round=pool.round, tournament=str(pool.tournament)).replace(" ", "%20")) return resp return self.get(request, *args, **kwargs)