Auto update Google Sheet after jury management

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2024-03-30 15:55:28 +01:00
parent 37ad3cf8a6
commit 3fae6a00dd
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
2 changed files with 79 additions and 19 deletions

View File

@ -422,10 +422,14 @@ class Tournament(models.Model):
def create_spreadsheet(self): def create_spreadsheet(self):
if self.notes_sheet_id: if self.notes_sheet_id:
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.notes_sheet_id)
spreadsheet.update_locale("fr_FR")
return self.notes_sheet_id return self.notes_sheet_id
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT) 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"Feuille de notes - {self.name}", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
spreadsheet.update_locale("fr_FR")
self.notes_sheet_id = spreadsheet.id self.notes_sheet_id = spreadsheet.id
self.save() self.save()
@ -637,7 +641,7 @@ class Pool(models.Model):
for _passage in passages), start=["Juré⋅e", ""]), for _passage in passages), start=["Juré⋅e", ""]),
] ]
notes = [] notes = [[]] # Begin with empty hidden line to ensure pretty design
for jury in self.juries.all(): for jury in self.juries.all():
line = [str(jury), jury.id] line = [str(jury), jury.id]
for passage in passages: for passage in passages:
@ -647,6 +651,7 @@ class Pool(models.Model):
if pool_size == 4: if pool_size == 4:
line.append(note.observer_oral) line.append(note.observer_oral)
notes.append(line) notes.append(line)
notes.append([]) # Add empty line to ensure pretty design
def getcol(number: int) -> str: def getcol(number: int) -> str:
""" """
@ -662,15 +667,15 @@ class Pool(models.Model):
subtotal = ["Sous-total", ""] subtotal = ["Sous-total", ""]
footer = [average, coeffs, subtotal, 32 * [""]] footer = [average, coeffs, subtotal, 32 * [""]]
min_row = 4 min_row = 5
max_row = min_row + self.juries.count() - 1 max_row = min_row + self.juries.count()
min_column = 3 min_column = 3
for i, passage in enumerate(passages): for i, passage in enumerate(passages):
for j, note in enumerate(passage.averages): for j, note in enumerate(passage.averages):
column = getcol(min_column + i * passage_width + j) column = getcol(min_column + i * passage_width + j)
average.append(f"=MOYENNE.SI(${getcol(min_column + i * passage_width)}${min_row}" average.append(f"=MOYENNE.SI(${getcol(min_column + i * passage_width)}${min_row - 1}"
f":${getcol(min_column + i * passage_width)}{max_row}; \">0\"; " f":${getcol(min_column + i * passage_width)}{max_row}; \">0\"; "
f"{column}${min_row}:{column}{max_row})") f"{column}${min_row - 1}:{column}{max_row})")
def_w_col = getcol(min_column + passage_width * i) def_w_col = getcol(min_column + passage_width * i)
def_o_col = getcol(min_column + passage_width * i + 1) 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}" subtotal.extend([f"={def_w_col}{max_row + 1} * {def_w_col}{max_row + 2}"
@ -733,6 +738,7 @@ class Pool(models.Model):
all_values = header + notes + footer + ranking all_values = header + notes + footer + ranking
worksheet.batch_clear([f"A1:AF{max_row + 5 + pool_size}"])
worksheet.update("A1:AF", all_values, raw=False) worksheet.update("A1:AF", all_values, raw=False)
format_requests = [] format_requests = []
@ -765,19 +771,21 @@ class Pool(models.Model):
format_requests.append({"mergeCells": {"mergeType": MergeType.merge_all, "range": grid_range}}) format_requests.append({"mergeCells": {"mergeType": MergeType.merge_all, "range": grid_range}})
# Make titles bold # 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}"] bold_ranges = [("A1:AF", False), ("A1:AF3", True),
for bold_range in bold_ranges: (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({ format_requests.append({
"repeatCell": { "repeatCell": {
"range": a1_range_to_grid_range(bold_range, worksheet.id), "range": a1_range_to_grid_range(bold_range, worksheet.id),
"cell": {"userEnteredFormat": {"textFormat": {"bold": True}}}, "cell": {"userEnteredFormat": {"textFormat": {"bold": bold}}},
"fields": "userEnteredFormat(textFormat)", "fields": "userEnteredFormat(textFormat)",
} }
}) })
# Set background color for headers and footers # Set background color for headers and footers
bg_colors = [(f"A1:{getcol(2 + pool_size * passage_width)}3", (0.8, 0.8, 0.8)), bg_colors = [("A1:AF", (1, 1, 1)),
(f"A{min_row}:B{max_row}", (0.95, 0.95, 0.95)), (f"A1:{getcol(2 + pool_size * 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)), (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"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 + 5}:E{max_row + 5}", (0.8, 0.8, 0.8)),
@ -830,29 +838,50 @@ class Pool(models.Model):
} }
}) })
# Hide second column (Jury ID) # Hide second column (Jury ID) and first and last jury rows
hidden_dimensions = [(1, "COLUMNS"), (3, "ROWS"), (max_row - 1, "ROWS")]
format_requests.append({ format_requests.append({
"updateDimensionProperties": { "updateDimensionProperties": {
"range": { "range": {
"sheetId": worksheet.id, "sheetId": worksheet.id,
"dimension": "COLUMNS", "dimension": "ROWS",
"startIndex": 1, "startIndex": 0,
"endIndex": 2, "endIndex": 1000,
}, },
"properties": { "properties": {
"hiddenByUser": True, "hiddenByUser": False,
}, },
"fields": "hiddenByUser", "fields": "hiddenByUser",
} }
}) })
for dimension_id, dimension_type in hidden_dimensions:
format_requests.append({
"updateDimensionProperties": {
"range": {
"sheetId": worksheet.id,
"dimension": dimension_type,
"startIndex": dimension_id,
"endIndex": dimension_id + 1,
},
"properties": {
"hiddenByUser": True,
},
"fields": "hiddenByUser",
}
})
# Define borders # Define borders
border_ranges = [(f"A1:{getcol(2 + pool_size * passage_width)}{max_row + 3}", "1111"), border_ranges = [("A1:AF", "0000"),
(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"A{max_row + 5}:E{max_row + pool_size + 5}", "1111"),
(f"A1:B{max_row + 3}", "1113"), (f"A1:B{max_row + 3}", "1113"),
(f"C1:{getcol(2 + (pool_size - 1) * passage_width)}1", "1113")] (f"C1:{getcol(2 + (pool_size - 1) * passage_width)}1", "1113")]
for i in range(pool_size - 1): for i in range(pool_size - 1):
border_ranges.append((f"{getcol(2 + (i + 1) * passage_width)}2" border_ranges.append((f"{getcol(1 + (i + 1) * passage_width)}2"
f":{getcol(2 + (i + 1) * passage_width)}2", "1113"))
border_ranges.append((f"{getcol(2 + (i + 1) * passage_width)}3"
f":{getcol(2 + (i + 1) * passage_width)}{max_row + 2}", "1113"))
border_ranges.append((f"{getcol(1 + (i + 1) * passage_width)}{max_row + 3}"
f":{getcol(2 + (i + 1) * passage_width)}{max_row + 3}", "1113")) f":{getcol(2 + (i + 1) * passage_width)}{max_row + 3}", "1113"))
sides_names = ['top', 'bottom', 'left', 'right'] sides_names = ['top', 'bottom', 'left', 'right']
styles = ["NONE", "SOLID", "SOLID_MEDIUM", "SOLID_THICK", "DOUBLE"] styles = ["NONE", "SOLID", "SOLID_MEDIUM", "SOLID_THICK", "DOUBLE"]
@ -882,9 +911,9 @@ class Pool(models.Model):
}) })
# Protect the header, the juries list, the footer and the ranking # Protect the header, the juries list, the footer and the ranking
protected_ranges = ["A1:AF3", protected_ranges = ["A1:AF4",
f"A{min_row}:B{max_row}", f"A{min_row}:B{max_row}",
f"A{max_row + 1}:AF{max_row + 5 + pool_size}"] f"A{max_row}:AF{max_row + 5 + pool_size}"]
for protected_range in protected_ranges: for protected_range in protected_ranges:
format_requests.append({ format_requests.append({
"addProtectedRange": { "addProtectedRange": {
@ -900,6 +929,33 @@ class Pool(models.Model):
body = {"requests": format_requests} body = {"requests": format_requests}
worksheet.client.batch_update(spreadsheet.id, body) worksheet.client.batch_update(spreadsheet.id, body)
def update_juries_lines_spreadsheet(self):
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}")
average_cell = worksheet.find("Moyenne")
min_row = 5
max_row = average_cell.row - 1
juries_visible = worksheet.get(f"A{min_row}:B{max_row}")
juries_visible = [t for t in juries_visible if t and len(t) == 2]
rows_to_delete = []
for i, (_jury_name, jury_id) in enumerate(juries_visible):
if not jury_id.isnumeric() or int(jury_id) not in self.juries.values_list("id", flat=True):
rows_to_delete.append(min_row + i)
for row_to_delete in rows_to_delete:
worksheet.delete_rows(row_to_delete)
max_row -= len(rows_to_delete)
for jury in self.juries.all():
if str(jury.id) not in list(map(lambda x: x[1], juries_visible)):
worksheet.insert_row([str(jury), jury.id], max_row)
max_row += 1
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.update_juries_lines_spreadsheet()
super().save(force_insert, force_update, using, update_fields)
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,
@ -1265,6 +1321,9 @@ class Note(models.Model):
self.observer_oral = observer_oral self.observer_oral = observer_oral
def update_spreadsheet(self): def update_spreadsheet(self):
if not self.has_any_note():
return
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT) gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
passage = Passage.objects.prefetch_related('pool__tournament', 'pool__participations').get(pk=self.passage.pk) passage = Passage.objects.prefetch_related('pool__tournament', 'pool__participations').get(pk=self.passage.pk)
spreadsheet_id = passage.pool.tournament.notes_sheet_id spreadsheet_id = passage.pool.tournament.notes_sheet_id

View File

@ -1035,6 +1035,7 @@ class PoolRemoveJuryView(VolunteerMixin, DetailView):
jury = pool.juries.get(pk=kwargs['jury_id']) jury = pool.juries.get(pk=kwargs['jury_id'])
pool.juries.remove(jury) pool.juries.remove(jury)
pool.save() pool.save()
Note.objects.filter(jury=jury, passage__pool=pool).delete()
messages.success(request, _("The jury {name} has been successfully removed!") messages.success(request, _("The jury {name} has been successfully removed!")
.format(name=f"{jury.user.first_name} {jury.user.last_name}")) .format(name=f"{jury.user.first_name} {jury.user.last_name}"))
return redirect(reverse_lazy('participation:pool_jury', args=(pool.pk,))) return redirect(reverse_lazy('participation:pool_jury', args=(pool.pk,)))