Add ZIP archive for tournament solutions

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2024-03-27 00:49:32 +01:00
parent 4583cf46b1
commit 5084bb65d9
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
6 changed files with 152 additions and 43 deletions

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TFJM\n" "Project-Id-Version: TFJM\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n" "Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -1150,23 +1150,27 @@ msgstr "Solutions :"
msgid "No solution was uploaded yet." msgid "No solution was uploaded yet."
msgstr "Aucune solution n'a encore été envoyée." 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:" msgid "Pools:"
msgstr "Poules :" msgstr "Poules :"
#: participation/templates/participation/participation_detail.html:51 #: participation/templates/participation/participation_detail.html:57
msgid "" msgid ""
"If you upload a solution, this will replace the version for the final " "If you upload a solution, this will replace the version for the final "
"tournament." "tournament."
msgstr "" msgstr ""
"Si vous envoyez une solution, elle va remplacer la version pour la finale." "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:60
#: participation/templates/participation/participation_detail.html:58 #: participation/templates/participation/participation_detail.html:64
msgid "Upload solution" msgid "Upload solution"
msgstr "Envoyer une 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/passage_detail.html:132
#: participation/templates/participation/pool_detail.html:138 #: participation/templates/participation/pool_detail.html:138
#: participation/templates/participation/team_detail.html:210 #: 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." msgid "You can't upload a solution after the deadline."
msgstr "Vous ne pouvez pas envoyer de solution après la date limite." 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 #, python-brace-format
msgid "Solutions for pool {pool} of tournament {tournament}.zip" msgid "Solutions for pool {pool} of tournament {tournament}.zip"
msgstr "Solutions pour la poule {pool} du tournoi {tournament}.zip" msgstr "Solutions pour la poule {pool} du tournoi {tournament}.zip"
#: participation/views.py:852 #: participation/views.py:908
#, python-brace-format #, python-brace-format
msgid "Syntheses for pool {pool} of tournament {tournament}.zip" msgid "Syntheses for pool {pool} of tournament {tournament}.zip"
msgstr "Notes de synthèses pour la poule {pool} du tournoi {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 #, python-brace-format
msgid "Jury of pool {pool} for {tournament} with teams {teams}" msgid "Jury of pool {pool} for {tournament} with teams {teams}"
msgstr "Jury de la poule {pool} pour {tournament} avec les équipes {teams}" msgstr "Jury de la poule {pool} pour {tournament} avec les équipes {teams}"
#: participation/views.py:897 #: participation/views.py:966
#, python-brace-format #, python-brace-format
msgid "The jury {name} is already in the pool!" msgid "The jury {name} is already in the pool!"
msgstr "{name} est déjà dans la poule !" msgstr "{name} est déjà dans la poule !"
#: participation/views.py:917 #: participation/views.py:986
msgid "New TFJM² jury account" msgid "New TFJM² jury account"
msgstr "Nouveau compte de juré⋅e pour le TFJM²" msgstr "Nouveau compte de juré⋅e pour le TFJM²"
#: participation/views.py:934 #: participation/views.py:1003
#, python-brace-format #, python-brace-format
msgid "The jury {name} has been successfully added!" msgid "The jury {name} has been successfully added!"
msgstr "{name} a été ajouté⋅e avec succès en tant que juré⋅e !" 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 #, python-brace-format
msgid "The jury {name} has been successfully removed!" msgid "The jury {name} has been successfully removed!"
msgstr "{name} a été retiré⋅e avec succès du jury !" msgstr "{name} a été retiré⋅e avec succès du jury !"
#: participation/views.py:995 #: participation/views.py:1064
#, python-brace-format #, python-brace-format
msgid "The jury {name} has been successfully promoted president!" msgid "The jury {name} has been successfully promoted president!"
msgstr "{name} a été nommé⋅e président⋅e du jury !" 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:" 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 :" 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." msgid "Notes were successfully uploaded."
msgstr "Les notes ont bien été envoyées." 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." 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." msgstr "Vous ne pouvez pas envoyer de note de synthèse après la date limite."

View File

@ -30,6 +30,12 @@
{% empty %} {% empty %}
<li>{% trans "No solution was uploaded yet." %}</li> <li>{% trans "No solution was uploaded yet." %}</li>
{% endfor %} {% endfor %}
<li>
<a href="{% url "participation:participation_solutions" team_id=participation.team_id %}"
class="btn btn-sm btn-info">
<i class="fas fa-archive"></i> {% trans "Download as ZIP" %}
</a>
</li>
</ul> </ul>
</dd> </dd>

View File

@ -38,7 +38,7 @@
{% for passage in pool.passages.all %} {% for passage in pool.passages.all %}
<a href="{{ passage.defended_solution.file.url }}">{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %} <a href="{{ passage.defended_solution.file.url }}">{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
<a href="{% url 'participation:pool_download_solutions' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary"> <a href="{% url 'participation:pool_download_solutions' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
<i class="fas fa-download"></i> {% trans "Download all" %} <i class="fas fa-download"></i> {% trans "Download all" %}
</a> </a>
</dd> </dd>
@ -57,7 +57,7 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<a href="{% url 'participation:pool_download_syntheses' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary"> <a href="{% url 'participation:pool_download_syntheses' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
<i class="fas fa-download"></i> {% trans "Download all" %} <i class="fas fa-download"></i> {% trans "Download all" %}
</a> </a>
</dd> </dd>

View File

@ -175,17 +175,22 @@
</a> </a>
</li> </li>
<li> <li>
<a> <a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}">
Archive de toutes les solutions envoyées triées par équipe Archive de toutes les solutions envoyées triées par équipe
</a> </a>
</li> </li>
<li> <li>
<a> <a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=problem">
Archive de toutes les solutions envoyées triées par problème Archive de toutes les solutions envoyées triées par problème
</a> </a>
</li> </li>
<li> <li>
<a> <a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=pool">
Archive de toutes les solutions envoyées triées par poule
</a>
</li>
<li>
<a href="{% url "participation:tournament_syntheses" tournament_id=tournament.id %}?sort_by=pool">
Archive de toutes les notes de synthèse triées par poule et par passage Archive de toutes les notes de synthèse triées par poule et par passage
</a> </a>
</li> </li>

View File

@ -6,12 +6,12 @@ from django.views.generic import TemplateView
from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \ from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \
MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \ MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \
PassageUpdateView, PoolCreateView, PoolDetailView, PoolDownloadView, PoolJuryView, PoolNotesTemplateView, \ PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, \ PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, \
ScaleNotationSheetTemplateView, SolutionUploadView, SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, \ ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
TeamLeaveView, TeamListView, TeamUpdateView, TeamUploadMotivationLetterView, TournamentCreateView, \ TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TournamentDetailView, TournamentExportCSVView, TournamentListView, TournamentPaymentsView, \ TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentPublishNotesView, TournamentUpdateView TournamentListView, TournamentPaymentsView, TournamentPublishNotesView, TournamentUpdateView
app_name = "participation" app_name = "participation"
@ -30,6 +30,7 @@ urlpatterns = [
path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"), path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"),
path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="participation_detail"), path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="participation_detail"),
path("detail/<int:pk>/solution/", SolutionUploadView.as_view(), name="upload_solution"), path("detail/<int:pk>/solution/", SolutionUploadView.as_view(), name="upload_solution"),
path("detail/<int:team_id>/solutions/", SolutionsDownloadView.as_view(), name="participation_solutions"),
path("tournament/", TournamentListView.as_view(), name="tournament_list"), path("tournament/", TournamentListView.as_view(), name="tournament_list"),
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"), path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"), path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"),
@ -38,13 +39,17 @@ urlpatterns = [
path("tournament/<int:pk>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"), path("tournament/<int:pk>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"),
path("tournament/<int:tournament_id>/authorizations/", TeamAuthorizationsView.as_view(), path("tournament/<int:tournament_id>/authorizations/", TeamAuthorizationsView.as_view(),
name="tournament_authorizations"), name="tournament_authorizations"),
path("tournament/<int:tournament_id>/solutions/", SolutionsDownloadView.as_view(),
name="tournament_solutions"),
path("tournament/<int:tournament_id>/syntheses/", SolutionsDownloadView.as_view(),
name="tournament_syntheses"),
path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(), path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
name="tournament_publish_notes"), name="tournament_publish_notes"),
path("pools/create/", PoolCreateView.as_view(), name="pool_create"), path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"), path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"), path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
path("pools/<int:pk>/solutions/", PoolDownloadView.as_view(), name="pool_download_solutions"), path("pools/<int:pool_id>/solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"),
path("pools/<int:pk>/syntheses/", PoolDownloadView.as_view(), name="pool_download_syntheses"), path("pools/<int:pool_id>/syntheses/", SolutionsDownloadView.as_view(), name="pool_download_syntheses"),
path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"), path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"),
path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"), path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"),
path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"), path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),

View File

@ -33,7 +33,7 @@ from odf.opendocument import OpenDocumentSpreadsheet
from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties
from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
from odf.text import P from odf.text import P
from registration.models import Payment, StudentRegistration, VolunteerRegistration from registration.models import Payment, VolunteerRegistration
from registration.tables import PaymentTable from registration.tables import PaymentTable
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
from tfjm.views import AdminMixin, VolunteerMixin from tfjm.views import AdminMixin, VolunteerMixin
@ -819,38 +819,107 @@ class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
return self.handle_no_permission() return self.handle_no_permission()
class PoolDownloadView(VolunteerMixin, DetailView): class SolutionsDownloadView(VolunteerMixin, View):
""" """
Download all solutions or syntheses as a ZIP archive. Download all solutions or syntheses as a ZIP archive.
""" """
model = Pool
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return self.handle_no_permission() return self.handle_no_permission()
reg = request.user.registration reg = request.user.registration
if reg.is_admin or reg.is_volunteer \ if reg.is_admin:
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()):
return super().dispatch(request, *args, **kwargs) 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() return self.handle_no_permission()
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
pool = self.get_object()
is_solution = 'solutions' in request.path 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() output = BytesIO()
zf = ZipFile(output, "w") zf = ZipFile(output, "w")
for s in (pool.solutions if is_solution else Synthesis.objects.filter(passage__pool=pool).all()): for s in (solutions if is_solution else syntheses):
zf.write("media/" + s.file.name, f"{s}.pdf") if s.file.storage.exists(s.file.path):
zf.write("media/" + s.file.name, prefix(s) + f"{s}.pdf")
zf.close() zf.close()
response = HttpResponse(content_type="application/zip") 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}\"" \ response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
.format(filename=filename) .format(filename=filename)
response.write(output.getvalue()) response.write(output.getvalue())