mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2024-12-25 06:22:22 +00:00
Export notation sheets on Google Sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
parent
0006ecc90d
commit
c522387482
17
participation/management/commands/update_notation_sheets.py
Normal file
17
participation/management/commands/update_notation_sheets.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (C) 2024 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from participation.models import Tournament
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
for tournament in Tournament.objects.all():
|
||||||
|
if options['verbosity'] >= 1:
|
||||||
|
self.stdout.write(f"Updating notation sheet for {tournament}")
|
||||||
|
tournament.create_spreadsheet()
|
||||||
|
for pool in tournament.pools.all():
|
||||||
|
if options['verbosity'] >= 1:
|
||||||
|
self.stdout.write(f"Updating notation sheet for pool {pool.short_name} for {tournament}")
|
||||||
|
pool.update_spreadsheet()
|
@ -0,0 +1,93 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-03-29 22:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0009_pool_jury_president"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="notes_sheet_id",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default="", max_length=64, verbose_name="Google Sheet ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="defender_oral",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
(11, 11),
|
||||||
|
(12, 12),
|
||||||
|
(13, 13),
|
||||||
|
(14, 14),
|
||||||
|
(15, 15),
|
||||||
|
(16, 16),
|
||||||
|
(17, 17),
|
||||||
|
(18, 18),
|
||||||
|
(19, 19),
|
||||||
|
(20, 20),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="defender oral note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="opponent_writing",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="opponent writing note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="reporter_writing",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="reporter writing note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -14,6 +14,8 @@ from django.utils import timezone
|
|||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
import gspread
|
||||||
|
from gspread.utils import a1_range_to_grid_range, MergeType
|
||||||
from registration.models import Payment, VolunteerRegistration
|
from registration.models import Payment, VolunteerRegistration
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
@ -337,6 +339,13 @@ class Tournament(models.Model):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
notes_sheet_id = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name=_("Google Sheet ID"),
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def teams_email(self):
|
def teams_email(self):
|
||||||
"""
|
"""
|
||||||
@ -409,6 +418,15 @@ class Tournament(models.Model):
|
|||||||
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
|
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
|
||||||
return '+'.join(map(str, sorted(fmt)))
|
return '+'.join(map(str, sorted(fmt)))
|
||||||
|
|
||||||
|
def create_spreadsheet(self):
|
||||||
|
if self.notes_sheet_id:
|
||||||
|
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)
|
||||||
|
self.notes_sheet_id = spreadsheet.id
|
||||||
|
self.save()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
||||||
|
|
||||||
@ -591,6 +609,295 @@ class Pool(models.Model):
|
|||||||
raise ValidationError({'jury_president': _("The president of the jury must be part of the jury.")})
|
raise ValidationError({'jury_president': _("The president of the jury must be part of the jury.")})
|
||||||
return super().validate_constraints()
|
return super().validate_constraints()
|
||||||
|
|
||||||
|
def update_spreadsheet(self): # noqa: C901
|
||||||
|
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, 32)
|
||||||
|
else:
|
||||||
|
worksheet = spreadsheet.worksheet(f"Poule {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 = 7 if pool_size == 4 else 6
|
||||||
|
passages = self.passages.all()
|
||||||
|
|
||||||
|
header = [
|
||||||
|
sum(([f"Problème {passage.solution_number}"] + (passage_width - 1) * [""]
|
||||||
|
for passage in passages), start=["Problème", ""]),
|
||||||
|
sum((["Défenseur⋅se", "", "Opposant⋅e", "", "Rapporteur⋅e", ""]
|
||||||
|
+ (["Observateur⋅rice"] if pool_size == 4 else [])
|
||||||
|
for _passage in passages), start=["Rôle", ""]),
|
||||||
|
sum((["Écrit (/20)", "Oral (/20)", "Écrit (/10)", "Oral (/10)", "Écrit (/10)", "Oral (/10)"]
|
||||||
|
+ (["Oral (± 4)"] if pool_size == 4 else [])
|
||||||
|
for _passage in passages), start=["Juré⋅e", ""]),
|
||||||
|
]
|
||||||
|
|
||||||
|
notes = []
|
||||||
|
for jury in self.juries.all():
|
||||||
|
line = [str(jury), jury.id]
|
||||||
|
for passage in passages:
|
||||||
|
note = passage.notes.filter(jury=jury).first()
|
||||||
|
line.extend([note.defender_writing, note.defender_oral, note.opponent_writing, note.opponent_oral,
|
||||||
|
note.reporter_writing, note.reporter_oral])
|
||||||
|
if pool_size == 4:
|
||||||
|
line.append(note.observer_oral)
|
||||||
|
notes.append(line)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
average = ["Moyenne", ""]
|
||||||
|
coeffs = sum(([1, 1.6 - 0.4 * passage.defender_penalties, 0.9, 2, 0.9, 1]
|
||||||
|
+ ([1] if pool_size == 4 else []) for passage in passages), start=["Coefficient", ""])
|
||||||
|
subtotal = ["Sous-total", ""]
|
||||||
|
footer = [average, coeffs, subtotal, 32 * [""]]
|
||||||
|
|
||||||
|
min_row = 4
|
||||||
|
max_row = min_row + self.juries.count() - 1
|
||||||
|
min_column = 3
|
||||||
|
for i, passage in enumerate(passages):
|
||||||
|
for j, note in enumerate(passage.averages):
|
||||||
|
column = getcol(min_column + i * passage_width + j)
|
||||||
|
average.append(f"=MOYENNE.SI(${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})")
|
||||||
|
def_w_col = getcol(min_column + passage_width * i)
|
||||||
|
def_o_col = getcol(min_column + passage_width * i + 1)
|
||||||
|
subtotal.extend([f"={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}", ""])
|
||||||
|
|
||||||
|
opp_w_col = getcol(min_column + passage_width * i + 2)
|
||||||
|
opp_o_col = getcol(min_column + passage_width * i + 3)
|
||||||
|
subtotal.extend([f"={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}", ""])
|
||||||
|
|
||||||
|
rep_w_col = getcol(min_column + passage_width * i + 4)
|
||||||
|
rep_o_col = getcol(min_column + passage_width * i + 5)
|
||||||
|
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 pool_size == 4:
|
||||||
|
obs_col = getcol(min_column + passage_width * i + 6)
|
||||||
|
subtotal.append(f"={obs_col}{max_row + 1} * {obs_col}{max_row + 2}")
|
||||||
|
|
||||||
|
ranking = [
|
||||||
|
["Équipe", "", "Problème", "Total", "Rang"],
|
||||||
|
]
|
||||||
|
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],
|
||||||
|
]
|
||||||
|
for passage in passages:
|
||||||
|
participation = passage.defender
|
||||||
|
passage_line = passage_matrix[passage.position - 1]
|
||||||
|
formula = "="
|
||||||
|
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)
|
||||||
|
ranking.append([f"{participation.team.name} ({participation.team.trigram})", "",
|
||||||
|
f"=${getcol(3 + (passage.position - 1) * passage_width)}$1", formula,
|
||||||
|
f"=RANG(D{max_row + 5 + passage.position}; "
|
||||||
|
f"D${max_row + 6}:D${max_row + 5 + pool_size})"])
|
||||||
|
|
||||||
|
all_values = header + notes + footer + ranking
|
||||||
|
|
||||||
|
worksheet.update("A1:AF", all_values, raw=False)
|
||||||
|
|
||||||
|
format_requests = []
|
||||||
|
|
||||||
|
# Merge cells
|
||||||
|
merge_cells = ["A1:B1"]
|
||||||
|
for i, passage in enumerate(passages):
|
||||||
|
merge_cells.append(f"{getcol(3 + i * passage_width)}1:{getcol(2 + passage_width + i * passage_width)}1")
|
||||||
|
|
||||||
|
merge_cells.append(f"{getcol(3 + i * passage_width)}2:{getcol(4 + i * passage_width)}2")
|
||||||
|
merge_cells.append(f"{getcol(5 + i * passage_width)}2:{getcol(6 + i * passage_width)}2")
|
||||||
|
merge_cells.append(f"{getcol(7 + i * passage_width)}2:{getcol(8 + i * passage_width)}2")
|
||||||
|
|
||||||
|
merge_cells.append(f"{getcol(3 + i * passage_width)}{max_row + 3}"
|
||||||
|
f":{getcol(4 + i * passage_width)}{max_row + 3}")
|
||||||
|
merge_cells.append(f"{getcol(5 + i * passage_width)}{max_row + 3}"
|
||||||
|
f":{getcol(6 + i * passage_width)}{max_row + 3}")
|
||||||
|
merge_cells.append(f"{getcol(7 + i * passage_width)}{max_row + 3}"
|
||||||
|
f":{getcol(8 + i * passage_width)}{max_row + 3}")
|
||||||
|
merge_cells.append(f"A{max_row + 1}:B{max_row + 1}")
|
||||||
|
merge_cells.append(f"A{max_row + 2}:B{max_row + 2}")
|
||||||
|
merge_cells.append(f"A{max_row + 3}:B{max_row + 3}")
|
||||||
|
|
||||||
|
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:AF", 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:AF3", f"A{max_row + 1}:B{max_row + 3}", f"A{max_row + 5}:E{max_row + 5}"]
|
||||||
|
for bold_range in bold_ranges:
|
||||||
|
format_requests.append({
|
||||||
|
"repeatCell": {
|
||||||
|
"range": a1_range_to_grid_range(bold_range, worksheet.id),
|
||||||
|
"cell": {"userEnteredFormat": {"textFormat": {"bold": True}}},
|
||||||
|
"fields": "userEnteredFormat(textFormat)",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set background color for headers and footers
|
||||||
|
bg_colors = [(f"A1:{getcol(2 + pool_size * passage_width)}3", (0.8, 0.8, 0.8)),
|
||||||
|
(f"A{min_row}:B{max_row}", (0.95, 0.95, 0.95)),
|
||||||
|
(f"A{max_row + 1}:B{max_row + 3}", (0.8, 0.8, 0.8)),
|
||||||
|
(f"C{max_row + 1}:{getcol(2 + pool_size * passage_width)}{max_row + 3}", (0.9, 0.9, 0.9)),
|
||||||
|
(f"A{max_row + 5}:E{max_row + 5}", (0.8, 0.8, 0.8)),
|
||||||
|
(f"A{max_row + 6}:E{max_row + 5 + pool_size}", (0.9, 0.9, 0.9)),]
|
||||||
|
for bg_range, bg_color in bg_colors:
|
||||||
|
r, g, b = bg_color
|
||||||
|
format_requests.append({
|
||||||
|
"repeatCell": {
|
||||||
|
"range": a1_range_to_grid_range(bg_range, worksheet.id),
|
||||||
|
"cell": {"userEnteredFormat": {"backgroundColor": {"red": r, "green": g, "blue": b}}},
|
||||||
|
"fields": "userEnteredFormat(backgroundColor)",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Freeze 2 first columns
|
||||||
|
format_requests.append({
|
||||||
|
"updateSheetProperties": {
|
||||||
|
"properties": {
|
||||||
|
"sheetId": worksheet.id,
|
||||||
|
"gridProperties": {
|
||||||
|
"frozenRowCount": 0,
|
||||||
|
"frozenColumnCount": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"fields": "gridProperties/frozenRowCount,gridProperties/frozenColumnCount",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set the width of the columns
|
||||||
|
column_widths = [("A", 250), ("B", 30)]
|
||||||
|
for passage in passages:
|
||||||
|
column_widths.append((f"{getcol(3 + passage_width * (passage.position - 1))}"
|
||||||
|
f":{getcol(8 + passage_width * (passage.position - 1))}", 75))
|
||||||
|
if pool_size == 4:
|
||||||
|
column_widths.append((getcol(9 + passage_width * (passage.position - 1)), 120))
|
||||||
|
for column, width in column_widths:
|
||||||
|
grid_range = a1_range_to_grid_range(column, worksheet.id)
|
||||||
|
format_requests.append({
|
||||||
|
"updateDimensionProperties": {
|
||||||
|
"range": {
|
||||||
|
"sheetId": worksheet.id,
|
||||||
|
"dimension": "COLUMNS",
|
||||||
|
"startIndex": grid_range['startColumnIndex'],
|
||||||
|
"endIndex": grid_range['endColumnIndex'],
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"pixelSize": width,
|
||||||
|
},
|
||||||
|
"fields": "pixelSize",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Hide second column (Jury ID)
|
||||||
|
format_requests.append({
|
||||||
|
"updateDimensionProperties": {
|
||||||
|
"range": {
|
||||||
|
"sheetId": worksheet.id,
|
||||||
|
"dimension": "COLUMNS",
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2,
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"hiddenByUser": True,
|
||||||
|
},
|
||||||
|
"fields": "hiddenByUser",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define borders
|
||||||
|
border_ranges = [(f"A1:{getcol(2 + pool_size * 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"),
|
||||||
|
(f"C1:{getcol(2 + (pool_size - 1) * passage_width)}1", "1113")]
|
||||||
|
for i in range(pool_size - 1):
|
||||||
|
border_ranges.append((f"{getcol(2 + (i + 1) * passage_width)}2"
|
||||||
|
f":{getcol(2 + (i + 1) * passage_width)}{max_row + 3}", "1113"))
|
||||||
|
sides_names = ['top', 'bottom', 'left', 'right']
|
||||||
|
styles = ["NONE", "SOLID", "SOLID_MEDIUM", "SOLID_THICK", "DOUBLE"]
|
||||||
|
for border_range, sides in border_ranges:
|
||||||
|
borders = {}
|
||||||
|
for side_name, side in zip(sides_names, sides):
|
||||||
|
borders[side_name] = {"style": styles[int(side)]}
|
||||||
|
format_requests.append({
|
||||||
|
"repeatCell": {
|
||||||
|
"range": a1_range_to_grid_range(border_range, worksheet.id),
|
||||||
|
"cell": {
|
||||||
|
"userEnteredFormat": {
|
||||||
|
"borders": borders,
|
||||||
|
"horizontalAlignment": "CENTER",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"fields": "userEnteredFormat(borders,horizontalAlignment)",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Remove old protected ranges
|
||||||
|
for protected_range in spreadsheet.list_protected_ranges(worksheet.id):
|
||||||
|
format_requests.append({
|
||||||
|
"deleteProtectedRange": {
|
||||||
|
"protectedRangeId": protected_range["protectedRangeId"],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Protect the header, the juries list, the footer and the ranking
|
||||||
|
protected_ranges = ["A1:AF3",
|
||||||
|
f"A{min_row}:B{max_row}",
|
||||||
|
f"A{max_row + 1}:AF{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",
|
||||||
|
"warningOnly": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
body = {"requests": format_requests}
|
||||||
|
worksheet.client.batch_update(spreadsheet.id, body)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Pool of day {round} for tournament {tournament} with teams {teams}")\
|
return _("Pool of day {round} for tournament {tournament} with teams {teams}")\
|
||||||
.format(round=self.round,
|
.format(round=self.round,
|
||||||
|
@ -1312,8 +1312,8 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
table.addElement(TableColumn(stylename=jury_id_style))
|
table.addElement(TableColumn(stylename=jury_id_style))
|
||||||
|
|
||||||
for i in range(line_length):
|
for i in range(line_length):
|
||||||
table.addElement(TableColumn(stylename=obs_col_style if pool_size == 4
|
table.addElement(TableColumn(
|
||||||
and i % passage_width == passage_width - 1 else col_style))
|
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
|
# Add line for the problems for different passages
|
||||||
header_pb = TableRow()
|
header_pb = TableRow()
|
||||||
@ -1671,8 +1671,8 @@ class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
|||||||
page = self.request.GET.get('page', '1')
|
page = self.request.GET.get('page', '1')
|
||||||
if not page.isnumeric() or page not in ['1', '2']:
|
if not page.isnumeric() or page not in ['1', '2']:
|
||||||
page = '1'
|
page = '1'
|
||||||
passages = passages.filter(id__in=[passages[0].id, passages[2].id, passages[4].id]
|
passages = passages.filter(id__in=([passages[0].id, passages[2].id, passages[4].id]
|
||||||
if page == '1' else [passages[1].id, passages[3].id])
|
if page == '1' else [passages[1].id, passages[3].id]))
|
||||||
context['page'] = page
|
context['page'] = page
|
||||||
|
|
||||||
context['passages'] = passages
|
context['passages'] = passages
|
||||||
@ -1761,8 +1761,9 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
|||||||
|
|
||||||
passages = pool.passages.all()
|
passages = pool.passages.all()
|
||||||
if passages.count() == 5:
|
if passages.count() == 5:
|
||||||
passages = passages.filter(id__in=[passages[0].id, passages[2].id, passages[4].id]
|
passages = passages.filter(
|
||||||
if page == '1' else [passages[1].id, passages[3].id])
|
id__in=([passages[0].id, passages[2].id, passages[4].id]
|
||||||
|
if page == '1' else [passages[1].id, passages[3].id]))
|
||||||
|
|
||||||
context['passages'] = passages
|
context['passages'] = passages
|
||||||
context['esp'] = passages.count() * '&'
|
context['esp'] = passages.count() * '&'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
channels[daphne]~=4.0.0
|
channels[daphne]~=4.0.0
|
||||||
channels-redis~=4.2.0
|
channels-redis~=4.2.0
|
||||||
crispy-bootstrap5~=2023.10
|
crispy-bootstrap5~=2023.10
|
||||||
Django>=5.0,<6.0
|
Django>=5.0.3,<6.0
|
||||||
django-crispy-forms~=2.1
|
django-crispy-forms~=2.1
|
||||||
django-extensions~=3.2.3
|
django-extensions~=3.2.3
|
||||||
django-filter~=23.5
|
django-filter~=23.5
|
||||||
@ -13,6 +13,10 @@ django-polymorphic~=3.1.0
|
|||||||
django-tables2~=2.7.0
|
django-tables2~=2.7.0
|
||||||
djangorestframework~=3.14.0
|
djangorestframework~=3.14.0
|
||||||
django-rest-polymorphic~=0.1.10
|
django-rest-polymorphic~=0.1.10
|
||||||
|
google-api-python-client~=2.124.0
|
||||||
|
google-auth-httplib2~=0.2.0
|
||||||
|
google-auth-oauthlib~=1.2.0
|
||||||
|
gspread~=6.1.0
|
||||||
gunicorn~=21.2.0
|
gunicorn~=21.2.0
|
||||||
odfpy~=1.4.1
|
odfpy~=1.4.1
|
||||||
phonenumbers~=8.13.27
|
phonenumbers~=8.13.27
|
||||||
|
@ -246,6 +246,23 @@ HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTING
|
|||||||
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
|
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||||
HELLOASSO_TEST_ENDPOINT = False # Enable custom test endpoint, for unit tests
|
HELLOASSO_TEST_ENDPOINT = False # Enable custom test endpoint, for unit tests
|
||||||
|
|
||||||
|
GOOGLE_SERVICE_CLIENT = {
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": os.getenv("GOOGLE_PROJECT_ID", "plateforme-tfjm"),
|
||||||
|
"private_key_id": os.getenv("GOOGLE_PRIVATE_KEY_ID", "CHANGE_ME_IN_ENV_SETTINGS"),
|
||||||
|
"private_key": os.getenv("GOOGLE_PRIVATE_KEY", "CHANGE_ME_IN_ENV_SETTINGS").replace("\\n", "\n"),
|
||||||
|
"client_email": os.getenv("GOOGLE_CLIENT_EMAIL", "CHANGE_ME_IN_ENV_SETTINGS"),
|
||||||
|
"client_id": os.getenv("GOOGLE_CLIENT_ID", "CHANGE_ME_IN_ENV_SETTINGS"),
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": os.getenv("GOOGLE_CLIENT_X509_CERT_URL", "CHANGE_ME_IN_ENV_SETTINGS"),
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The ID of the Google Drive folder where to store the notation sheets
|
||||||
|
NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS")
|
||||||
|
|
||||||
# Custom parameters
|
# Custom parameters
|
||||||
PROBLEMS = [
|
PROBLEMS = [
|
||||||
"Triominos",
|
"Triominos",
|
||||||
|
Loading…
Reference in New Issue
Block a user