Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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")
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
    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}")
Exemplo n.º 5
0
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