diff --git a/participation/forms.py b/participation/forms.py index 5691971..0a3f0f9 100644 --- a/participation/forms.py +++ b/participation/forms.py @@ -288,7 +288,7 @@ class UploadNotesForm(forms.Form): continue name = line[0] - if name in ["Rôle", "Juré", "moyenne", "coefficient", "sous-total", "Equipe"]: + if name.lower() in ["rôle", "juré", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]: continue notes = line[1:line_length] if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes): diff --git a/participation/models.py b/participation/models.py index 41113ef..88434c0 100644 --- a/participation/models.py +++ b/participation/models.py @@ -534,6 +534,17 @@ class Passage(models.Model): def average_observer(self) -> float: return self.avg(note.observer_oral for note in self.notes.all()) + @property + def averages(self): + yield self.average_defender_writing + yield self.average_defender_oral + yield self.average_opponent_writing + yield self.average_opponent_oral + yield self.average_reporter_writing + yield self.average_reporter_oral + if self.observer: + yield self.average_observer + def average(self, participation): return self.average_defender if participation == self.defender else self.average_opponent \ if participation == self.opponent else self.average_reporter if participation == self.reporter \ @@ -740,6 +751,16 @@ class Note(models.Model): default=0, ) + def get_all(self): + yield self.defender_writing + yield self.defender_oral + yield self.opponent_writing + yield self.opponent_oral + yield self.reporter_writing + yield self.reporter_oral + if self.passage.observer: + yield self.observer_oral + def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int, reporter_writing: int, reporter_oral: int, observer_oral: int = 0): self.defender_writing = defender_writing diff --git a/participation/static/Fiche notations - 3 équipes.ods b/participation/static/Fiche notations - 3 équipes.ods deleted file mode 100644 index 11afeb9..0000000 Binary files a/participation/static/Fiche notations - 3 équipes.ods and /dev/null differ diff --git a/participation/static/Fiche notations - 4 équipes.ods b/participation/static/Fiche notations - 4 équipes.ods deleted file mode 100644 index acd1367..0000000 Binary files a/participation/static/Fiche notations - 4 équipes.ods and /dev/null differ diff --git a/participation/static/Fiche notations - 5 équipes.ods b/participation/static/Fiche notations - 5 équipes.ods deleted file mode 100644 index 3bfd357..0000000 Binary files a/participation/static/Fiche notations - 5 équipes.ods and /dev/null differ diff --git a/participation/templates/participation/upload_notes.html b/participation/templates/participation/upload_notes.html index 8a0e3f6..a38c0b0 100644 --- a/participation/templates/participation/upload_notes.html +++ b/participation/templates/participation/upload_notes.html @@ -2,20 +2,13 @@ {% load crispy_forms_tags %} {% load i18n %} -{% load static %} {% block content %}
{% csrf_token %} diff --git a/participation/urls.py b/participation/urls.py index 4ad4b13..0e01cf6 100644 --- a/participation/urls.py +++ b/participation/urls.py @@ -6,8 +6,8 @@ from django.views.generic import TemplateView from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \ MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \ - PassageUpdateView, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolUpdateTeamsView, PoolUpdateView, \ - PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, SynthesisUploadView,\ + PassageUpdateView, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolNotesTemplateView, PoolUpdateTeamsView, \ + PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, SynthesisUploadView, \ TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \ TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ TournamentListView, TournamentUpdateView @@ -42,6 +42,7 @@ urlpatterns = [ path("pools//update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"), path("pools//add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"), path("pools//upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"), + path("pools//upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"), path("pools/passages/add//", PassageCreateView.as_view(), name="passage_create"), path("pools/passages//", PassageDetailView.as_view(), name="passage_detail"), path("pools/passages//update/", PassageUpdateView.as_view(), name="passage_update"), diff --git a/participation/views.py b/participation/views.py index 72cc994..523d988 100644 --- a/participation/views.py +++ b/participation/views.py @@ -26,6 +26,10 @@ from django.views.generic import CreateView, DetailView, FormView, RedirectView, from django.views.generic.edit import FormMixin, ProcessFormView from django_tables2 import SingleTableView from magic import Magic +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 StudentRegistration, VolunteerRegistration from tfjm.lists import get_sympa_client from tfjm.matrix import Matrix @@ -824,6 +828,509 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView): return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],)) +class PoolNotesTemplateView(VolunteerMixin, DetailView): + """ + Generate an ODS sheet to fill the notes of the pool. + """ + model = Pool + + def render_to_response(self, context, **response_kwargs): # noqa: C901 + pool_size = self.object.passages.count() + passage_width = 7 if pool_size == 4 else 6 + line_length = pool_size * passage_width + + def getcol(number: int) -> str: + """ + Translates the given number to the nth column name + """ + if number == 0: + return '' + return getcol((number - 1) // 26) + chr(65 + (number - 1) % 26) + + doc = OpenDocumentSpreadsheet() + + # Define styles + style = Style(name="Contenu", family="table-cell") + style.addElement(TableCellProperties(border="0.75pt solid #000000")) + doc.styles.addElement(style) + + style_left = Style(name="Contenu gauche", family="table-cell") + style_left.addElement(TableCellProperties(border="0.75pt solid #000000", borderleft="2pt solid #000000")) + doc.styles.addElement(style_left) + + style_right = Style(name="Contenu droite", family="table-cell") + style_right.addElement(TableCellProperties(border="0.75pt solid #000000", borderright="2pt solid #000000")) + doc.styles.addElement(style_right) + + style_top = Style(name="Contenu haut", family="table-cell") + style_top.addElement(TableCellProperties(border="0.75pt solid #000000", bordertop="2pt solid #000000")) + doc.styles.addElement(style_top) + + style_topright = Style(name="Contenu haut droite", family="table-cell") + style_topright.addElement(TableCellProperties(border="0.75pt solid #000000", + borderright="2pt solid #000000", + bordertop="2pt solid #000000")) + doc.styles.addElement(style_topright) + + style_topleftright = Style(name="Contenu haut gauche droite", family="table-cell") + style_topleftright.addElement(TableCellProperties(border="0.75pt solid #000000", + borderleft="2pt solid #000000", + borderright="2pt solid #000000", + bordertop="2pt solid #000000")) + doc.styles.addElement(style_topleftright) + + style_leftright = Style(name="Contenu haut gauche droite", family="table-cell") + style_leftright.addElement(TableCellProperties(border="0.75pt solid #000000", + borderleft="2pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(style_leftright) + + style_botleft = Style(name="Contenu bas gauche", family="table-cell") + style_botleft.addElement(TableCellProperties(border="0.75pt solid #000000", + borderbottom="2pt solid #000000", + borderleft="2pt solid #000000")) + doc.styles.addElement(style_botleft) + + style_bot = Style(name="Contenu bas", family="table-cell") + style_bot.addElement(TableCellProperties(border="0.75pt solid #000000", borderbottom="2pt solid #000000")) + doc.styles.addElement(style_bot) + + style_botright = Style(name="Contenu bas droite", family="table-cell") + style_botright.addElement(TableCellProperties(border="0.75pt solid #000000", + borderbottom="2pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(style_botright) + + title_style = Style(name="Titre", family="table-cell") + title_style.addElement(TextProperties(fontweight="bold")) + title_style.addElement(TableCellProperties(border="0.75pt solid #000000")) + doc.styles.addElement(title_style) + + title_style_left = Style(name="Titre gauche", family="table-cell") + title_style_left.addElement(TextProperties(fontweight="bold")) + title_style_left.addElement(TableCellProperties(border="0.75pt solid #000000", + borderleft="2pt solid #000000")) + doc.styles.addElement(title_style_left) + + title_style_right = Style(name="Titre droite", family="table-cell") + title_style_right.addElement(TextProperties(fontweight="bold")) + title_style_right.addElement(TableCellProperties(border="0.75pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(title_style_right) + + title_style_leftright = Style(name="Titre gauche droite", family="table-cell") + title_style_leftright.addElement(TextProperties(fontweight="bold")) + title_style_leftright.addElement(TableCellProperties(border="0.75pt solid #000000", + borderleft="2pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(title_style_leftright) + + title_style_top = Style(name="Titre haut", family="table-cell") + title_style_top.addElement(TextProperties(fontweight="bold")) + title_style_top.addElement(TableCellProperties(border="0.75pt solid #000000", + bordertop="2pt solid #000000")) + doc.styles.addElement(title_style_top) + + title_style_topbot = Style(name="Titre haut bas", family="table-cell") + title_style_topbot.addElement(TextProperties(fontweight="bold")) + title_style_topbot.addElement(TableCellProperties(border="0.75pt solid #000000", + bordertop="2pt solid #000000", + borderbottom="2pt solid #000000")) + doc.styles.addElement(title_style_topbot) + + title_style_topleft = Style(name="Titre haut gauche", family="table-cell") + title_style_topleft.addElement(TextProperties(fontweight="bold")) + title_style_topleft.addElement(TableCellProperties(border="0.75pt solid #000000", + bordertop="2pt solid #000000", + borderleft="2pt solid #000000")) + doc.styles.addElement(title_style_topleft) + + title_style_topbotleft = Style(name="Titre haut bas gauche", family="table-cell") + title_style_topbotleft.addElement(TextProperties(fontweight="bold")) + title_style_topbotleft.addElement(TableCellProperties(border="0.75pt solid #000000", + bordertop="2pt solid #000000", + borderbottom="2pt solid #000000", + borderleft="2pt solid #000000")) + doc.styles.addElement(title_style_topbotleft) + + title_style_topright = Style(name="Titre haut droite", family="table-cell") + title_style_topright.addElement(TextProperties(fontweight="bold")) + title_style_topright.addElement(TableCellProperties(border="0.75pt solid #000000", + bordertop="2pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(title_style_topright) + + title_style_topbotright = Style(name="Titre haut bas droite", family="table-cell") + title_style_topbotright.addElement(TextProperties(fontweight="bold")) + title_style_topbotright.addElement(TableCellProperties(border="0.75pt solid #000000", + bordertop="2pt solid #000000", + borderbottom="2pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(title_style_topbotright) + + title_style_topleftright = Style(name="Titre haut gauche droite", family="table-cell") + title_style_topleftright.addElement(TextProperties(fontweight="bold")) + title_style_topleftright.addElement(TableCellProperties(border="0.75pt solid #000000", + bordertop="2pt solid #000000", + borderleft="2pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(title_style_topleftright) + + title_style_bot = Style(name="Titre bas", family="table-cell") + title_style_bot.addElement(TextProperties(fontweight="bold")) + title_style_bot.addElement(TableCellProperties(border="0.75pt solid #000000", + borderbottom="2pt solid #000000")) + doc.styles.addElement(title_style_bot) + + title_style_botleft = Style(name="Titre bas gauche", family="table-cell") + title_style_botleft.addElement(TextProperties(fontweight="bold")) + title_style_botleft.addElement(TableCellProperties(border="0.75pt solid #000000", + borderbottom="2pt solid #000000", + borderleft="2pt solid #000000")) + doc.styles.addElement(title_style_botleft) + + title_style_botright = Style(name="Titre bas droite", family="table-cell") + title_style_botright.addElement(TextProperties(fontweight="bold")) + title_style_botright.addElement(TableCellProperties(border="0.75pt solid #000000", + borderbottom="2pt solid #000000", + borderright="2pt solid #000000")) + doc.styles.addElement(title_style_botright) + + first_col_style = Style(name="co1", family="table-column") + first_col_style.addElement(TableColumnProperties(columnwidth="9cm", breakbefore="auto")) + doc.automaticstyles.addElement(first_col_style) + + col_style = Style(name="co2", family="table-column") + col_style.addElement(TableColumnProperties(columnwidth="2.6cm", breakbefore="auto")) + doc.automaticstyles.addElement(col_style) + + obs_col_style = Style(name="co3", family="table-column") + obs_col_style.addElement(TableColumnProperties(columnwidth="5.2cm", breakbefore="auto")) + doc.automaticstyles.addElement(obs_col_style) + + table = Table(name=f"Poule {self.object.get_letter_display()}{self.object.round}") + doc.spreadsheet.addElement(table) + + table.addElement(TableColumn(stylename=first_col_style)) + + for i in range(line_length): + table.addElement(TableColumn(stylename=obs_col_style if pool_size == 4 + and i % passage_width == passage_width - 1 else col_style)) + + # Add line for the problems for different passages + header_pb = TableRow() + table.addElement(header_pb) + problems_tc = TableCell(valuetype="string", stylename=title_style_topleft) + problems_tc.addElement(P(text="Problème")) + header_pb.addElement(problems_tc) + for passage in self.object.passages.all(): + tc = TableCell(valuetype="string", stylename=title_style_topleftright) + tc.addElement(P(text=f"Problème {passage.solution_number}")) + tc.setAttribute('numbercolumnsspanned', "7" if pool_size == 4 else "6") + tc.setAttribute("formula", f"of:=[.B{8 + self.object.juries.count() + passage.position}]") + header_pb.addElement(tc) + header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=6 if pool_size == 4 else 5)) + + # Add roles on the second line of the table + header_role = TableRow() + table.addElement(header_role) + role_tc = TableCell(valuetype="string", stylename=title_style_left) + role_tc.addElement(P(text="Rôle")) + header_role.addElement(role_tc) + for i in range(pool_size): + defender_tc = TableCell(valuetype="string", stylename=title_style_left) + defender_tc.addElement(P(text="Défenseur⋅se")) + defender_tc.setAttribute('numbercolumnsspanned', "2") + header_role.addElement(defender_tc) + header_role.addElement(CoveredTableCell()) + + opponent_tc = TableCell(valuetype="string", stylename=title_style) + opponent_tc.addElement(P(text="Opposant⋅e")) + opponent_tc.setAttribute('numbercolumnsspanned', "2") + header_role.addElement(opponent_tc) + header_role.addElement(CoveredTableCell()) + + reporter_tc = TableCell(valuetype="string", + stylename=title_style_right if pool_size != 4 else title_style) + reporter_tc.addElement(P(text="Rapporteur⋅e")) + reporter_tc.setAttribute('numbercolumnsspanned', "2") + header_role.addElement(reporter_tc) + header_role.addElement(CoveredTableCell()) + + if pool_size == 4: + observer_tc = TableCell(valuetype="string", stylename=title_style_right) + observer_tc.addElement(P(text="Intervention exceptionnelle")) + header_role.addElement(observer_tc) + + # Add maximum notes on the third line + header_notes = TableRow() + table.addElement(header_notes) + jury_tc = TableCell(valuetype="string", value="Juré⋅e", stylename=title_style_botleft) + jury_tc.addElement(P(text="Juré⋅e")) + header_notes.addElement(jury_tc) + + for i in range(pool_size): + defender_w_tc = TableCell(valuetype="string", stylename=title_style_botleft) + defender_w_tc.addElement(P(text="Écrit (/20)")) + header_notes.addElement(defender_w_tc) + + defender_o_tc = TableCell(valuetype="string", stylename=title_style_bot) + defender_o_tc.addElement(P(text="Oral (/16)")) + header_notes.addElement(defender_o_tc) + + opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot) + opponent_w_tc.addElement(P(text="Écrit (/9)")) + header_notes.addElement(opponent_w_tc) + + opponent_o_tc = TableCell(valuetype="string", stylename=title_style_bot) + opponent_o_tc.addElement(P(text="Oral (/10)")) + header_notes.addElement(opponent_o_tc) + + reporter_w_tc = TableCell(valuetype="string", stylename=title_style_bot) + reporter_w_tc.addElement(P(text="Écrit (/9)")) + header_notes.addElement(reporter_w_tc) + + reporter_o_tc = TableCell(valuetype="string", + stylename=title_style_botright if pool_size != 4 else title_style_bot) + reporter_o_tc.addElement(P(text="Oral (/10)")) + header_notes.addElement(reporter_o_tc) + + if pool_size == 4: + observer_tc = TableCell(valuetype="string", + stylename=title_style_botright) + observer_tc.addElement(P(text="Oral (± 4)")) + header_notes.addElement(observer_tc) + + # Add a notation line for each jury + for jury in self.object.juries.all(): + jury_row = TableRow() + table.addElement(jury_row) + + name_tc = TableCell(valuetype="string", stylename=style_leftright) + name_tc.addElement(P(text=f"{jury.user.first_name} {jury.user.last_name}")) + jury_row.addElement(name_tc) + + for passage in self.object.passages.all(): + notes = Note.objects.get(jury=jury, passage=passage) + for j, note in enumerate(notes.get_all()): + note_tc = TableCell(valuetype="float", value=note, + stylename=style_right if j == passage_width - 1 else style) + note_tc.addElement(P(text=str(note))) + jury_row.addElement(note_tc) + + jury_size = self.object.juries.count() + min_row = 4 + max_row = 4 + jury_size - 1 + min_column = 2 + + # Add line for averages + average_row = TableRow() + table.addElement(average_row) + average_tc = TableCell(valuetype="string", stylename=title_style_topleftright) + average_tc.addElement(P(text="Moyenne")) + average_row.addElement(average_tc) + for i, passage in enumerate(self.object.passages.all()): + for j, note in enumerate(passage.averages): + tc = TableCell(valuetype="float", value=note, + stylename=style_topright if j == passage_width - 1 else style_top) + tc.addElement(P(text=str(note))) + column = getcol(min_column + i * passage_width + j) + tc.setAttribute("formula", f"of:=AVERAGEIF([.${getcol(min_column + i * passage_width)}${min_row}" + f":${getcol(min_column + i * passage_width)}{max_row}]; \">0\"; " + f"[.{column}${min_row}:{column}{max_row}])") + average_row.addElement(tc) + + # Add coefficients for each note on the next line + coeff_row = TableRow() + table.addElement(coeff_row) + coeff_tc = TableCell(valuetype="string", stylename=title_style_leftright) + coeff_tc.addElement(P(text="Coefficient")) + coeff_row.addElement(coeff_tc) + for passage in self.object.passages.all(): + defender_w_tc = TableCell(valuetype="float", value=1, stylename=style_left) + defender_w_tc.addElement(P(text="1")) + coeff_row.addElement(defender_w_tc) + + defender_o_tc = TableCell(valuetype="float", value=2 - 0.5 * passage.defender_penalties, stylename=style) + defender_o_tc.addElement(P(text=str(2 - 0.5 * passage.defender_penalties))) + coeff_row.addElement(defender_o_tc) + + opponent_w_tc = TableCell(valuetype="float", value=1, stylename=style) + opponent_w_tc.addElement(P(text="1")) + coeff_row.addElement(opponent_w_tc) + + opponent_o_tc = TableCell(valuetype="float", value=2, stylename=style) + opponent_o_tc.addElement(P(text="2")) + coeff_row.addElement(opponent_o_tc) + + reporter_w_tc = TableCell(valuetype="float", value=1, stylename=style) + reporter_w_tc.addElement(P(text="1")) + coeff_row.addElement(reporter_w_tc) + + reporter_o_tc = TableCell(valuetype="float", value=1, + stylename=style_right if pool_size != 4 else style) + reporter_o_tc.addElement(P(text="1")) + coeff_row.addElement(reporter_o_tc) + + if pool_size == 4: + observer_tc = TableCell(valuetype="float", value=1, stylename=style_right) + observer_tc.addElement(P(text="1")) + coeff_row.addElement(observer_tc) + + # Add the subtotal on the next line + subtotal_row = TableRow() + table.addElement(subtotal_row) + subtotal_tc = TableCell(valuetype="string", stylename=title_style_botleft) + subtotal_tc.addElement(P(text="Sous-total")) + subtotal_row.addElement(subtotal_tc) + for i, passage in enumerate(self.object.passages.all()): + def_w_col = getcol(min_column + passage_width * i) + def_o_col = getcol(min_column + passage_width * i + 1) + defender_tc = TableCell(valuetype="float", value=passage.average_defender, stylename=style_botleft) + defender_tc.addElement(P(text=str(passage.average_defender))) + defender_tc.setAttribute('numbercolumnsspanned', "2") + defender_tc.setAttribute("formula", f"of:=[.{def_w_col}{max_row + 1}] * [.{def_w_col}{max_row + 2}]" + f" + [.{def_o_col}{max_row + 1}] * [.{def_o_col}{max_row + 2}]") + subtotal_row.addElement(defender_tc) + subtotal_row.addElement(CoveredTableCell()) + + opp_w_col = getcol(min_column + passage_width * i + 2) + opp_o_col = getcol(min_column + passage_width * i + 3) + opponent_tc = TableCell(valuetype="float", value=passage.average_opponent, stylename=style_bot) + opponent_tc.addElement(P(text=str(passage.average_opponent))) + opponent_tc.setAttribute('numbercolumnsspanned', "2") + opponent_tc.setAttribute("formula", f"of:=[.{opp_w_col}{max_row + 1}] * [.{opp_w_col}{max_row + 2}]" + f" + [.{opp_o_col}{max_row + 1}] * [.{opp_o_col}{max_row + 2}]") + subtotal_row.addElement(opponent_tc) + subtotal_row.addElement(CoveredTableCell()) + + rep_w_col = getcol(min_column + passage_width * i + 4) + rep_o_col = getcol(min_column + passage_width * i + 5) + reporter_tc = TableCell(valuetype="float", value=passage.average_reporter, + stylename=style_botright if pool_size != 4 else style_bot) + reporter_tc.addElement(P(text=str(passage.average_reporter))) + reporter_tc.setAttribute('numbercolumnsspanned', "2") + reporter_tc.setAttribute("formula", f"of:=[.{rep_w_col}{max_row + 1}] * [.{rep_w_col}{max_row + 2}]" + f" + [.{rep_o_col}{max_row + 1}] * [.{rep_o_col}{max_row + 2}]") + subtotal_row.addElement(reporter_tc) + subtotal_row.addElement(CoveredTableCell()) + + if pool_size == 4: + obs_col = getcol(min_column + passage_width * i + 6) + observer_tc = TableCell(valuetype="float", value=passage.average_observer, + stylename=style_botright) + observer_tc.addElement(P(text=str(passage.average_observer))) + observer_tc.setAttribute("formula", f"of:=[.{obs_col}{max_row + 1}] * [.{obs_col}{max_row + 2}]") + subtotal_row.addElement(observer_tc) + + table.addElement(TableRow()) + + # Compute the total scores in a new table + scores_header = TableRow() + table.addElement(scores_header) + team_tc = TableCell(valuetype="string", stylename=title_style_topbotleft) + team_tc.addElement(P(text="Équipe")) + scores_header.addElement(team_tc) + problem_tc = TableCell(valuetype="string", stylename=title_style_topbot) + problem_tc.addElement(P(text="Problème")) + scores_header.addElement(problem_tc) + total_tc = TableCell(valuetype="string", stylename=title_style_topbot) + total_tc.addElement(P(text="Total")) + scores_header.addElement(total_tc) + rank_tc = TableCell(valuetype="string", stylename=title_style_topbotright) + rank_tc.addElement(P(text="Rang")) + scores_header.addElement(rank_tc) + + # For each line of the matrix P, the ith team is defender on the passage number Pi0, + # opponent on the passage number Pi1, reporter on the passage number Pi2 + # and eventually observer on the passage number Pi3. + passage_matrix = [] + match pool_size: + case 3: + passage_matrix = [ + [0, 2, 1], + [1, 0, 2], + [2, 1, 0], + ] + case 4: + passage_matrix = [ + [0, 3, 2, 1], + [1, 0, 3, 2], + [2, 1, 0, 3], + [3, 2, 1, 0], + ] + case 5: + passage_matrix = [ + [0, 2, 3], + [1, 4, 2], + [2, 0, 4], + [3, 1, 0], + [4, 3, 1], + ] + + sorted_participations = sorted(self.object.participations.all(), key=lambda p: -self.object.average(p)) + for passage in self.object.passages.all(): + team_row = TableRow() + table.addElement(team_row) + + team_tc = TableCell(valuetype="string", + stylename=style_botleft if passage.position == pool_size else style_left) + team_tc.addElement(P(text=f"{passage.defender.team.name} ({passage.defender.team.trigram})")) + team_row.addElement(team_tc) + + problem_tc = TableCell(valuetype="string", + stylename=style_bot if passage.position == pool_size else style) + problem_tc.addElement(P(text=f"Problème {passage.solution_number}")) + team_row.addElement(problem_tc) + + passage_line = passage_matrix[passage.position - 1] + score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender), + stylename=style_bot if passage.position == pool_size else style) + score_tc.addElement(P(text=self.object.average(passage.defender))) + formula = "of:=" + formula += getcol(min_column + passage_line[0] * passage_width) + str(max_row + 3) # Defender + formula += " + " + getcol(min_column + passage_line[1] * passage_width + 2) + str(max_row + 3) # Opponent + formula += " + " + getcol(min_column + passage_line[2] * passage_width + 4) + str(max_row + 3) # Reporter + if pool_size == 4: + # Observer + formula += " + " + getcol(min_column + passage_line[3] * passage_width + 6) + str(max_row + 3) + score_tc.setAttribute("formula", formula) + team_row.addElement(score_tc) + + score_col = 'C' + rank_tc = TableCell(valuetype="float", value=sorted_participations.index(passage.defender) + 1, + stylename=style_botright if passage.position == pool_size else style_right) + rank_tc.addElement(P(text=str(sorted_participations.index(passage.defender) + 1))) + rank_tc.setAttribute("formula", f"of:=RANK([.{score_col}{max_row + 5 + passage.position}]; " + f"[.{score_col}${max_row + 6}]:[.{score_col}${max_row + 5 + pool_size}])") + team_row.addElement(rank_tc) + + table.addElement(TableRow()) + + # Add small instructions + instructions_tr = TableRow() + table.addElement(instructions_tr) + instructions_tc = TableCell() + instructions_tc.addElement(P(text="Merci de ne pas toucher aux noms des juré⋅es.\n" + "Si nécessaire, faites les modifications sur le site\n" + "et récupérez le nouveau template.\n" + "N'entrez que des notes entières.\n" + "Ne retirez pas de 0 : toute ligne incomplète sera ignorée.\n" + "Dans le cadre de poules à 5, laissez des 0 en face des\n" + "juré⋅es qui ne sont pas dans le passage souhaité,\n" + "et remplissez uniquement les notes nécessaires dans le tableau.\n" + "Les moyennes calculées ignorent les 0, donc pas d'inquiétude.")) + instructions_tr.addElement(instructions_tc) + + # Save the sheet in a temporary file and send it in the response + doc.save('/tmp/notes.ods') + + return FileResponse(streaming_content=open("/tmp/notes.ods", "rb"), + content_type="application/vnd.oasis.opendocument.spreadsheet", + filename=f"Feuille de notes - {self.object.tournament.name} " + f"- Poule {self.object.get_letter_display()}{self.object.round}.ods") + + class NotationSheetTemplateView(VolunteerMixin, DetailView): """ Generate a PDF from a LaTeX template for the notation papers. diff --git a/requirements.txt b/requirements.txt index cb64647..51dfa6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ djangorestframework~=3.14 django-rest-polymorphic~=0.1 gunicorn~=20.1 matrix-nio~=0.20 +odfpy~=1.4.1 phonenumbers~=8.12.57 psycopg2-binary~=2.9.5 pypdf~=3.4