1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-22 10:38:25 +02:00

Update GSheets for ETEAM

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello
2024-07-05 16:48:17 +02:00
parent 44302a9ff4
commit d13ae89267
5 changed files with 577 additions and 303 deletions

View File

@ -302,25 +302,26 @@ class UploadNotesForm(forms.Form):
line = [s for s in line if s == s]
# Strip cases
line = [str(s).strip() for s in line if str(s)]
if line and line[0] == 'Problème':
if line and line[0] in ["Problème", "Problem"]:
pool_size = len(line) - 1
line_length = 2 + 6 * pool_size
line_length = 2 + (8 if df.iat[1, 8] == "Observer" else 6) * pool_size
continue
if pool_size == 0 or len(line) < line_length:
continue
name = line[0]
if name.lower() in ["rôle", "juré⋅e", "juré?e", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]:
if name.lower() in ["rôle", "juré⋅e", "juré?e", "moyenne", "coefficient", "sous-total", "équipe", "equipe",
"role", "juree", "average", "coefficient", "subtotal", "team"]:
continue
notes = line[2:line_length]
print(name, notes)
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
continue
notes = list(map(lambda x: int(float(x)), notes))
print(notes)
max_notes = pool_size * [20, 20, 10, 10, 10, 10]
max_notes = pool_size * [20 if settings.TFJM_APP == "TFJM" else 10,
20 if settings.TFJM_APP == "TFJM" else 10,
10, 10, 10, 10, 10, 10]
for n, max_n in zip(notes, max_notes):
if n > max_n:
self.add_error('file',

View File

@ -458,7 +458,7 @@ class Tournament(models.Model):
return self.notes_sheet_id
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.create(f"Feuille de notes - {self.name}", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
spreadsheet = gc.create(f"{_('Notation sheet')} - {self.name}", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
spreadsheet.update_locale("fr_FR")
spreadsheet.share(None, "anyone", "writer", with_link=True)
self.notes_sheet_id = spreadsheet.id
@ -470,17 +470,21 @@ class Tournament(models.Model):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.notes_sheet_id)
worksheets = spreadsheet.worksheets()
if "Classement final" not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet("Classement final", 100, 26)
if _("Final ranking") not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet(_("Final ranking"), 30, 10)
else:
worksheet = spreadsheet.worksheet("Classement final")
worksheet = spreadsheet.worksheet(_("Final ranking"))
if worksheet.index != self.pools.count():
worksheet.update_index(self.pools.count())
header = [["Équipe", "Score jour 1", "Harmonisation 1", "Score jour 2", "Harmonisation 2", "Total", "Rang"]]
header = [[_("Team"), _("Scores day 1"), _("Tweaks day 1"), _("Scores day 2"), _("Tweaks day 2")]
+ ([_("Total D1 + D2"), _("Scores day 3"), _("Tweaks day 3")]
if settings.NB_ROUNDS >= 3 else [])
+ [_("Total"), _("Rank")]]
lines = []
participations = self.participations.filter(pools__round=1, pools__tournament=self).distinct().all()
total_col, rank_col = ("F", "G") if settings.NB_ROUNDS == 2 else ("I", "J")
for i, participation in enumerate(participations):
line = [f"{participation.team.name} ({participation.team.trigram})"]
lines.append(line)
@ -494,7 +498,7 @@ class Tournament(models.Model):
tweak1_qs = Tweak.objects.filter(pool=pool1, participation=participation)
tweak1 = tweak1_qs.get() if tweak1_qs.exists() else None
line.append(f"=SIERREUR('Poule {pool1.short_name}'!$D{pool1.juries.count() + 10 + position1}; 0)")
line.append(f"=SIERREUR('{_('Pool')} {pool1.short_name}'!$D{pool1.juries.count() + 10 + position1}; 0)")
line.append(tweak1.diff if tweak1 else 0)
if Passage.objects.filter(pool__tournament=self, pool__round=2, defender=participation).exists():
@ -508,23 +512,49 @@ class Tournament(models.Model):
tweak2 = tweak2_qs.get() if tweak2_qs.exists() else None
line.append(
f"=SIERREUR('Poule {pool2.short_name}'!$D{pool2.juries.count() + 10 + position2}; 0)")
f"=SIERREUR('{_('Pool')} {pool2.short_name}'!$D{pool2.juries.count() + 10 + position2}; 0)")
line.append(tweak2.diff if tweak2 else 0)
if settings.NB_ROUNDS >= 3:
line.append(f"=$B{i + 2} + $C{i + 2} + $D{i + 2} + E{i + 2}")
if Passage.objects.filter(pool__tournament=self, pool__round=3, defender=participation).exists():
passage3 = Passage.objects.get(pool__tournament=self, pool__round=3, defender=participation)
pool3 = passage3.pool
if pool3.participations.count() != 5:
position3 = passage3.position
else:
position3 = (passage3.position - 1) * 2 + pool3.room
tweak3_qs = Tweak.objects.filter(pool=pool3, participation=participation)
tweak3 = tweak3_qs.get() if tweak3_qs.exists() else None
line.append(
f"=SIERREUR('{_('Pool')} {pool3.short_name}'!$D{pool3.juries.count() + 10 + position3}; 0)")
line.append(tweak3.diff if tweak3 else 0)
else:
line.append(0)
line.append(0)
else:
# User has no second pool yet
# There is no second pool yet
line.append(0)
line.append(0)
line.append(f"=$B{i + 2} + $C{i + 2} + $D{i + 2} + E{i + 2}")
line.append(f"=RANG($F{i + 2}; $F$2:$F${participations.count() + 1})")
if settings.NB_ROUNDS >= 3:
line.append(f"=$B{i + 2} + $C{i + 2} + $D{i + 2} + E{i + 2}")
line.append(0)
line.append(0)
final_ranking = [["", "", "", ""], ["", "", "", ""], ["Équipe", "Score", "Rang", "Mention"],
line.append(f"=$B{i + 2} + $C{i + 2} + $D{i + 2} + E{i + 2}"
+ (f" + (PI() - 2) * $G{i + 2} + $H{i + 2}" if settings.NB_ROUNDS >= 3 else ""))
line.append(f"=RANG(${total_col}{i + 2}; ${total_col}$2:${total_col}${participations.count() + 1})")
final_ranking = [["", "", "", ""], ["", "", "", ""], [_("Team"), _("Score"), _("Rank"), _("Mention")],
[f"=SORT($A$2:$A${participations.count() + 1}; "
f"$F$2:$F${participations.count() + 1}; FALSE)",
f"=SORT($F$2:$F${participations.count() + 1}; "
f"$F$2:$F${participations.count() + 1}; FALSE)",
f"=SORT($G$2:$G${participations.count() + 1}; "
f"$F$2:$F${participations.count() + 1}; FALSE)", ]]
f"${total_col}$2:${total_col}${participations.count() + 1}; FALSE)",
f"=SORT(${total_col}$2:${total_col}${participations.count() + 1}; "
f"${total_col}$2:${total_col}${participations.count() + 1}; FALSE)",
f"=SORT(${rank_col}$2:${rank_col}${participations.count() + 1}; "
f"${total_col}$2:${total_col}${participations.count() + 1}; FALSE)", ]]
final_ranking += [["", "", ""] for _i in range(participations.count() - 1)]
notes = dict()
@ -538,12 +568,13 @@ class Tournament(models.Model):
final_ranking[i + 3].append(participation.mention if not self.final else participation.mention_final)
data = header + lines + final_ranking
worksheet.update(data, f"A1:G{2 * participations.count() + 4}", raw=False)
worksheet.update(data, f"A1:{rank_col}{2 * participations.count() + 4}", raw=False)
format_requests = []
# Set the width of the columns
column_widths = [("A", 300), ("B", 150), ("C", 150), ("D", 150), ("E", 150), ("F", 150), ("G", 150)]
column_widths = [("A", 300), ("B", 150), ("C", 150), ("D", 150), ("E", 150), ("F", 150), ("G", 150),
("H", 150), ("I", 150), ("J", 150)]
for column, width in column_widths:
grid_range = a1_range_to_grid_range(column, worksheet.id)
format_requests.append({
@ -563,7 +594,7 @@ class Tournament(models.Model):
# Set borders
border_ranges = [("A1:Z", "0000"),
(f"A1:G{participations.count() + 1}", "1111"),
(f"A1:{rank_col}{participations.count() + 1}", "1111"),
(f"A{participations.count() + 4}:D{2 * participations.count() + 4}", "1111")]
sides_names = ['top', 'bottom', 'left', 'right']
styles = ["NONE", "SOLID", "SOLID_MEDIUM", "SOLID_THICK", "DOUBLE"]
@ -585,7 +616,7 @@ class Tournament(models.Model):
})
# Make titles bold
bold_ranges = [("A1:Z", False), ("A1:G1", True),
bold_ranges = [("A1:Z", False), (f"A1:{rank_col}1", True),
(f"A{participations.count() + 4}:D{participations.count() + 4}", True)]
for bold_range, bold in bold_ranges:
format_requests.append({
@ -598,14 +629,18 @@ class Tournament(models.Model):
# Set background color for headers and footers
bg_colors = [("A1:Z", (1, 1, 1)),
("A1:G1", (0.8, 0.8, 0.8)),
(f"A1:{rank_col}1", (0.8, 0.8, 0.8)),
(f"A2:B{participations.count() + 1}", (0.9, 0.9, 0.9)),
(f"C2:C{participations.count() + 1}", (1, 1, 1)),
(f"D2:D{participations.count() + 1}", (0.9, 0.9, 0.9)),
(f"E2:E{participations.count() + 1}", (1, 1, 1)),
(f"F2:G{participations.count() + 1}", (0.9, 0.9, 0.9)),
(f"A{participations.count() + 4}:D{participations.count() + 4}", (0.8, 0.8, 0.8)),
(f"A{participations.count() + 5}:C{2 * participations.count() + 4}", (0.9, 0.9, 0.9)),]
if settings.NB_ROUNDS >= 3:
bg_colors.append((f"F2:G{participations.count() + 1}", (0.9, 0.9, 0.9)))
bg_colors.append((f"H2:I{participations.count() + 1}", (0.9, 0.9, 0.9)))
else:
bg_colors.append((f"F2:G{participations.count() + 1}", (0.9, 0.9, 0.9)))
for bg_range, bg_color in bg_colors:
r, g, b = bg_color
format_requests.append({
@ -622,9 +657,15 @@ class Tournament(models.Model):
(f"D2:D{participations.count() + 1}", "0.0"),
(f"E2:E{participations.count() + 1}", "0"),
(f"F2:F{participations.count() + 1}", "0.0"),
(f"G2:G{participations.count() + 1}", "0"),
(f"B{participations.count() + 5}:B{2 * participations.count() + 5}", "0.0"),
(f"C{participations.count() + 5}:C{2 * participations.count() + 5}", "0"), ]
if settings.NB_ROUNDS >= 3:
number_format_ranges += [(f"G2:G{participations.count() + 1}", "0.0"),
(f"H2:H{participations.count() + 1}", "0"),
(f"I2:I{participations.count() + 1}", "0.0"),
(f"J2:J{participations.count() + 1}", "0"), ]
else:
number_format_ranges.append((f"G2:G{participations.count() + 1}", "0"))
for number_format_range, pattern in number_format_ranges:
format_requests.append({
"repeatCell": {
@ -643,16 +684,16 @@ class Tournament(models.Model):
})
# Protect the header, the juries list, the footer and the ranking
protected_ranges = ["A1:G1", f"A2:B{participations.count() + 1}",
protected_ranges = ["A1:J1", f"A2:B{participations.count() + 1}",
f"D2:D{participations.count() + 1}", f"F2:G{participations.count() + 1}",
f"I2:J{participations.count() + 1}",
f"A{participations.count() + 4}:C{2 * participations.count() + 4}", ]
for protected_range in protected_ranges:
format_requests.append({
"addProtectedRange": {
"protectedRange": {
"range": a1_range_to_grid_range(protected_range, worksheet.id),
"description": "Structure du tableur à ne pas modifier "
"pour une meilleure prise en charge automatisée",
"description": _("Don't update the table structure for a better automated integration."),
"warningOnly": True,
},
}
@ -666,19 +707,21 @@ class Tournament(models.Model):
# Draw has not been done yet
return
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.notes_sheet_id)
worksheet = spreadsheet.worksheet("Classement final")
worksheet = spreadsheet.worksheet(_("Final ranking"))
score_cell = worksheet.find("Score")
score_cell = worksheet.find(_("Score"))
max_row = score_cell.row - 3
if max_row == 1:
# There is no team
return
data = worksheet.get_values(f"A2:E{max_row}")
data = worksheet.get_values(f"A2:H{max_row}")
for line in data:
trigram = line[0][-4:-1]
trigram = line[0][-settings.TEAM_CODE_LENGTH - 1:-1]
participation = self.participations.get(team__trigram=trigram)
pool1 = self.pools.get(round=1, participations=participation, room=1)
tweak1_qs = Tweak.objects.filter(pool=pool1, participation=participation)
@ -701,6 +744,17 @@ class Tournament(models.Model):
create_defaults={'diff': tweak2_nb, 'pool': pool2,
'participation': participation})
if self.pools.filter(round=3, participations=participation).exists():
pool3 = self.pools.get(round=3, participations=participation, room=1)
tweak3_qs = Tweak.objects.filter(pool=pool3, participation=participation)
tweak3_nb = int(line[7])
if not tweak3_nb:
tweak3_qs.delete()
else:
tweak3_qs.update_or_create(defaults={'diff': tweak3_nb},
create_defaults={'diff': tweak3_nb, 'pool': pool3,
'participation': participation})
nb_participations = self.participations.filter(valid=True).count()
mentions = worksheet.get_values(f"A{score_cell.row + 1}:D{score_cell.row + nb_participations}")
notes = dict()
@ -1164,26 +1218,31 @@ class Pool(models.Model):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheets = spreadsheet.worksheets()
if f"Poule {self.short_name}" not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet(f"Poule {self.short_name}", 100, 26)
if f"{_('Pool')} {self.short_name}" not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet(f"{_('Pool')} {self.short_name}", 100, 34)
else:
worksheet = spreadsheet.worksheet(f"Poule {self.short_name}")
worksheet = spreadsheet.worksheet(f"{_('Pool')} {self.short_name}")
if any(ws.title == "Sheet1" for ws in worksheets):
spreadsheet.del_worksheet(spreadsheet.worksheet("Sheet1"))
pool_size = self.participations.count()
passage_width = 6
has_observer = settings.TFJM_APP == "ETEAM" and pool_size >= 4
passage_width = 6 + (2 if has_observer else 0)
passages = self.passages.all()
header = [
sum(([f"Problème {passage.solution_number}"] + (passage_width - 1) * [""]
for passage in passages), start=["Problème", ""]),
sum(([f"Défenseur⋅se ({passage.defender.team.trigram})", "",
f"Opposant⋅e ({passage.opponent.team.trigram})", "",
f"Rapporteur⋅rice ({passage.reviewer.team.trigram})", ""]
sum(([_("Problem #{problem}").format(problem=passage.solution_number)] + (passage_width - 1) * [""]
for passage in passages), start=[_("Problem"), ""]),
sum(([f"{_('Defender')} ({passage.defender.team.trigram})", "",
f"{_('Opponent')} ({passage.opponent.team.trigram})", "",
f"{_('Reviewer')} ({passage.reviewer.team.trigram})", ""]
+ ([f"{('Observer')} ({passage.observer.team.trigram})", ""] if has_observer else [])
for passage in passages), start=["Rôle", ""]),
sum((["Écrit (/20)", "Oral (/20)", "Écrit (/10)", "Oral (/10)", "Écrit (/10)", "Oral (/10)"]
for _passage in passages), start=["Juré⋅e", ""]),
sum(([f"{_('Writing')} (/{20 if settings.TFJM_APP == "TFJM" else 10})",
f"{_('Oral')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"
f"{_('Writing')} (/10)", f"{_('Oral')} (/10)", f"{_('Writing')} (/10)", f"{_('Oral')} (/10)"]
+ ([f"{_('Writing')} (/10)", f"{_('Oral')} (/10)"] if has_observer else [])
for _passage in passages), start=[_("Juree"), ""]),
]
notes = [[]] # Begin with empty hidden line to ensure pretty design
@ -1193,6 +1252,8 @@ class Pool(models.Model):
note = passage.notes.filter(jury=jury).first()
line.extend([note.defender_writing, note.defender_oral, note.opponent_writing, note.opponent_oral,
note.reviewer_writing, note.reviewer_oral])
if has_observer:
line.extend([note.observer_writing, note.observer_oral])
notes.append(line)
notes.append([]) # Add empty line to ensure pretty design
@ -1204,11 +1265,15 @@ class Pool(models.Model):
return ''
return getcol((number - 1) // 26) + chr(65 + (number - 1) % 26)
average = ["Moyenne", ""]
coeffs = sum(([1, 1.6 - 0.4 * passage.defender_penalties, 0.9, 2, 0.9, 1] for passage in passages),
start=["Coefficient", ""])
subtotal = ["Sous-total", ""]
footer = [average, coeffs, subtotal, 26 * [""]]
average = [_("Average"), ""]
coeffs = sum(([passage.coeff_defender_writing, passage.coeff_defender_oral,
passage.coeff_opponent_writing, passage.coeff_opponent_oral,
passage.coeff_reviewer_writing, passage.coeff_reviewer_oral]
+ ([passage.coeff_observer_writing, passage.coeff_observer_oral] if has_observer else [])
for passage in passages),
start=[_("Coefficient"), ""])
subtotal = [_("Subtotal"), ""]
footer = [average, coeffs, subtotal, 34 * [""]]
min_row = 5
max_row = min_row + self.juries.count()
@ -1234,8 +1299,14 @@ class Pool(models.Model):
subtotal.extend([f"={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}", ""])
if has_observer:
obs_w_col = getcol(min_column + passage_width * i + 6)
obs_o_col = getcol(min_column + passage_width * i + 7)
subtotal.extend([f"={obs_w_col}{max_row + 1} * {obs_w_col}{max_row + 2}"
f" + {obs_o_col}{max_row + 1} * {obs_o_col}{max_row + 2}", ""])
ranking = [
["Équipe", "", "Problème", "Total", "Rang"],
[_("Team"), "", _("Problem"), _("Total"), _("Rank")],
]
all_passages = Passage.objects.filter(pool__tournament=self.tournament,
pool__round=self.round,
@ -1258,14 +1329,22 @@ class Pool(models.Model):
reviewer_col = reviewer_passage.position - 1
formula = "="
formula += (f"'Poule {defender_passage.pool.short_name}'"
formula += (f"'{_('Pool')} {defender_passage.pool.short_name}'"
f"!{getcol(min_column + defender_col * passage_width)}{defender_row + 3}") # Defender
formula += (f" + 'Poule {opponent_passage.pool.short_name}'"
formula += (f" + '{_('Pool')} {opponent_passage.pool.short_name}'"
f"!{getcol(min_column + opponent_col * passage_width + 2)}{opponent_row + 3}") # Opponent
formula += (f" + 'Poule {reviewer_passage.pool.short_name}'"
formula += (f" + '{_('Pool')} {reviewer_passage.pool.short_name}'"
f"!{getcol(min_column + reviewer_col * passage_width + 4)}{reviewer_row + 3}") # reviewer
if has_observer:
observer_passage = Passage.objects.get(observer=participation,
pool__tournament=self.tournament, pool__round=self.round)
observer_row = 5 + observer_passage.pool.juries.count()
observer_col = observer_passage.position - 1
formula += (f" + '{_('Pool')} {observer_passage.pool.short_name}'"
f"!{getcol(min_column + observer_col * passage_width + 6)}{observer_row + 3}")
ranking.append([f"{participation.team.name} ({participation.team.trigram})", "",
f"='Poule {defender_passage.pool.short_name}'"
f"='{_('Pool')} {defender_passage.pool.short_name}'"
f"!${getcol(3 + defender_col * passage_width)}$1",
formula,
f"=RANG(D{max_row + 6 + i}; "
@ -1273,8 +1352,8 @@ class Pool(models.Model):
all_values = header + notes + footer + ranking
worksheet.batch_clear([f"A1:Z{max_row + 5 + pool_size}"])
worksheet.update("A1:Z", all_values, raw=False)
worksheet.batch_clear([f"A1:AH{max_row + 5 + pool_size}"])
worksheet.update("A1:AH", all_values, raw=False)
format_requests = []
@ -1300,13 +1379,13 @@ class Pool(models.Model):
for i in range(pool_size + 1):
merge_cells.append(f"A{max_row + 5 + i}:B{max_row + 5 + i}")
format_requests.append({"unmergeCells": {"range": a1_range_to_grid_range("A1:Z", worksheet.id)}})
format_requests.append({"unmergeCells": {"range": a1_range_to_grid_range("A1:AH", worksheet.id)}})
for name in merge_cells:
grid_range = a1_range_to_grid_range(name, worksheet.id)
format_requests.append({"mergeCells": {"mergeType": MergeType.merge_all, "range": grid_range}})
# Make titles bold
bold_ranges = [("A1:Z", False), ("A1:Z3", True),
bold_ranges = [("A1:AH", False), ("A1:AH3", True),
(f"A{max_row + 1}:B{max_row + 3}", True), (f"A{max_row + 5}:E{max_row + 5}", True)]
for bold_range, bold in bold_ranges:
format_requests.append({
@ -1318,7 +1397,7 @@ class Pool(models.Model):
})
# Set background color for headers and footers
bg_colors = [("A1:Z", (1, 1, 1)),
bg_colors = [("A1:AH", (1, 1, 1)),
(f"A1:{getcol(2 + passages.count() * passage_width)}3", (0.8, 0.8, 0.8)),
(f"A{min_row - 1}:B{max_row}", (0.95, 0.95, 0.95)),
(f"A{max_row + 1}:B{max_row + 3}", (0.8, 0.8, 0.8)),
@ -1407,7 +1486,7 @@ class Pool(models.Model):
})
# Define borders
border_ranges = [("A1:Z", "0000"),
border_ranges = [("A1:AH", "0000"),
(f"A1:{getcol(2 + passages.count() * passage_width)}{max_row + 3}", "1111"),
(f"A{max_row + 5}:E{max_row + pool_size + 5}", "1111"),
(f"A1:B{max_row + 3}", "1113"),
@ -1443,7 +1522,7 @@ class Pool(models.Model):
for j in range(passage_width):
column = getcol(min_column + i * passage_width + j)
min_note = 0
max_note = 20 if j < 2 else 10
max_note = 20 if j < 2 and settings.TFJM_APP == "TFJM" else 10
format_requests.append({
"setDataValidation": {
"range": a1_range_to_grid_range(f"{column}{min_row - 1}:{column}{max_row}", worksheet.id),
@ -1453,8 +1532,8 @@ class Pool(models.Model):
"values": [{"userEnteredValue": f'=ET(REGEXMATCH(TO_TEXT({column}4); "^-?[0-9]+$"); '
f'{column}4>={min_note}; {column}4<={max_note})'},],
},
"inputMessage": f"La saisie doit être un entier valide "
f"compris entre {min_note} et {max_note}.",
"inputMessage": (_("Input must be a valid integer between {min_note} and {max_note}.")
.format(min_note=min_note, max_note=max_note)),
"strict": True,
},
}
@ -1482,16 +1561,15 @@ class Pool(models.Model):
})
# Protect the header, the juries list, the footer and the ranking
protected_ranges = ["A1:Z4",
protected_ranges = ["A1:AH4",
f"A{min_row}:B{max_row}",
f"A{max_row}:Z{max_row + 5 + pool_size}"]
f"A{max_row}:AH{max_row + 5 + pool_size}"]
for protected_range in protected_ranges:
format_requests.append({
"addProtectedRange": {
"protectedRange": {
"range": a1_range_to_grid_range(protected_range, worksheet.id),
"description": "Structure du tableur à ne pas modifier "
"pour une meilleure prise en charge automatisée",
"description": _("Don't update the table structure for a better automated integration."),
"warningOnly": True,
},
}
@ -1505,9 +1583,9 @@ class Pool(models.Model):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet(f"Poule {self.short_name}")
worksheet = spreadsheet.worksheet(f"{_('Pool')} {self.short_name}")
average_cell = worksheet.find("Moyenne")
average_cell = worksheet.find(_("Average"))
min_row = 5
max_row = average_cell.row - 1
juries_visible = worksheet.get(f"A{min_row}:B{max_row}")
@ -1527,16 +1605,17 @@ class Pool(models.Model):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
self.tournament.create_spreadsheet()
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet(f"Poule {self.short_name}")
worksheet = spreadsheet.worksheet(f"{_('Pool')} {self.short_name}")
average_cell = worksheet.find("Moyenne")
average_cell = worksheet.find(_("Average"))
min_row = 5
max_row = average_cell.row - 2
data = worksheet.get_values(f"A{min_row}:Z{max_row}")
data = worksheet.get_values(f"A{min_row}:AH{max_row}")
if not data or not data[0]:
return
passage_width = 6
has_observer = settings.TFJM_APP == "ETEAM" and self.participations.count() >= 4
passage_width = 6 + (2 if has_observer else 0)
for line in data:
jury_name = line[0]
jury_id = line[1]
@ -1640,56 +1719,87 @@ class Passage(models.Model):
def average_defender_writing(self) -> float:
return self.avg(note.defender_writing for note in self.notes.all())
@property
def coeff_defender_writing(self) -> float:
return 1 if settings.TFJM_APP == "TFJM" else 2
@property
def average_defender_oral(self) -> float:
return self.avg(note.defender_oral for note in self.notes.all())
@property
def coeff_defender_oral(self) -> float:
coeff = 1.6 if settings.TFJM_APP == "TFJM" else 3
coeff *= 1 - 0.25 * self.defender_penalties
return coeff
@property
def average_defender(self) -> float:
writing_coeff = 1 if settings.TFJM_APP == "TFJM" else 2
oral_coeff = 1.6 if settings.TFJM_APP == "TFJM" else 3
oral_coeff *= 1 - 0.25 * self.defender_penalties
return writing_coeff * self.average_defender_writing + oral_coeff * self.average_defender_oral
return (self.coeff_defender_writing * self.average_defender_writing
+ self.coeff_defender_oral * self.average_defender_oral)
@property
def average_opponent_writing(self) -> float:
return self.avg(note.opponent_writing for note in self.notes.all())
@property
def coeff_opponent_writing(self) -> float:
return 0.9 if not self.observer else 0.6
@property
def average_opponent_oral(self) -> float:
return self.avg(note.opponent_oral for note in self.notes.all())
@property
def coeff_opponent_oral(self) -> float:
return 2
@property
def average_opponent(self) -> float:
writing_coeff = 0.9 if not self.observer else 0.6
oral_coeff = 2
return writing_coeff * self.average_opponent_writing + oral_coeff * self.average_opponent_oral
return (self.coeff_opponent_writing * self.average_opponent_writing
+ self.coeff_opponent_oral * self.average_opponent_oral)
@property
def average_reviewer_writing(self) -> float:
return self.avg(note.reviewer_writing for note in self.notes.all())
@property
def coeff_reviewer_writing(self):
return 0.9 if not self.observer else 0.6
@property
def average_reviewer_oral(self) -> float:
return self.avg(note.reviewer_oral for note in self.notes.all())
@property
def coeff_reviewer_oral(self):
return 1 if settings.TFJM_APP == "TFJM" else 1.2
@property
def average_reviewer(self) -> float:
writing_coeff = 0.9 if not self.observer else 0.6
oral_coeff = 1 if settings.TFJM_APP == "TFJM" else 1.2
return writing_coeff * self.average_reviewer_writing + oral_coeff * self.average_reviewer_oral
return (self.coeff_reviewer_writing * self.average_reviewer_writing
+ self.coeff_reviewer_oral * self.average_reviewer_oral)
@property
def average_observer_writing(self) -> float:
return self.avg(note.observer_writing for note in self.notes.all())
@property
def coeff_observer_writing(self):
return 0.6
@property
def average_observer_oral(self) -> float:
return self.avg(note.observer_oral for note in self.notes.all())
@property
def coeff_observer_oral(self):
return 0.5
@property
def average_observer(self) -> float:
return 0.6 * self.average_observer_writing + 0.5 * self.average_observer_oral
return (self.coeff_observer_writing * self.average_observer_writing
+ self.coeff_observer_oral * self.average_observer_oral)
@property
def averages(self):
@ -1707,9 +1817,7 @@ class Passage(models.Model):
avg = self.average_defender if participation == self.defender else self.average_opponent \
if participation == self.opponent else self.average_reviewer if participation == self.reviewer \
else self.average_observer if participation == self.observer else 0
if self.pool.round == 3 and settings.TFJM_APP == "ETEAM":
avg *= math.pi - 2
avg *= self.pool.coeff
return avg
@ -1957,7 +2065,7 @@ class Note(models.Model):
passage = Passage.objects.prefetch_related('pool__tournament', 'pool__participations').get(pk=self.passage.pk)
spreadsheet_id = passage.pool.tournament.notes_sheet_id
spreadsheet = gc.open_by_key(spreadsheet_id)
worksheet = spreadsheet.worksheet(f"Poule {passage.pool.short_name}")
worksheet = spreadsheet.worksheet(f"{_('Pool')} {passage.pool.short_name}")
jury_id_cell = worksheet.find(str(self.jury_id), in_column=2)
if not jury_id_cell:
raise ValueError("The jury ID cell was not found in the spreadsheet.")

View File

@ -82,13 +82,17 @@
{% trans "Average points for the defender writing" %}
({{ passage.defender.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_defender_writing|floatformat }}/20</dd>
<dd class="col-sm-4">
{{ passage.average_defender_writing|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
</dd>
<dt class="col-sm-8">
{% trans "Average points for the defender oral" %}
({{ passage.defender.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_defender_oral|floatformat }}/20</dd>
<dd class="col-sm-4">
{{ passage.average_defender_oral|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
</dd>
<dt class="col-sm-8">
{% trans "Average points for the opponent writing" %}
@ -113,14 +117,14 @@
({{ passage.reviewer.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_reviewer_oral|floatformat }}/10</dd>
{% if passage.observer %}
<dt class="col-sm-8">
{% trans "Average points for the observer writing" %}
({{ passage.observer.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_observer_writing|floatformat }}/10</dd>
<dt class="col-sm-8">
{% trans "Average points for the observer oral" %}
({{ passage.observer.team.trigram }}) :
@ -136,27 +140,31 @@
{% trans "Defender points" %}
({{ passage.defender.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_defender|floatformat }}/52</dd>
<dd class="col-sm-4">
{{ passage.average_defender|floatformat }}/{% if TFJM_APP == "TFJM" %}52{% else %}50{% endif %}
</dd>
<dt class="col-sm-8">
{% trans "Opponent points" %}
({{ passage.opponent.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_opponent|floatformat }}/29</dd>
<dd class="col-sm-4">
{{ passage.average_opponent|floatformat }}/{% if TFJM_APP == "TFJM" %}29{% else %}{% if passage.observer %}26{% else %}29{% endif %}{% endif %}
</dd>
<dt class="col-sm-8">
{% trans "reviewer points" %}
({{ passage.reviewer.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_reviewer|floatformat }}/19</dd>
<dd class="col-sm-4">{{ passage.average_reviewer|floatformat }}/{% if TFJM_APP == "TFJM" %}19{% else %}{% if passage.observer %}18{% else %}21{% endif %}{% endif %}</dd>
{% if passage.observer %}
<dt class="col-sm-8">
{% trans "observer points" %}
({{ passage.observer.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/10</dd>
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/6</dd>
{% endif %}
</dl>
</div>

View File

@ -24,7 +24,7 @@ from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils import timezone, translation
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.timezone import localtime
@ -1254,7 +1254,7 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
return self.form_invalid(form)
for vr, notes in parsed_notes.items():
notes_count = 6
notes_count = 6 + (2 if pool.participations.count() >= 4 and settings.TFJM_APP == "ETEAM" else 0)
for i, passage in enumerate(pool.passages.all()):
note = Note.objects.get_or_create(jury=vr, passage=passage)[0]
passage_notes = notes[notes_count * i:notes_count * (i + 1)]
@ -1289,8 +1289,11 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
return self.handle_no_permission()
def render_to_response(self, context, **response_kwargs): # noqa: C901
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
pool_size = self.object.passages.count()
passage_width = 6
has_observer = self.object.participations.count() >= 4 and settings.TFJM_APP == "ETEAM"
passage_width = 6 + (2 if has_observer else 0)
line_length = pool_size * passage_width
def getcol(number: int) -> str:
@ -1475,79 +1478,96 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
header_pb = TableRow()
table.addElement(header_pb)
problems_tc = TableCell(valuetype="string", stylename=title_style_topleft)
problems_tc.addElement(P(text="Problème"))
problems_tc.addElement(P(text=_("Problem")))
problems_tc.setAttribute('numbercolumnsspanned', "2")
header_pb.addElement(problems_tc)
header_pb.addElement(CoveredTableCell())
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', "6")
tc.addElement(P(text=_("Problem #{problem}").format(problem=passage.solution_number)))
tc.setAttribute('numbercolumnsspanned', str(passage_width))
header_pb.addElement(tc)
header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=5))
header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=passage_width - 1))
# 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"))
role_tc.addElement(P(text=_("Role")))
role_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(role_tc)
header_role.addElement(CoveredTableCell())
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.addElement(P(text=_("Defender")))
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.addElement(P(text=_("Opponent")))
opponent_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(opponent_tc)
header_role.addElement(CoveredTableCell())
reviewer_tc = TableCell(valuetype="string",
stylename=title_style_right)
reviewer_tc.addElement(P(text="Rapporteur⋅rice"))
stylename=title_style if has_observer else title_style_right)
reviewer_tc.addElement(P(text=_("Reviewer")))
reviewer_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(reviewer_tc)
header_role.addElement(CoveredTableCell())
if has_observer:
observer_tc = TableCell(valuetype="string", stylename=title_style_right)
observer_tc.addElement(P(text=_("Observer")))
observer_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(observer_tc)
header_role.addElement(CoveredTableCell())
# 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"))
jury_tc = TableCell(valuetype="string", value=_("Juree"), stylename=title_style_botleft)
jury_tc.addElement(P(text=_("Juree")))
jury_tc.setAttribute('numbercolumnsspanned', "2")
header_notes.addElement(jury_tc)
header_notes.addElement(CoveredTableCell())
for i in range(pool_size):
defender_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
defender_w_tc.addElement(P(text="Écrit (/20)"))
defender_w_tc.addElement(P(text=f"{_('Writing')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(defender_w_tc)
defender_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
defender_o_tc.addElement(P(text="Oral (/20)"))
defender_o_tc.addElement(P(text=f"{_('Oral')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(defender_o_tc)
opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
opponent_w_tc.addElement(P(text="Écrit (/10)"))
opponent_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
header_notes.addElement(opponent_w_tc)
opponent_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
opponent_o_tc.addElement(P(text="Oral (/10)"))
opponent_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
header_notes.addElement(opponent_o_tc)
reviewer_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
reviewer_w_tc.addElement(P(text="Écrit (/10)"))
reviewer_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
header_notes.addElement(reviewer_w_tc)
reviewer_o_tc = TableCell(valuetype="string", stylename=title_style_botright)
reviewer_o_tc.addElement(P(text="Oral (/10)"))
reviewer_o_tc = TableCell(valuetype="string",
stylename=title_style_bot if has_observer else title_style_botright)
reviewer_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
header_notes.addElement(reviewer_o_tc)
if has_observer:
observer_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
observer_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
header_notes.addElement(observer_w_tc)
observer_o_tc = TableCell(valuetype="string", stylename=title_style_botright)
observer_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
header_notes.addElement(observer_o_tc)
# Add a notation line for each jury
for jury in self.object.juries.all():
jury_row = TableRow()
@ -1577,7 +1597,7 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
average_row = TableRow()
table.addElement(average_row)
average_tc = TableCell(valuetype="string", stylename=title_style_topleftright)
average_tc.addElement(P(text="Moyenne"))
average_tc.addElement(P(text=_("Average")))
average_tc.setAttribute('numbercolumnsspanned', "2")
average_row.addElement(average_tc)
average_row.addElement(CoveredTableCell())
@ -1596,40 +1616,50 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
coeff_row = TableRow()
table.addElement(coeff_row)
coeff_tc = TableCell(valuetype="string", stylename=title_style_leftright)
coeff_tc.addElement(P(text="Coefficient"))
coeff_tc.addElement(P(text=_("Coefficient")))
coeff_tc.setAttribute('numbercolumnsspanned', "2")
coeff_row.addElement(coeff_tc)
coeff_row.addElement(CoveredTableCell())
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"))
defender_w_tc = TableCell(valuetype="float", value=passage.coeff_defender_writing, stylename=style_left)
defender_w_tc.addElement(P(text=str(passage.coeff_defender_writing)))
coeff_row.addElement(defender_w_tc)
defender_o_tc = TableCell(valuetype="float", value=1.6 - 0.4 * passage.defender_penalties, stylename=style)
defender_o_tc.addElement(P(text=str(2 - 0.4 * passage.defender_penalties)))
defender_o_tc = TableCell(valuetype="float", value=passage.coeff_defender_oral, stylename=style)
defender_o_tc.addElement(P(text=str(passage.coeff_defender_oral)))
coeff_row.addElement(defender_o_tc)
opponent_w_tc = TableCell(valuetype="float", value=0.9, stylename=style)
opponent_w_tc.addElement(P(text="1"))
opponent_w_tc = TableCell(valuetype="float", value=passage.coeff_opponent_writing, stylename=style)
opponent_w_tc.addElement(P(text=str(passage.coeff_opponent_writing)))
coeff_row.addElement(opponent_w_tc)
opponent_o_tc = TableCell(valuetype="float", value=2, stylename=style)
opponent_o_tc.addElement(P(text="2"))
opponent_o_tc = TableCell(valuetype="float", value=passage.coeff_opponent_oral, stylename=style)
opponent_o_tc.addElement(P(text=str(passage.coeff_opponent_oral)))
coeff_row.addElement(opponent_o_tc)
reviewer_w_tc = TableCell(valuetype="float", value=0.9, stylename=style)
reviewer_w_tc.addElement(P(text="1"))
reviewer_w_tc = TableCell(valuetype="float", value=passage.coeff_reviewer_writing, stylename=style)
reviewer_w_tc.addElement(P(text=str(passage.coeff_reviewer_writing)))
coeff_row.addElement(reviewer_w_tc)
reviewer_o_tc = TableCell(valuetype="float", value=1, stylename=style_right)
reviewer_o_tc.addElement(P(text="1"))
reviewer_o_tc = TableCell(valuetype="float", value=passage.coeff_reviewer_oral,
stylename=style if has_observer else style_right)
reviewer_o_tc.addElement(P(text=str(passage.coeff_reviewer_oral)))
coeff_row.addElement(reviewer_o_tc)
if has_observer:
observer_w_tc = TableCell(valuetype="float", value=passage.coeff_observer_writing, stylename=style)
observer_w_tc.addElement(P(text=str(passage.coeff_observer_writing)))
coeff_row.addElement(observer_w_tc)
observer_o_tc = TableCell(valuetype="float", value=passage.coeff_observer_oral, stylename=style_right)
observer_o_tc.addElement(P(text=str(passage.coeff_observer_oral)))
coeff_row.addElement(observer_o_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_tc.addElement(P(text=_("Subtotal")))
subtotal_tc.setAttribute('numbercolumnsspanned', "2")
subtotal_row.addElement(subtotal_tc)
subtotal_row.addElement(CoveredTableCell())
@ -1656,7 +1686,8 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
rep_w_col = getcol(min_column + passage_width * i + 4)
rep_o_col = getcol(min_column + passage_width * i + 5)
reviewer_tc = TableCell(valuetype="float", value=passage.average_reviewer, stylename=style_botright)
reviewer_tc = TableCell(valuetype="float", value=passage.average_reviewer,
stylename=style_bot if has_observer else style_botright)
reviewer_tc.addElement(P(text=str(passage.average_reviewer)))
reviewer_tc.setAttribute('numbercolumnsspanned', "2")
reviewer_tc.setAttribute("formula", f"of:=[.{rep_w_col}{max_row + 1}] * [.{rep_w_col}{max_row + 2}]"
@ -1664,6 +1695,17 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
subtotal_row.addElement(reviewer_tc)
subtotal_row.addElement(CoveredTableCell())
if has_observer:
obs_w_col = getcol(min_column + passage_width * i + 6)
obs_o_col = getcol(min_column + passage_width * i + 7)
observer_tc = TableCell(valuetype="float", value=passage.average_observer, stylename=style_botright)
observer_tc.addElement(P(text=str(passage.average_observer)))
observer_tc.setAttribute('numbercolumnsspanned', "2")
observer_tc.setAttribute("formula", f"of:=[.{obs_w_col}{max_row + 1}] * [.{obs_w_col}{max_row + 2}]"
f" + [.{obs_o_col}{max_row + 1}] * [.{obs_o_col}{max_row + 2}]")
subtotal_row.addElement(observer_tc)
subtotal_row.addElement(CoveredTableCell())
table.addElement(TableRow())
if self.object.participations.count() == 5:
@ -1681,17 +1723,17 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
scores_header = TableRow()
table.addElement(scores_header)
team_tc = TableCell(valuetype="string", stylename=title_style_topbotleft)
team_tc.addElement(P(text="Équipe"))
team_tc.addElement(P(text=_("Team")))
team_tc.setAttribute('numbercolumnsspanned', "2")
scores_header.addElement(team_tc)
problem_tc = TableCell(valuetype="string", stylename=title_style_topbot)
problem_tc.addElement(P(text="Problème"))
problem_tc.addElement(P(text=_("Problem")))
scores_header.addElement(problem_tc)
total_tc = TableCell(valuetype="string", stylename=title_style_topbot)
total_tc.addElement(P(text="Total"))
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"))
rank_tc.addElement(P(text=_("Rank")))
scores_header.addElement(rank_tc)
sorted_participations = sorted(self.object.participations.all(), key=lambda p: -self.object.average(p))
@ -1707,13 +1749,15 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
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}"))
problem_tc.addElement(P(text=_("Problem #{problem}").format(problem=passage.solution_number)))
problem_tc.setAttribute("formula", f"of:=[.B{3 + passage_width * (passage.position - 1)}]")
team_row.addElement(problem_tc)
defender_pos = passage.position - 1
opponent_pos = self.object.passages.get(opponent=passage.defender).position - 1
reviewer_pos = self.object.passages.get(reviewer=passage.defender).position - 1
observer_pos = self.object.passages.get(observer=passage.defender).position - 1 \
if has_observer else None
score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender),
stylename=style_bot if passage.position == pool_size else style)
@ -1721,7 +1765,10 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
formula = "of:="
formula += getcol(min_column + defender_pos * passage_width) + str(max_row + 3) # Defender
formula += " + " + getcol(min_column + opponent_pos * passage_width + 2) + str(max_row + 3) # Opponent
formula += " + " + getcol(min_column + reviewer_pos * passage_width + 4) + str(max_row + 3) # reviewer
formula += " + " + getcol(min_column + reviewer_pos * passage_width + 4) + str(max_row + 3) # Reviewer
if has_observer:
# Observer
formula += " + " + getcol(min_column + observer_pos * passage_width + 6) + str(max_row + 3)
score_tc.setAttribute("formula", formula)
team_row.addElement(score_tc)
@ -1730,7 +1777,8 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
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}])")
f"[.{score_col}${max_row + 6}]:"
f"[.{score_col}${max_row + 5 + pool_size}])")
team_row.addElement(rank_tc)
table.addElement(TableRow())
@ -1755,8 +1803,8 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
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.short_name}.ods")
filename=f"{_('Notation sheet')} - {self.object.tournament.name} "
f"- {_('Pool')} {self.object.short_name}.ods")
class NotationSheetTemplateView(VolunteerMixin, DetailView):