diff --git a/apps/tournament/models.py b/apps/tournament/models.py index 23163b9..eeb94c2 100644 --- a/apps/tournament/models.py +++ b/apps/tournament/models.py @@ -63,6 +63,11 @@ class Tournament(models.Model): verbose_name=_("year"), ) + @property + def solutions(self): + from member.models import Solution + return Solution.objects.filter(team__tournament=self) + @classmethod def get_final(cls): return cls.objects.get(year=os.getenv("TFJM_YEAR"), final=True) diff --git a/apps/tournament/tables.py b/apps/tournament/tables.py index 8f100a8..7cebb16 100644 --- a/apps/tournament/tables.py +++ b/apps/tournament/tables.py @@ -38,3 +38,25 @@ class TeamTable(tables.Table): attrs = { 'class': 'table table-condensed table-striped table-hover' } + + +class SolutionTable(tables.Table): + file = tables.LinkColumn( + "document", + args=[A("file")], + attrs={ + "a": { + "data-turbolinks": "false", + } + } + ) + + def render_file(self): + return _("Download") + + class Meta: + model = Team + fields = ("team", "team.tournament", "problem", "uploaded_at", "file", ) + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } diff --git a/apps/tournament/urls.py b/apps/tournament/urls.py index bfe39cf..4d03d7f 100644 --- a/apps/tournament/urls.py +++ b/apps/tournament/urls.py @@ -1,7 +1,7 @@ from django.urls import path from django.views.generic import RedirectView -from .views import TournamentListView, TournamentDetailView, TeamDetailView +from .views import TournamentListView, TournamentDetailView, TeamDetailView, SolutionsView, SolutionsOrgaListView app_name = "tournament" @@ -11,8 +11,8 @@ urlpatterns = [ path('/', TournamentDetailView.as_view(), name="detail"), path('team//', TeamDetailView.as_view(), name="team_detail"), path("add-organizer/", RedirectView.as_view(pattern_name="index"), name="add_organizer"), - path("solutions/", RedirectView.as_view(pattern_name="index"), name="solutions"), - path("all-solutions/", RedirectView.as_view(pattern_name="index"), name="all_solutions"), + path("solutions/", SolutionsView.as_view(), name="solutions"), + path("all-solutions/", SolutionsOrgaListView.as_view(), name="all_solutions"), path("syntheses/", RedirectView.as_view(pattern_name="index"), name="syntheses"), path("all_syntheses/", RedirectView.as_view(pattern_name="index"), name="all_syntheses"), ] diff --git a/apps/tournament/views.py b/apps/tournament/views.py index 124017e..7fbb047 100644 --- a/apps/tournament/views.py +++ b/apps/tournament/views.py @@ -1,12 +1,31 @@ +import zipfile +from io import BytesIO + from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import PermissionDenied from django.db.models import Q +from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView from django_tables2.views import SingleTableView -from member.models import TFJMUser +from member.models import TFJMUser, Solution from .models import Tournament, Team -from .tables import TournamentTable, TeamTable +from .tables import TournamentTable, TeamTable, SolutionTable + + +class AdminMixin(object): + def dispatch(self, request, *args, **kwargs): + if not request.user.admin: + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) + + +class TeamMixin(object): + def dispatch(self, request, *args, **kwargs): + if not request.user.team: + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) class TournamentListView(SingleTableView): @@ -61,3 +80,88 @@ class TeamDetailView(LoginRequiredMixin, DetailView): context["title"] = _("Information about team") return context + + +class SolutionsView(LoginRequiredMixin, TeamMixin, SingleTableView): + model = Solution + table_class = SolutionTable + 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 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: + qs = qs.filter(team__tournament__organizers=self.request.user) + return qs.order_by('team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'problem',) + + +class SolutionsOrgaListView(LoginRequiredMixin, AdminMixin, SingleTableView): + 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=request.POST["tournament_zip"][0]) + solutions = tournament.solutions + if not request.user.admin and request.user not in tournament.organizers: + 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: + qs = qs.filter(team__tournament__organizers=self.request.user) + return qs.order_by('team__tournament__date_start', 'team__tournament__name', 'team__trigram', 'problem',) diff --git a/templates/tournament/solutions_list.html b/templates/tournament/solutions_list.html new file mode 100644 index 0000000..3be9113 --- /dev/null +++ b/templates/tournament/solutions_list.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% load i18n django_tables2 %} + +{% block content %} + {% render_table table %} +{% endblock %} diff --git a/templates/tournament/solutions_orga_list.html b/templates/tournament/solutions_orga_list.html new file mode 100644 index 0000000..fe83b4f --- /dev/null +++ b/templates/tournament/solutions_orga_list.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load i18n django_tables2 %} + +{% block content %} + {% render_table table %} + +
+ +
+ {% csrf_token %} +
+ {% for tournament in tournaments.all %} + + {% endfor %} +
+
+{% endblock %} diff --git a/templates/tournament/team_detail.html b/templates/tournament/team_detail.html index 224480d..0fc3a86 100644 --- a/templates/tournament/team_detail.html +++ b/templates/tournament/team_detail.html @@ -43,7 +43,7 @@ {% if team.motivation_letters.count %}
{% blocktrans with version=team.motivation_letters.count %}Motivation letter (version {{ version }}):{% endblocktrans %} - {% trans "Download" %} + {% trans "Download" %}
{% endif %}