def validate_sync(modifs): """Valide des modificatons sur le Tableau de bord (case plus en rouge). Args: modifs (list[.TDBModif]): liste des modifications à apporter. Modifie sur le Tableau de bord (variable d'environment ``LGREZ_TDB_SHEET_ID``) et applique les modifications contenues dans ``modifs``. """ SHEET_ID = env.load("LGREZ_TDB_SHEET_ID") workbook = gsheets.connect(SHEET_ID) # Tableau de bord sheet = workbook.worksheet(config.tdb_main_sheet) gsheets.update(sheet, *modifs)
def export_vote(vote, utilisation): """Enregistre un vote/les actions résolues dans le GSheet ad hoc. Écrit dans le GSheet ``LGREZ_DATA_SHEET_ID``. Peut être écrasé pour une autre implémentation. Args: vote (.bdd.Vote): le vote concerné, ou ``None`` pour une action. utilisation (.bdd.Utilisation): l'utilisation qui vient d'être effectuée. Doit être remplie (:attr:`.bdd.Utilisation.is_filled`). Raises: RuntimeError: si la variable d'environnement ``LGREZ_DATA_SHEET_ID`` n'est pas définie. """ if vote and not isinstance(vote, Vote): vote = Vote[vote] # str -> Vote joueur = utilisation.action.joueur if vote == Vote.cond: sheet_name = config.db_votecond_sheet data = [joueur.nom, utilisation.cible.nom] elif vote == Vote.maire: sheet_name = config.db_votemaire_sheet data = [joueur.nom, utilisation.cible.nom] elif vote == Vote.loups: sheet_name = config.db_voteloups_sheet data = [joueur.nom, joueur.camp.slug, utilisation.cible.nom] else: sheet_name = config.db_actions_sheet recap = "\n+\n".join( f"{action.base.slug}({last_util.decision})" for action in joueur.actions_actives if ((last_util := action.derniere_utilisation) and last_util.is_filled # action effectuée and last_util.ts_decision.date() == datetime.date.today()) # Et dernière décision aujourd'hui ==> on met dans le TDB ) data = [joueur.nom, joueur.role.slug, joueur.camp.slug, recap] LGREZ_DATA_SHEET_ID = env.load("LGREZ_DATA_SHEET_ID") sheet = gsheets.connect(LGREZ_DATA_SHEET_ID).worksheet(sheet_name) timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') sheet.append_row([timestamp, *data], value_input_option="USER_ENTERED")
def register_on_tdb(joueur): """Enregistre un joueur dans le Tableau de bord. Peut être personnalisé à un autre système de gestion des joueurs. Args: joueur (.bdd.Joueur): le joueur à enregistrer. """ SHEET_ID = env.load("LGREZ_TDB_SHEET_ID") workbook = gsheets.connect(SHEET_ID) sheet = workbook.worksheet(config.tdb_main_sheet) values = sheet.get_all_values() # Liste de listes head = values[config.tdb_header_row - 1] # Ligne d'en-têtes (noms des colonnes), - 1 car indexé à 0 id_index = gsheets.a_to_index(config.tdb_id_column) pk = head[id_index] if pk != Joueur.primary_col.key: raise ValueError("Tableau de bord : la cellule " "`config.tdb_id_column` / `config.tdb_header_row` = " f"`{config.tdb_id_column}{config.tdb_header_row}` " f"vaut `{pk}` au lieu de la clé primaire de la table " f"`Joueur`, `{Joueur.primary_col.key}` !") mstart, mstop = config.tdb_main_columns main_indexes = range(gsheets.a_to_index(mstart), gsheets.a_to_index(mstop) + 1) # Indices des colonnes à remplir cols = {} for index in main_indexes: col = head[index] if col in Joueur.attrs: cols[col] = Joueur.attrs[col] else: raise ValueError( f"Tableau de bord : l'index de la zone principale " f"`{col}` n'est pas une colonne de la table `Joueur` !" " (voir `lgrez.config.main_indexes` / " "`lgrez.config.tdb_header_row`)") tstart, tstop = config.tdb_tampon_columns tampon_indexes = range(gsheets.a_to_index(tstart), gsheets.a_to_index(tstop) + 1) TDB_tampon_index = {} for index in tampon_indexes: col = head[index].partition("_")[2] if col in cols: TDB_tampon_index[col] = index else: raise ValueError( f"Tableau de bord : l'index de zone tampon `{head[index]}` " f"réfère à la colonne `{col}` (partie suivant le premier " f"underscore), qui n'est pas une colonne de la zone " "principale ! (voir `lgrez.config.tampon_indexes` / " "`lgrez.config.main_indexes`)") plv = config.tdb_header_row # Première Ligne Vide # (si tableau vide, suit directement le header, - 1 pour l'indexage) for i_row, row in enumerate(values): # On parcourt les lignes du TDB if i_row < config.tdb_header_row: # Ligne avant le header / le header (car décalage de 1) continue if row[id_index].isdigit(): # Si il y a un vrai ID dans la colonne ID plv = i_row + 1 modifs = [gsheets.Modif(plv, id_index, joueur.discord_id)] for index in main_indexes: val = getattr(joueur, head[index]) modifs.append(gsheets.Modif(plv, index, val)) for index in tampon_indexes: val = getattr(joueur, head[index].partition("_")[2]) # Colonnes "tampon_<col>" ==> <col> modifs.append(gsheets.Modif(plv, index, val)) gsheets.update(sheet, *modifs)
async def fillroles(self, ctx): """Remplit les tables et #roles depuis le GSheet ad hoc (COMMANDE MJ) - Remplit les tables :class:`.bdd.Camp`, :class:`.bdd.Role`, :class:`.bdd.BaseAction` et :class:`.bdd.BaseCiblage` avec les informations du Google Sheets "Rôles et actions" (variable d'environnement ``LGREZ_ROLES_SHEET_ID``) ; - Vide le chan ``#roles`` puis le remplit avec les descriptifs de chaque rôle et camp. Utile à chaque début de saison / changement dans les rôles/actions. Met à jour les entrées déjà en base, créé les nouvelles, supprime celles obsolètes. """ # ==== Mise à jour tables === SHEET_ID = env.load("LGREZ_ROLES_SHEET_ID") workbook = gsheets.connect(SHEET_ID) # Rôles et actions for table in [Camp, Role, BaseAction]: await ctx.send( f"Remplissage de la table {tools.code(table.__name__)}...") # --- 1 : Récupération des valeurs async with ctx.typing(): try: sheet = workbook.worksheet(table.__tablename__) except gsheets.WorksheetNotFound: raise ValueError( f"!fillroles : feuille '{table.__tablename__}' non " "trouvée dans le GSheet *Rôles et actions* " "(`LGREZ_ROLES_SHEET_ID`)") from None values = sheet.get_all_values() # Liste de liste des valeurs des cellules # --- 2 : Détermination colonnes à récupérer cols = table.columns # "dictionnaire" nom -> colonne cols = { col: cols[col] for col in cols.keys() if not col.startswith("_") } # Colonnes publiques if table == Role: cols["camp"] = Role.attrs["camp"] elif table == BaseAction: # BaseCiblages : au bout de la feuille bc_cols = BaseCiblage.columns bc_cols = { col: bc_cols[col] for col in bc_cols.keys() if not col.startswith("_") } bc_cols_for_ith = [] for i in range(config.max_ciblages_per_action): prefix = f"c{i + 1}_" bc_cols_for_ith.append({ # colonne -> nom dans la table pour le ièmme col: f"{prefix}{col}" for col in bc_cols }) primary_key = table.primary_col.key # --- 3 : Localisation des colonnes (indices GSheet) cols_index = {} try: for key in cols.keys(): cols_index[key] = values[0].index(key) if table == BaseAction: key = "roles" roles_idx = values[0].index(key) # BaseCiblages : au bout de la feuille ciblages_idx = [] for bc_cols_names in bc_cols_for_ith: idx = {} for col, key in bc_cols_names.items(): idx[col] = values[0].index(key) ciblages_idx.append(idx) except ValueError: raise ValueError( f"!fillroles : colonne '{key}' non trouvée dans " f"la feuille '{table.__tablename__}' du GSheet " "*Rôles et actions* (`LGREZ_ROLES_SHEET_ID`)") from None # --- 4 : Constrution dictionnaires de comparaison existants = {item.primary_key: item for item in table.query.all()} new = {} for row in values[1:]: args = { key: transtype(row[cols_index[key]], col) for key, col in cols.items() } if table == BaseAction: # Many-to-many BaseAction <-> Rôle roles = row[roles_idx].strip() if roles.startswith("#"): args["roles"] = [] else: args["roles"] = [ transtype(slug.strip(), Role) for slug in roles.split(",") if slug ] # BaseCiblages new_bcs = [] for idx in ciblages_idx: if row[idx["slug"]]: # ciblage défini bc_args = { key: transtype(row[idx[key]], col) for key, col in bc_cols.items() } new_bcs.append(bc_args) args["base_ciblages"] = new_bcs new[args[primary_key]] = args # --- 5 : Comparaison et MAJ res = _compare_items( existants, new, table, cols, primary_key, bc_cols=bc_cols if table == BaseAction else None) config.session.commit() await ctx.send(f"> Table {tools.code(table.__name__)} remplie ! " + res.bilan) await tools.log(f"`!fillroles` > {table.__name__}:\n{res.log}") if table == BaseAction: sub_res = res.sub_results await ctx.send(f"> Table {tools.code('BaseCiblage')} remplie " "simultanément " + sub_res.bilan) await tools.log(f"`!fillroles` > BaseCiblage:\n{sub_res.log}") # ==== Remplissage #rôles === chan_roles = config.Channel.roles mess = await ctx.send(f"Purger et re-remplir {chan_roles.mention} ?") if not await tools.yes_no(mess): await ctx.send(f"Fini (voir {config.Channel.logs.mention}).") return await ctx.send(f"Vidage de {chan_roles.mention}...") async with ctx.typing(): await chan_roles.purge(limit=1000) camps = Camp.query.filter_by(public=True).all() est = sum(len(camp.roles) + 2 for camp in camps) + 1 await ctx.send(f"Remplissage... (temps estimé : {est} secondes)") t0 = time.time() await chan_roles.send("Voici la liste des rôles " f"(voir aussi {tools.code('!roles')}) :") async with ctx.typing(): shortcuts = [] for camp in camps: if not camp.roles: continue embed = Embed(title=f"Camp : {camp.nom}", description=camp.description, color=0x64b9e9) if (emoji := camp.discord_emoji_or_none): embed.set_image(url=emoji.url) mess = await chan_roles.send(camp.nom, embed=embed) shortcuts.append(mess) for role in camp.roles: if role.actif: await chan_roles.send(embed=role.embed) for mess in shortcuts: await mess.reply("Accès rapide : " "\N{UPWARDS BLACK ARROW}")
def get_sync(): """Récupère les modifications en attente sur le TDB. Charge les données du Tableau de bord (variable d'environment ``LGREZ_TDB_SHEET_ID``), compare les informations qui y figurent avec celles de la base de données (:class:`.bdd.Joueur`). Supprime les joueurs en base absents du Tableau de bord, lève une erreur dans le cas inverse, n'applique aucune autre modification. Returns: list[.TDBModif]: La liste des modifications à apporter """ # RÉCUPÉRATION INFOS GSHEET ET VÉRIFICATIONS SHEET_ID = env.load("LGREZ_TDB_SHEET_ID") workbook = gsheets.connect(SHEET_ID) sheet = workbook.worksheet(config.tdb_main_sheet) values = sheet.get_all_values() # Liste de listes head = values[config.tdb_header_row - 1] # Ligne d'en-têtes (noms des colonnes), - 1 car indexé à 0 id_index = gsheets.a_to_index(config.tdb_id_column) pk = head[id_index] if pk != Joueur.primary_col.key: raise ValueError("Tableau de bord : la cellule " "`config.tdb_id_column` / `config.tdb_header_row` = " f"`{config.tdb_id_column}{config.tdb_header_row}` " f"vaut `{pk}` au lieu de la clé primaire de la table " f"`Joueur`, `{Joueur.primary_col.key}` !") mstart, mstop = config.tdb_main_columns main_indexes = range(gsheets.a_to_index(mstart), gsheets.a_to_index(mstop) + 1) # Indices des colonnes à remplir cols = {} for index in main_indexes: col = head[index] if col in Joueur.attrs: cols[col] = Joueur.attrs[col] else: raise ValueError( f"Tableau de bord : l'index de la zone principale " f"`{col}` n'est pas une colonne de la table `Joueur` !" " (voir `lgrez.config.main_indexes` / " "`lgrez.config.tdb_header_row`)") tstart, tstop = config.tdb_tampon_columns tampon_indexes = range(gsheets.a_to_index(tstart), gsheets.a_to_index(tstop) + 1) TDB_tampon_index = {} for index in tampon_indexes: col = head[index].partition("_")[2] if col in cols: TDB_tampon_index[col] = index else: raise ValueError( f"Tableau de bord : l'index de zone tampon `{head[index]}` " f"réfère à la colonne `{col}` (partie suivant le premier " f"underscore), qui n'est pas une colonne de la zone " "principale ! (voir `lgrez.config.tampon_indexes` / " "`lgrez.config.main_indexes`)") # CONVERSION INFOS GSHEET EN PSEUDO-UTILISATEURS joueurs_TDB = [] # Joueurs tels qu'actuellement dans le TDB ids_TDB = [] # discord_ids des différents joueurs du TDB rows_TDB = {} # Lignes ou sont les différents joueurs du TDB for i_row, row in enumerate(values): # On parcourt les lignes du TDB if i_row < config.tdb_header_row: # Ligne avant le header / le header (car décalage de 1) continue id_cell = row[id_index] if not id_cell.isdigit(): # La cellule ne contient pas un ID ==> skip continue id = int(id_cell) # Construction dictionnaire correspondant à l'utilisateur joueur_TDB = { head[index]: transtype(row[index], cols[head[index]]) for index in main_indexes } joueur_TDB[pk] = id joueurs_TDB.append(joueur_TDB) ids_TDB.append(id) rows_TDB[id] = i_row # RÉCUPÉRATION UTILISATEURS BDD joueurs_BDD = {joueur.discord_id: joueur for joueur in Joueur.query.all()} # COMPARAISON for id, joueur in list(joueurs_BDD.items()): if id not in ids_TDB: # Joueur en base supprimé du TDB del joueurs_BDD[id] joueur.delete() modifs = [] # modifs à porter au TDB (liste de TDBModifs) for joueur_TDB in joueurs_TDB: # Différences id = joueur_TDB[pk] try: joueur = joueurs_BDD[id] except KeyError: # Joueur en base pas dans le TDB raise ValueError(f"Joueur `{joueur_TDB['nom']}` hors base : " "vérifier processus d'inscription") from None for col in cols: if getattr(joueur, col) != joueur_TDB[col]: # Si <col> diffère entre TDB et cache, # on ajoute la modif (avec update du tampon) modifs.append( TDBModif(id=id, col=col, val=joueur_TDB[col], row=rows_TDB[id], column=TDB_tampon_index[col])) return modifs