From 3fae6a00dd255700e8bcf4fdcac9964c4c6e6454 Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Sat, 30 Mar 2024 15:55:28 +0100 Subject: [PATCH] Auto update Google Sheet after jury management Signed-off-by: Emmy D'Anello --- participation/models.py | 97 +++++++++++++++++++++++++++++++++-------- participation/views.py | 1 + 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/participation/models.py b/participation/models.py index 32d7b7d..1edf492 100644 --- a/participation/models.py +++ b/participation/models.py @@ -422,10 +422,14 @@ class Tournament(models.Model): def create_spreadsheet(self): 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 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.update_locale("fr_FR") self.notes_sheet_id = spreadsheet.id self.save() @@ -637,7 +641,7 @@ class Pool(models.Model): for _passage in passages), start=["Juré⋅e", ""]), ] - notes = [] + notes = [[]] # Begin with empty hidden line to ensure pretty design for jury in self.juries.all(): line = [str(jury), jury.id] for passage in passages: @@ -647,6 +651,7 @@ class Pool(models.Model): if pool_size == 4: line.append(note.observer_oral) notes.append(line) + notes.append([]) # Add empty line to ensure pretty design def getcol(number: int) -> str: """ @@ -662,15 +667,15 @@ class Pool(models.Model): subtotal = ["Sous-total", ""] footer = [average, coeffs, subtotal, 32 * [""]] - min_row = 4 - max_row = min_row + self.juries.count() - 1 + min_row = 5 + max_row = min_row + self.juries.count() 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}" + 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"{column}${min_row}:{column}{max_row})") + f"{column}${min_row - 1}:{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}" @@ -733,6 +738,7 @@ class Pool(models.Model): 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) format_requests = [] @@ -765,19 +771,21 @@ class Pool(models.Model): 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: + bold_ranges = [("A1:AF", False), ("A1:AF3", 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({ "repeatCell": { "range": a1_range_to_grid_range(bold_range, worksheet.id), - "cell": {"userEnteredFormat": {"textFormat": {"bold": True}}}, + "cell": {"userEnteredFormat": {"textFormat": {"bold": bold}}}, "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)), + bg_colors = [("A1:AF", (1, 1, 1)), + (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"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)), @@ -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({ "updateDimensionProperties": { "range": { "sheetId": worksheet.id, - "dimension": "COLUMNS", - "startIndex": 1, - "endIndex": 2, + "dimension": "ROWS", + "startIndex": 0, + "endIndex": 1000, }, "properties": { - "hiddenByUser": True, + "hiddenByUser": False, }, "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 - 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"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" + 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")) sides_names = ['top', 'bottom', 'left', 'right'] 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 - protected_ranges = ["A1:AF3", + protected_ranges = ["A1:AF4", 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: format_requests.append({ "addProtectedRange": { @@ -900,6 +929,33 @@ class Pool(models.Model): body = {"requests": format_requests} 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): return _("Pool of day {round} for tournament {tournament} with teams {teams}")\ .format(round=self.round, @@ -1265,6 +1321,9 @@ class Note(models.Model): self.observer_oral = observer_oral def update_spreadsheet(self): + if not self.has_any_note(): + return + gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT) passage = Passage.objects.prefetch_related('pool__tournament', 'pool__participations').get(pk=self.passage.pk) spreadsheet_id = passage.pool.tournament.notes_sheet_id diff --git a/participation/views.py b/participation/views.py index b171e83..85e45ec 100644 --- a/participation/views.py +++ b/participation/views.py @@ -1035,6 +1035,7 @@ class PoolRemoveJuryView(VolunteerMixin, DetailView): jury = pool.juries.get(pk=kwargs['jury_id']) pool.juries.remove(jury) pool.save() + Note.objects.filter(jury=jury, passage__pool=pool).delete() messages.success(request, _("The jury {name} has been successfully removed!") .format(name=f"{jury.user.first_name} {jury.user.last_name}")) return redirect(reverse_lazy('participation:pool_jury', args=(pool.pk,)))