From 5084bb65d96685c99a0ac878d536f8eb3cb45855 Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Wed, 27 Mar 2024 00:49:32 +0100 Subject: [PATCH] Add ZIP archive for tournament solutions Signed-off-by: Emmy D'Anello --- locale/fr/LC_MESSAGES/django.po | 58 +++++++---- .../participation/participation_detail.html | 6 ++ .../templates/participation/pool_detail.html | 4 +- .../participation/tournament_detail.html | 11 ++- participation/urls.py | 19 ++-- participation/views.py | 97 ++++++++++++++++--- 6 files changed, 152 insertions(+), 43 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 0773190..1d66a78 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: TFJM\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-26 23:52+0100\n" +"POT-Creation-Date: 2024-03-27 00:47+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Emmy D'Anello \n" "Language-Team: LANGUAGE \n" @@ -1150,23 +1150,27 @@ msgstr "Solutions :" msgid "No solution was uploaded yet." msgstr "Aucune solution n'a encore été envoyée." -#: participation/templates/participation/participation_detail.html:37 +#: participation/templates/participation/participation_detail.html:36 +msgid "Download as ZIP" +msgstr "Télécharger en tant que ZIP" + +#: participation/templates/participation/participation_detail.html:43 msgid "Pools:" msgstr "Poules :" -#: participation/templates/participation/participation_detail.html:51 +#: participation/templates/participation/participation_detail.html:57 msgid "" "If you upload a solution, this will replace the version for the final " "tournament." msgstr "" "Si vous envoyez une solution, elle va remplacer la version pour la finale." -#: participation/templates/participation/participation_detail.html:54 -#: participation/templates/participation/participation_detail.html:58 +#: participation/templates/participation/participation_detail.html:60 +#: participation/templates/participation/participation_detail.html:64 msgid "Upload solution" msgstr "Envoyer une solution" -#: participation/templates/participation/participation_detail.html:59 +#: participation/templates/participation/participation_detail.html:65 #: participation/templates/participation/passage_detail.html:132 #: participation/templates/participation/pool_detail.html:138 #: participation/templates/participation/team_detail.html:210 @@ -1805,54 +1809,74 @@ msgstr "Notes publiées !" msgid "You can't upload a solution after the deadline." msgstr "Vous ne pouvez pas envoyer de solution après la date limite." -#: participation/views.py:851 +#: participation/views.py:866 +#, python-brace-format +msgid "Solutions of team {trigram}.zip" +msgstr "Solutions de l'équipe {trigram}.zip" + +#: participation/views.py:866 +#, python-brace-format +msgid "Syntheses of team {trigram}.zip" +msgstr "Notes de synthèse de l'équipe {trigram}.zip" + +#: participation/views.py:883 participation/views.py:898 +#, python-brace-format +msgid "Solutions of {tournament}.zip" +msgstr "Solutions de {tournament}.zip" + +#: participation/views.py:883 participation/views.py:898 +#, python-brace-format +msgid "Syntheses of {tournament}.zip" +msgstr "Notes de synthèse de {tournament}.zip" + +#: participation/views.py:907 #, python-brace-format msgid "Solutions for pool {pool} of tournament {tournament}.zip" msgstr "Solutions pour la poule {pool} du tournoi {tournament}.zip" -#: participation/views.py:852 +#: participation/views.py:908 #, python-brace-format msgid "Syntheses for pool {pool} of tournament {tournament}.zip" msgstr "Notes de synthèses pour la poule {pool} du tournoi {tournament}.zip" -#: participation/views.py:881 +#: participation/views.py:950 #, python-brace-format msgid "Jury of pool {pool} for {tournament} with teams {teams}" msgstr "Jury de la poule {pool} pour {tournament} avec les équipes {teams}" -#: participation/views.py:897 +#: participation/views.py:966 #, python-brace-format msgid "The jury {name} is already in the pool!" msgstr "{name} est déjà dans la poule !" -#: participation/views.py:917 +#: participation/views.py:986 msgid "New TFJM² jury account" msgstr "Nouveau compte de juré⋅e pour le TFJM²" -#: participation/views.py:934 +#: participation/views.py:1003 #, python-brace-format msgid "The jury {name} has been successfully added!" msgstr "{name} a été ajouté⋅e avec succès en tant que juré⋅e !" -#: participation/views.py:969 +#: participation/views.py:1038 #, python-brace-format msgid "The jury {name} has been successfully removed!" msgstr "{name} a été retiré⋅e avec succès du jury !" -#: participation/views.py:995 +#: participation/views.py:1064 #, python-brace-format msgid "The jury {name} has been successfully promoted president!" msgstr "{name} a été nommé⋅e président⋅e du jury !" -#: participation/views.py:1023 +#: participation/views.py:1092 msgid "The following user is not registered as a jury:" msgstr "L'utilisateur⋅rice suivant n'est pas inscrit⋅e en tant que juré⋅e :" -#: participation/views.py:1037 +#: participation/views.py:1106 msgid "Notes were successfully uploaded." msgstr "Les notes ont bien été envoyées." -#: participation/views.py:1742 +#: participation/views.py:1811 msgid "You can't upload a synthesis after the deadline." msgstr "Vous ne pouvez pas envoyer de note de synthèse après la date limite." diff --git a/participation/templates/participation/participation_detail.html b/participation/templates/participation/participation_detail.html index 2ff3f59..e0d7757 100644 --- a/participation/templates/participation/participation_detail.html +++ b/participation/templates/participation/participation_detail.html @@ -30,6 +30,12 @@ {% empty %}
  • {% trans "No solution was uploaded yet." %}
  • {% endfor %} +
  • + + {% trans "Download as ZIP" %} + +
  • diff --git a/participation/templates/participation/pool_detail.html b/participation/templates/participation/pool_detail.html index 8f43fb3..773d65f 100644 --- a/participation/templates/participation/pool_detail.html +++ b/participation/templates/participation/pool_detail.html @@ -38,7 +38,7 @@ {% for passage in pool.passages.all %} {{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}{% if not forloop.last %}, {% endif %} {% endfor %} - + {% trans "Download all" %} @@ -57,7 +57,7 @@ {% endfor %} - + {% trans "Download all" %} diff --git a/participation/templates/participation/tournament_detail.html b/participation/templates/participation/tournament_detail.html index 901fc47..2b70576 100644 --- a/participation/templates/participation/tournament_detail.html +++ b/participation/templates/participation/tournament_detail.html @@ -175,17 +175,22 @@
  • - + Archive de toutes les solutions envoyées triées par équipe
  • - + Archive de toutes les solutions envoyées triées par problème
  • - + + Archive de toutes les solutions envoyées triées par poule + +
  • +
  • + Archive de toutes les notes de synthèse triées par poule et par passage
  • diff --git a/participation/urls.py b/participation/urls.py index d4d064a..a77032c 100644 --- a/participation/urls.py +++ b/participation/urls.py @@ -6,12 +6,12 @@ from django.views.generic import TemplateView from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \ MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \ - PassageUpdateView, PoolCreateView, PoolDetailView, PoolDownloadView, PoolJuryView, PoolNotesTemplateView, \ + PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \ PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, \ - ScaleNotationSheetTemplateView, SolutionUploadView, SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, \ - TeamLeaveView, TeamListView, TeamUpdateView, TeamUploadMotivationLetterView, TournamentCreateView, \ - TournamentDetailView, TournamentExportCSVView, TournamentListView, TournamentPaymentsView, \ - TournamentPublishNotesView, TournamentUpdateView + ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \ + TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \ + TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ + TournamentListView, TournamentPaymentsView, TournamentPublishNotesView, TournamentUpdateView app_name = "participation" @@ -30,6 +30,7 @@ urlpatterns = [ path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"), path("detail//", ParticipationDetailView.as_view(), name="participation_detail"), path("detail//solution/", SolutionUploadView.as_view(), name="upload_solution"), + path("detail//solutions/", SolutionsDownloadView.as_view(), name="participation_solutions"), path("tournament/", TournamentListView.as_view(), name="tournament_list"), path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"), path("tournament//", TournamentDetailView.as_view(), name="tournament_detail"), @@ -38,13 +39,17 @@ urlpatterns = [ path("tournament//csv/", TournamentExportCSVView.as_view(), name="tournament_csv"), path("tournament//authorizations/", TeamAuthorizationsView.as_view(), name="tournament_authorizations"), + path("tournament//solutions/", SolutionsDownloadView.as_view(), + name="tournament_solutions"), + path("tournament//syntheses/", SolutionsDownloadView.as_view(), + name="tournament_syntheses"), path("tournament//publish-notes//", TournamentPublishNotesView.as_view(), name="tournament_publish_notes"), path("pools/create/", PoolCreateView.as_view(), name="pool_create"), path("pools//", PoolDetailView.as_view(), name="pool_detail"), path("pools//update/", PoolUpdateView.as_view(), name="pool_update"), - path("pools//solutions/", PoolDownloadView.as_view(), name="pool_download_solutions"), - path("pools//syntheses/", PoolDownloadView.as_view(), name="pool_download_syntheses"), + path("pools//solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"), + path("pools//syntheses/", SolutionsDownloadView.as_view(), name="pool_download_syntheses"), path("pools//notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"), path("pools//notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"), path("pools//update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"), diff --git a/participation/views.py b/participation/views.py index e7320ab..25bed04 100644 --- a/participation/views.py +++ b/participation/views.py @@ -33,7 +33,7 @@ from odf.opendocument import OpenDocumentSpreadsheet from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow from odf.text import P -from registration.models import Payment, StudentRegistration, VolunteerRegistration +from registration.models import Payment, VolunteerRegistration from registration.tables import PaymentTable from tfjm.lists import get_sympa_client from tfjm.views import AdminMixin, VolunteerMixin @@ -819,38 +819,107 @@ class PoolUpdateTeamsView(VolunteerMixin, UpdateView): return self.handle_no_permission() -class PoolDownloadView(VolunteerMixin, DetailView): +class SolutionsDownloadView(VolunteerMixin, View): """ Download all solutions or syntheses as a ZIP archive. """ - model = Pool def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() + reg = request.user.registration - if reg.is_admin or reg.is_volunteer \ - and (self.get_object().tournament in reg.organized_tournaments.all() - or reg in self.get_object().juries.all() - or reg.pools_presided.filter(tournament=self.get_object().tournament).exists()): + if reg.is_admin: return super().dispatch(request, *args, **kwargs) + + if 'team_id' in kwargs: + team = Team.objects.get(pk=kwargs["team_id"]) + tournament = team.participation.tournament + if reg.participates and reg.team == team \ + or reg.is_volunteer and (reg in tournament.organizers.all() or team.participation.final + and reg in Tournament.final_tournament().organizers): + return super().dispatch(request, *args, **kwargs) + elif 'tournament_id' in kwargs: + tournament = Tournament.objects.get(pk=kwargs["tournament_id"]) + if reg.is_volunteer \ + and (tournament in reg.organized_tournaments.all() + or reg.pools_presided.filter(tournament=tournament).exists()): + return super().dispatch(request, *args, **kwargs) + else: + pool = Pool.objects.get(pk=kwargs["pool_id"]) + tournament = pool.tournament + if reg.is_volunteer \ + and (reg in tournament.organizers.all() + or reg in pool.juries.all() + or reg.pools_presided.filter(tournament=tournament).exists()): + return super().dispatch(request, *args, **kwargs) + return self.handle_no_permission() def get(self, request, *args, **kwargs): - pool = self.get_object() - is_solution = 'solutions' in request.path + if 'team_id' in kwargs: + team = Team.objects.get(pk=kwargs["team_id"]) + solutions = Solution.objects.filter(participation=team.participation).all() + syntheses = Synthesis.objects.filter(participation=team.participation).all() + filename = _("Solutions of team {trigram}.zip") if is_solution else _("Syntheses of team {trigram}.zip") + filename = filename.format(trigram=team.trigram) + + def prefix(s: Solution | Synthesis) -> str: + return "" + elif 'tournament_id' in kwargs: + tournament = Tournament.objects.get(pk=kwargs["tournament_id"]) + sort_by = request.GET.get('sort_by', 'team').lower() + + if sort_by == 'pool': + pools = Pool.objects.filter(tournament=tournament).all() + solutions = [] + for pool in pools: + for sol in pool.solutions: + sol.pool = pool + solutions.append(sol) + syntheses = Synthesis.objects.filter(passage__pool__tournament=tournament).all() + filename = _("Solutions of {tournament}.zip") if is_solution else _("Syntheses of {tournament}.zip") + filename = filename.format(tournament=tournament.name) + + def prefix(s: Solution | Synthesis) -> str: + pool = s.pool if is_solution else s.passage.pool + p = f"Poule {pool.get_letter_display()}{pool.round}/" + if not is_solution: + p += f"Passage {s.passage.position}/" + return p + else: + if not tournament.final: + solutions = Solution.objects.filter(participation__tournament=tournament).all() + else: + solutions = Solution.objects.filter(final_solution=True).all() + syntheses = Synthesis.objects.filter(passage__pool__tournament=tournament).all() + filename = _("Solutions of {tournament}.zip") if is_solution else _("Syntheses of {tournament}.zip") + filename = filename.format(tournament=tournament.name) + + def prefix(s: Solution | Synthesis) -> str: + return f"{s.participation.team.trigram}/" if sort_by == "team" else f"Problème {s.problem}/" + else: + pool = Pool.objects.get(pk=kwargs["pool_id"]) + solutions = pool.solutions + syntheses = Synthesis.objects.filter(passage__pool=pool).all() + filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \ + if is_solution else _("Syntheses for pool {pool} of tournament {tournament}.zip") + filename = filename.format(pool=pool.get_letter_display() + str(pool.round), + tournament=pool.tournament.name) + + def prefix(s: Solution | Synthesis) -> str: + return "" + output = BytesIO() zf = ZipFile(output, "w") - for s in (pool.solutions if is_solution else Synthesis.objects.filter(passage__pool=pool).all()): - zf.write("media/" + s.file.name, f"{s}.pdf") + for s in (solutions if is_solution else syntheses): + if s.file.storage.exists(s.file.path): + zf.write("media/" + s.file.name, prefix(s) + f"{s}.pdf") zf.close() response = HttpResponse(content_type="application/zip") - filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \ - if is_solution else _("Syntheses for pool {pool} of tournament {tournament}.zip") - filename = filename.format(pool=pool.get_letter_display() + str(pool.round), tournament=pool.tournament.name) response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \ .format(filename=filename) response.write(output.getvalue())