Add harmonization view

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2024-03-30 20:38:13 +01:00
parent 109b603b7a
commit 1e7bd209a1
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
6 changed files with 238 additions and 68 deletions

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: TFJM\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-30 19:13+0100\n"
"POT-Creation-Date: 2024-03-30 20:33+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -333,6 +333,7 @@ msgstr "Continuer le tirage"
#: draw/templates/draw/tournament_content.html:216 participation/admin.py:167
#: participation/models.py:253 participation/models.py:653
#: participation/templates/participation/tournament_harmonize.html:15
#: registration/models.py:156 registration/models.py:609
#: registration/tables.py:39
#: registration/templates/registration/payment_form.html:52
@ -377,8 +378,8 @@ msgstr "Êtes-vous sûr·e de vouloir annuler le tirage au sort ?"
msgid "Close"
msgstr "Fermer"
#: draw/views.py:31 participation/views.py:155 participation/views.py:469
#: participation/views.py:500
#: draw/views.py:31 participation/views.py:156 participation/views.py:470
#: participation/views.py:501
msgid "You are not in a team."
msgstr "Vous n'êtes pas dans une équipe."
@ -490,7 +491,7 @@ msgstr "Ce trigramme est déjà utilisé."
msgid "No team was found with this access code."
msgstr "Aucune équipe n'a été trouvée avec ce code d'accès."
#: participation/forms.py:59 participation/views.py:471
#: participation/forms.py:59 participation/views.py:472
msgid "The team is already validated or the validation is pending."
msgstr "La validation de l'équipe est déjà faite ou en cours."
@ -1355,6 +1356,7 @@ msgstr "Aller à la page Google Sheets de la poule"
#: participation/templates/participation/pool_detail.html:83
#: participation/templates/participation/tournament_detail.html:98
#: participation/templates/participation/tournament_harmonize.html:8
msgid "Ranking"
msgstr "Classement"
@ -1582,7 +1584,7 @@ msgid "Invalidate"
msgstr "Invalider"
#: participation/templates/participation/team_detail.html:209
#: participation/views.py:326
#: participation/views.py:327
msgid "Upload motivation letter"
msgstr "Envoyer la lettre de motivation"
@ -1591,7 +1593,7 @@ msgid "Update team"
msgstr "Modifier l'équipe"
#: participation/templates/participation/team_detail.html:219
#: participation/views.py:463
#: participation/views.py:464
msgid "Leave team"
msgstr "Quitter l'équipe"
@ -1685,10 +1687,41 @@ msgstr "Publier les notes pour le premier tour"
msgid "Publish notes for second round"
msgstr "Publier les notes pour le second tour"
#: participation/templates/participation/tournament_detail.html:133
#: participation/templates/participation/tournament_detail.html:128
#: participation/templates/participation/tournament_detail.html:132
msgid "Harmonize"
msgstr "Harmoniser"
#: participation/templates/participation/tournament_detail.html:128
#: participation/templates/participation/tournament_detail.html:132
msgid "Day"
msgstr "Jour"
#: participation/templates/participation/tournament_detail.html:143
msgid "Files available for download"
msgstr "Fichiers disponibles au téléchargement"
#: participation/templates/participation/tournament_harmonize.html:14
msgid "Rank"
msgstr "Rang"
#: participation/templates/participation/tournament_harmonize.html:16
msgid "Note"
msgstr "Note"
#: participation/templates/participation/tournament_harmonize.html:17
msgid "Including bonus / malus"
msgstr "Dont bonus / malus"
#: participation/templates/participation/tournament_harmonize.html:18
msgid "Add bonus / malus"
msgstr "Ajouter bonus / malus"
#: participation/templates/participation/tournament_harmonize.html:48
#: participation/templates/participation/tournament_payments.html:9
msgid "Back to tournament page"
msgstr "Retour à la page du tournoi"
#: participation/templates/participation/tournament_list.html:6
#: tfjm/templates/base.html:67
msgid "All tournaments"
@ -1698,10 +1731,6 @@ msgstr "Tous les tournois"
msgid "Add tournament"
msgstr "Ajouter un tournoi"
#: participation/templates/participation/tournament_payments.html:9
msgid "Back to tournament page"
msgstr "Retour à la page du tournoi"
#: participation/templates/participation/upload_motivation_letter.html:6
msgid "Back to the team detail"
msgstr "Retour aux détails de l'utilisateur⋅rice"
@ -1718,44 +1747,44 @@ msgstr "Modèles :"
msgid "Warning: non-free format"
msgstr "Attention : format non libre"
#: participation/views.py:55 tfjm/templates/base.html:79
#: participation/views.py:56 tfjm/templates/base.html:79
#: tfjm/templates/navbar.html:35
msgid "Create team"
msgstr "Créer une équipe"
#: participation/views.py:64 participation/views.py:105
#: participation/views.py:65 participation/views.py:106
msgid "You don't participate, so you can't create a team."
msgstr "Vous ne participez pas, vous ne pouvez pas créer d'équipe."
#: participation/views.py:66 participation/views.py:107
#: participation/views.py:67 participation/views.py:108
msgid "You are already in a team."
msgstr "Vous êtes déjà dans une équipe."
#: participation/views.py:96 tfjm/templates/base.html:74
#: participation/views.py:97 tfjm/templates/base.html:74
#: tfjm/templates/navbar.html:40
msgid "Join team"
msgstr "Rejoindre une équipe"
#: participation/views.py:156 participation/views.py:501
#: participation/views.py:157 participation/views.py:502
msgid "You don't participate, so you don't have any team."
msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe."
#: participation/views.py:182
#: participation/views.py:183
#, python-brace-format
msgid "Detail of team {trigram}"
msgstr "Détails de l'équipe {trigram}"
#: participation/views.py:211
#: participation/views.py:212
msgid "You don't participate, so you can't request the validation of the team."
msgstr ""
"Vous ne participez pas, vous ne pouvez pas demander la validation de "
"l'équipe."
#: participation/views.py:214
#: participation/views.py:215
msgid "The validation of the team is already done or pending."
msgstr "La validation de l'équipe est déjà faite ou en cours."
#: participation/views.py:217
#: participation/views.py:218
msgid ""
"The team can't be validated: missing email address confirmations, "
"authorizations, people, motivation letter or the tournament is not set."
@ -1764,158 +1793,163 @@ msgstr ""
"d'adresse e-mail, soit une autorisation, soit des personnes, soit la lettre "
"de motivation, soit le tournoi n'a pas été choisi."
#: participation/views.py:239
#: participation/views.py:240
msgid "You are not an organizer of the tournament."
msgstr "Vous n'êtes pas un⋅e organisateur⋅rice du tournoi."
#: participation/views.py:242
#: participation/views.py:243
msgid "This team has no pending validation."
msgstr "L'équipe n'a pas de validation en attente."
#: participation/views.py:269
#: participation/views.py:270
msgid "You must specify if you validate the registration or not."
msgstr "Vous devez spécifier si vous validez l'inscription ou non."
#: participation/views.py:304
#: participation/views.py:305
#, python-brace-format
msgid "Update team {trigram}"
msgstr "Mise à jour de l'équipe {trigram}"
#: participation/views.py:365 participation/views.py:448
#: participation/views.py:366 participation/views.py:449
#, python-brace-format
msgid "Motivation letter of {team}.{ext}"
msgstr "Lettre de motivation de {team}.{ext}"
#: participation/views.py:397
#: participation/views.py:398
#, python-brace-format
msgid "Authorizations of team {trigram}.zip"
msgstr "Autorisations de l'équipe {trigram}.zip"
#: participation/views.py:401
#: participation/views.py:402
#, python-brace-format
msgid "Authorizations of {tournament}.zip"
msgstr "Autorisations du tournoi {tournament}.zip"
#: participation/views.py:417
#: participation/views.py:418
#, python-brace-format
msgid "Photo authorization of {participant}.{ext}"
msgstr "Autorisation de droit à l'image de {participant}.{ext}"
#: participation/views.py:425
#: participation/views.py:426
#, python-brace-format
msgid "Parental authorization of {participant}.{ext}"
msgstr "Autorisation parentale de {participant}.{ext}"
#: participation/views.py:433
#: participation/views.py:434
#, python-brace-format
msgid "Health sheet of {participant}.{ext}"
msgstr "Fiche sanitaire de {participant}.{ext}"
#: participation/views.py:441
#: participation/views.py:442
#, python-brace-format
msgid "Vaccine sheet of {participant}.{ext}"
msgstr "Carnet de vaccination de {participant}.{ext}"
#: participation/views.py:515
#: participation/views.py:516
msgid "The team is not validated yet."
msgstr "L'équipe n'est pas encore validée."
#: participation/views.py:529
#: participation/views.py:530
#, python-brace-format
msgid "Participation of team {trigram}"
msgstr "Participation de l'équipe {trigram}"
#: participation/views.py:611
#: participation/views.py:612
#, python-brace-format
msgid "Payments of {tournament}"
msgstr "Paiements de {tournament}"
#: participation/views.py:706
#: participation/views.py:707
msgid "Notes published!"
msgstr "Notes publiées !"
#: participation/views.py:742
#: participation/views.py:738
#, python-brace-format
msgid "Harmonize notes of {tournament} - Day {round}"
msgstr "Harmoniser les notes de {tournament} - Jour {round}"
#: participation/views.py:818
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:866
#: participation/views.py:942
#, python-brace-format
msgid "Solutions of team {trigram}.zip"
msgstr "Solutions de l'équipe {trigram}.zip"
#: participation/views.py:866
#: participation/views.py:942
#, 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
#: participation/views.py:959 participation/views.py:974
#, python-brace-format
msgid "Solutions of {tournament}.zip"
msgstr "Solutions de {tournament}.zip"
#: participation/views.py:883 participation/views.py:898
#: participation/views.py:959 participation/views.py:974
#, python-brace-format
msgid "Syntheses of {tournament}.zip"
msgstr "Notes de synthèse de {tournament}.zip"
#: participation/views.py:907
#: participation/views.py:983
#, 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:908
#: participation/views.py:984
#, 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:950
#: participation/views.py:1026
#, 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:966
#: participation/views.py:1042
#, python-brace-format
msgid "The jury {name} is already in the pool!"
msgstr "{name} est déjà dans la poule !"
#: participation/views.py:986
#: participation/views.py:1062
msgid "New TFJM² jury account"
msgstr "Nouveau compte de juré⋅e pour le TFJM²"
#: participation/views.py:1003
#: participation/views.py:1079
#, 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:1039
#: participation/views.py:1115
#, 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:1065
#: participation/views.py:1141
#, 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:1093
#: participation/views.py:1169
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:1107
#: participation/views.py:1183
msgid "Notes were successfully uploaded."
msgstr "Les notes ont bien été envoyées."
#: participation/views.py:1740
#: participation/views.py:1816
#, python-brace-format
msgid "Notation sheets of pool {pool} of {tournament}.zip"
msgstr "Feuilles de notations pour la poule {pool} du tournoi {tournament}.zip"
#: participation/views.py:1745
#: participation/views.py:1821
#, python-brace-format
msgid "Notation sheets of {tournament}.zip"
msgstr "Feuilles de notation de {tournament}.zip"
#: participation/views.py:1924
#: participation/views.py:2000
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."
@ -3385,12 +3419,3 @@ msgstr "Aucun résultat."
#: tfjm/templates/sidebar.html:10 tfjm/templates/sidebar.html:21
msgid "Informations"
msgstr "Informations"
#~ msgid "Export as CSV"
#~ msgstr "Exporter en CSV"
#~ msgid "Add new pool"
#~ msgstr "Ajouter une nouvelle poule"
#~ msgid "Add pool"
#~ msgstr "Ajouter une poule"

View File

@ -201,4 +201,6 @@ class TournamentAdmin(admin.ModelAdmin):
@admin.register(Tweak)
class TweakAdmin(admin.ModelAdmin):
list_display = ('participation', 'pool', 'diff',)
list_filter = ('pool__tournament', 'pool__round',)
search_fields = ('participation__team__name', 'participation__team__trigram',)
autocomplete_fields = ('participation', 'pool',)

View File

@ -104,9 +104,9 @@
{% endfor %}
</ul>
</div>
{% if not available_notes_1 or not available_notes_2 %}
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
<div class="card-footer text-center">
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
<div class="card-footer text-center">
{% if not available_notes_1 or not available_notes_2 %}
<div class="btn-group">
{% if not available_notes_1 %}
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=1 %}" class="btn btn-info">
@ -121,8 +121,18 @@
</a>
{% endif %}
</div>
{% endif %}
<div class="btn-group">
<a href="{% url 'participation:tournament_harmonize' pk=tournament.pk round=1 %}" class="btn btn-secondary">
<i class="fas fa-ranking-star"></i>
{% trans "Harmonize" %} - {% trans "Day" %} 1
</a>
<a href="{% url 'participation:tournament_harmonize' pk=tournament.pk round=2 %}" class="btn btn-secondary">
<i class="fas fa-ranking-star"></i>
{% trans "Harmonize" %} - {% trans "Day" %} 2
</a>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}

View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h5>{% trans "Ranking" %}</h5>
</div>
<div class="card-body">
<table class="table table-striped text-center">
<thead>
<tr>
<th>{% trans "Rank" %}</th>
<th>{% trans "team"|capfirst %}</th>
<th>{% trans "Note" %}</th>
<th>{% trans "Including bonus / malus" %}</th>
<th>{% trans "Add bonus / malus" %}</th>
</tr>
</thead>
<tbody>
{% for participation, note in notes %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ participation.team }}</td>
<td>{{ note.note|floatformat }}</td>
<td>{% if note.tweak >= 0 %}+{% endif %}{{ note.tweak }}</td>
<td>
<div class="btn-group">
<a href="{% url 'participation:tournament_harmonize_note' pk=tournament.pk round=round action="add" trigram=participation.team.trigram %}"
class="btn btn-sm btn-success">
+1
</a>
<a href="{% url 'participation:tournament_harmonize_note' pk=tournament.pk round=round action="remove" trigram=participation.team.trigram %}"
class="btn btn-sm btn-danger">
-1
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer text-center">
<a class="btn btn-secondary" href="{% url 'participation:tournament_detail' pk=tournament.pk %}">
<i class="fas fa-arrow-left-long"></i>
{% trans "Back to tournament page" %}
</a>
</div>
</div>
{% endblock %}

View File

@ -11,7 +11,8 @@ from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView,
ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentPaymentsView, TournamentPublishNotesView, TournamentUpdateView
TournamentHarmonizeView, TournamentHarmonizeNoteView, TournamentListView, TournamentPaymentsView, \
TournamentPublishNotesView, TournamentUpdateView
app_name = "participation"
@ -47,6 +48,10 @@ urlpatterns = [
name="tournament_notation_sheets"),
path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
name="tournament_publish_notes"),
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),
name="tournament_harmonize"),
path("tournament/<int:pk>/harmonize/<int:round>/<str:action>/<str:trigram>/", TournamentHarmonizeNoteView.as_view(),
name="tournament_harmonize_note"),
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),

View File

@ -9,6 +9,7 @@ from tempfile import mkdtemp
from typing import Any, Dict
from zipfile import ZipFile
import gspread
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
@ -41,7 +42,7 @@ from tfjm.views import AdminMixin, VolunteerMixin
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
PoolForm, PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
UploadNotesForm, ValidateParticipationForm
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
@ -710,6 +711,81 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView
return reverse_lazy("participation:tournament_detail", args=(kwargs['pk'],))
class TournamentHarmonizeView(VolunteerMixin, DetailView):
"""
Harmonize the notes of a tournament.
"""
model = Tournament
template_name = "participation/tournament_harmonize.html"
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
tournament = self.get_object()
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
if self.kwargs['round'] not in (1, 2):
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tournament = self.get_object()
context['round'] = self.kwargs['round']
context['pools'] = tournament.pools.filter(round=context["round"]).all()
context['title'] = _("Harmonize notes of {tournament} - Day {round}") \
.format(tournament=tournament, round=context["round"])
notes = dict()
for participation in self.object.participations.all():
note = sum(pool.average(participation) for pool in context['pools'])
tweak = sum(tweak.diff for tweak in participation.tweaks.filter(pool__in=context['pools']).all())
notes[participation] = {'note': note, 'tweak': tweak}
context["notes"] = sorted(notes.items(), key=lambda x: x[1]['note'], reverse=True)
return context
class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
model = Tournament
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
tournament = self.get_object()
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
if self.kwargs['round'] not in (1, 2) or self.kwargs['action'] not in ('add', 'remove') \
or self.kwargs['trigram'] not in [p.team.trigram for p in tournament.participations.all()]:
raise Http404
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
tournament = self.get_object()
participation = tournament.participations.get(team__trigram=kwargs['trigram'])
pool = tournament.pools.get(round=kwargs['round'], participations=participation)
tweak_qs = Tweak.objects.filter(participation=participation, pool=pool)
old_diff = tweak_qs.first().diff if tweak_qs.exists() else 0
new_diff = old_diff + (1 if kwargs['action'] == 'add' else -1)
if new_diff == 0:
tweak_qs.delete()
else:
tweak_qs.update_or_create(defaults={'diff': new_diff},
create_defaults={'diff': new_diff, 'participation': participation, 'pool': pool})
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet("Classement final")
column = 3 if kwargs['round'] == '1' else 5
row = worksheet.find(f"{participation.team.name} ({participation.team.trigram})", in_column=1).row
worksheet.update_cell(row, column, new_diff)
return redirect(reverse_lazy("participation:tournament_harmonize", args=(tournament.pk, kwargs['round'],)))
class SolutionUploadView(LoginRequiredMixin, FormView):
template_name = "participation/upload_solution.html"
form_class = SolutionForm