Customize the notation sheet template for juries

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2023-04-07 21:47:06 +02:00
parent b921ca045e
commit 505a94e3aa
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
9 changed files with 535 additions and 12 deletions

View File

@ -288,7 +288,7 @@ class UploadNotesForm(forms.Form):
continue continue
name = line[0] 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 continue
notes = line[1:line_length] notes = line[1:line_length]
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes): if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):

View File

@ -534,6 +534,17 @@ class Passage(models.Model):
def average_observer(self) -> float: def average_observer(self) -> float:
return self.avg(note.observer_oral for note in self.notes.all()) 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): def average(self, participation):
return self.average_defender if participation == self.defender else self.average_opponent \ return self.average_defender if participation == self.defender else self.average_opponent \
if participation == self.opponent else self.average_reporter if participation == self.reporter \ if participation == self.opponent else self.average_reporter if participation == self.reporter \
@ -740,6 +751,16 @@ class Note(models.Model):
default=0, 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, 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): reporter_writing: int, reporter_oral: int, observer_oral: int = 0):
self.defender_writing = defender_writing self.defender_writing = defender_writing

View File

@ -2,20 +2,13 @@
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block content %} {% block content %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<div id="form-content"> <div id="form-content">
<div class="alert alert-info"> <div class="alert alert-info">
{% if object.participations.count == 3 %} <a class="alert-link" href="{% url "participation:pool_notes_template" pk=pool.pk %}">
<a class="alert-link" href="{% static "Fiche notations - 3 équipes.ods" %}"> {% trans "Download empty notation sheet" %}
{% elif object.participations.count == 4 %}
<a class="alert-link" href="{% static "Fiche notations - 4 équipes.ods" %}">
{% elif object.participations.count == 5 %}
<a class="alert-link" href="{% static "Fiche notations - 5 équipes.ods" %}">
{% endif %}
{% trans "Download empty notation sheet" %}
</a> </a>
</div> </div>
{% csrf_token %} {% csrf_token %}

View File

@ -6,8 +6,8 @@ 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, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolUpdateTeamsView, PoolUpdateView, \ PassageUpdateView, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolNotesTemplateView, PoolUpdateTeamsView, \
PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, SynthesisUploadView,\ PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, SynthesisUploadView, \
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \ TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentUpdateView TournamentListView, TournamentUpdateView
@ -42,6 +42,7 @@ urlpatterns = [
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"),
path("pools/<int:pk>/add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"), path("pools/<int:pk>/add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"),
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"), path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
path("pools/<int:pk>/upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"),
path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"), path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"),
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"), path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"), path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),

View File

@ -26,6 +26,10 @@ from django.views.generic import CreateView, DetailView, FormView, RedirectView,
from django.views.generic.edit import FormMixin, ProcessFormView from django.views.generic.edit import FormMixin, ProcessFormView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from magic import Magic 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 registration.models import StudentRegistration, VolunteerRegistration
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix from tfjm.matrix import Matrix
@ -824,6 +828,509 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],)) 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): class NotationSheetTemplateView(VolunteerMixin, DetailView):
""" """
Generate a PDF from a LaTeX template for the notation papers. Generate a PDF from a LaTeX template for the notation papers.

View File

@ -14,6 +14,7 @@ djangorestframework~=3.14
django-rest-polymorphic~=0.1 django-rest-polymorphic~=0.1
gunicorn~=20.1 gunicorn~=20.1
matrix-nio~=0.20 matrix-nio~=0.20
odfpy~=1.4.1
phonenumbers~=8.12.57 phonenumbers~=8.12.57
psycopg2-binary~=2.9.5 psycopg2-binary~=2.9.5
pypdf~=3.4 pypdf~=3.4