Votación de temas Autovoto automático

:information_source: Resumen Habilitar votos automáticos de temas cuando un usuario crea un tema
:hammer_and_wrench: Repositorio GitHub - dereklputnam/discourse-topic-voting-auto-self-vote
:question: Guía de instalación Cómo instalar un tema o componente de tema
:open_book: ¿Nuevo en los temas de Discourse? Guía para principiantes sobre el uso de temas de Discourse

Instalar este componente de tema

Si tu comunidad tiene un número limitado de votos para emitir, como aquí en Meta, tiene sentido que el voto del OP (autor del tema) no se emita automáticamente (ver discusión aquí). Pero para las comunidades con votación ilimitada, es un punto de fricción, ¡y los usuarios pueden ni siquiera votar en su propio tema! De ahí la necesidad de este componente.

Configuración

  • Categorías de voto automático: si no deseas que esto esté habilitado en todo el sitio, especifica a qué categorías debe aplicarse.
  • Grupos excluidos: si no deseas que esto se aplique a grupos específicos (quizás equipos internos), agrégalos aquí.
  • Voto automático al visitar: una forma suave de limpiar cualquier tema existente donde el OP no votó.

Relleno de temas anteriores (Backfilling topics)

Identificar los temas

Utiliza esta consulta del explorador de datos para identificar temas donde el OP no votó:

-- [params]
-- null category_id :category_id
-- null string :category_slug

SELECT
  t.id AS topic_id,
  u.username AS "Username",
  t.title AS "Topic Title",
  t.user_id AS "Author",
  t.created_at AS "Created At",
  c.name AS "Category Name",
  c.slug AS "Category Slug"
FROM topics t
JOIN users u ON u.id = t.user_id
JOIN categories c ON c.id = t.category_id
JOIN topic_voting_topic_vote_count tvvc ON tvvc.topic_id = t.id
LEFT JOIN topic_voting_votes tvv ON tvv.topic_id = t.id AND tvv.user_id = t.user_id
WHERE tvv.id IS NULL
  AND t.deleted_at IS NULL
  AND (
    :category_id IS NULL OR c.id = :category_id
  )
  AND (
    :category_slug IS NULL OR c.slug = :category_slug
  )

Consejo: Vuelve a ejecutar esta consulta después de actualizar tus temas para verificar que se emitieron los votos :ballot_box:

Relleno mediante API

A partir de ahí, ¡utiliza esa lista y ejecuta un script de API para rellenar los votos! Para usar mi script a continuación, deberás recortar las columnas más allá de las dos primeras. Las dejé en la consulta del explorador de datos para que sea más fácil identificar los temas en toda tu comunidad.

Nota: tu clave de API debe tener permisos para todos los usuarios, ya que está suplantando a cada uno de ellos.

No tengo permiso para adjuntar el script que utilicé, pero aquí está en texto:

Script de relleno (Backfill script)
#!/usr/bin/env python3
"""
Relleno de Votos Propios a través de la API de Discourse

Este script emite votos para los autores de temas que no han votado en sus propios temas.
Utiliza la API de Discourse para suplantar a los usuarios y emitir votos en su nombre.

Requisitos:
- Python 3.7+
- Librería requests (pip install requests)
- Una clave de API de administrador con permisos de suplantación

Uso:
1. Actualiza la sección de configuración a continuación
2. Prepara un archivo CSV con las columnas topic_id y username
3. Ejecuta: python backfill_votes_api.py

Formato CSV:
    topic_id,username
    12345,john_doe
    12346,jane_smith
"""

import csv
import time
import requests
from datetime import datetime

#==============================================================================
# CONFIGURACIÓN
#==============================================================================

# URL de la instancia de Discourse (sin barra al final)
DISCOURSE_URL = 'https://community.netwrix.com'

# Clave de API de administrador (debe tener permisos de suplantación)
API_KEY = 'YOUR_API_KEY_HERE'

# Nombre de usuario de administrador que posee la clave de API
API_USERNAME = 'system'

# Ruta al archivo CSV con las columnas topic_id y username
CSV_FILE = 'topics_to_vote.csv'

# Modo de prueba (Dry run) - establecer en False para emitir votos reales
DRY_RUN = True

# Retardo entre llamadas a la API (segundos) para evitar la limitación de velocidad
DELAY_BETWEEN_REQUESTS = 0.5

#==============================================================================
# SCRIPT
#==============================================================================

def cast_vote(topic_id: int, username: str) -> dict:
    """
    Emite un voto en un tema como un usuario específico.

    Args:
        topic_id: El ID del tema para votar
        username: El nombre de usuario a suplantar al votar

    Returns:
        Diccionario con booleano 'success' y cadena 'message'
    """
    url = f"{DISCOURSE_URL}/voting/vote"

    headers = {
        'Api-Key': API_KEY,
        'Api-Username': username,  # Suplantar al usuario
        'Content-Type': 'application/json'
    }

    data = {
        'topic_id': topic_id
    }

    try:
        response = requests.post(url, headers=headers, json=data)

        if response.status_code == 200:
            return {'success': True, 'message': 'Vote cast successfully'}
        elif response.status_code == 422:
            # Usualmente significa que ya votó o la votación no está habilitada
            return {'success': False, 'message': 'Already voted or voting not enabled'}
        elif response.status_code == 403:
            return {'success': False, 'message': 'Permission denied - check API key permissions'}
        elif response.status_code == 404:
            return {'success': False, 'message': 'Topic not found or voting not enabled on category'}
        else:
            return {'success': False, 'message': f'HTTP {response.status_code}: {response.text[:200]}'}

    except requests.RequestException as e:
        return {'success': False, 'message': f'Request error: {str(e)}'}

def main():
    print("\n" + "=" * 60)
    print("Relleno de Votos Propios vía API")
    print("=" * 60)
    print(f"Modo: {'PRUEBA (no se realizarán cambios)' if DRY_RUN else 'EN VIVO (se emitirán votos)'}")
    print(f"Objetivo: {DISCOURSE_URL}")
    print(f"Archivo CSV: {CSV_FILE}")
    print("=" * 60 + "\n")

    # Leer archivo CSV
    try:
        with open(CSV_FILE, 'r', newline='', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            rows = list(reader)
    except FileNotFoundError:
        print(f"ERROR: Archivo CSV no encontrado: {CSV_FILE}")
        print("\nCrea un archivo CSV con el siguiente formato:")
        print("topic_id,username")
        print("12345,john_doe")
        print("12346,jane_smith")
        return
    except Exception as e:
        print(f"ERROR: Falló la lectura del archivo CSV: {e}")
        return

    if not rows:
        print("ERROR: El archivo CSV está vacío")
        return

    # Validar columnas del CSV - soporta tanto 'username' como 'Author Username'
    columns = set(rows[0].keys())
    username_col = 'username' if 'username' in columns else 'Author Username'

    if 'topic_id' not in columns or username_col not in columns:
        print(f"ERROR: El CSV debe tener las columnas: topic_id y (username o 'Author Username')")
        print(f"Columnas encontradas: {columns}")
        return

    print(f"Encontrados {len(rows)} temas para procesar\n")

    # Procesar cada fila
    success_count = 0
    skip_count = 0
    error_count = 0

    for i, row in enumerate(rows, 1):
        topic_id = row['topic_id'].strip()
        username = row.get('username', row.get('Author Username', '')).strip()

        print(f"[{i}/{len(rows)}] Tema #{topic_id} por @{username}", end=" ")

        if DRY_RUN:
            print("-> votaría")
            success_count += 1
        else:
            result = cast_vote(int(topic_id), username)

            if result['success']:
                print("-> ¡votado!")
                success_count += 1
            elif 'Already voted' in result['message']:
                print("-> ya votado (omitido)")
                skip_count += 1
            else:
                print(f"-> ERROR: {result['message']}")
                error_count += 1

            # Limitación de velocidad
            if i < len(rows):
                time.sleep(DELAY_BETWEEN_REQUESTS)

    # Resumen
    print("\n" + "=" * 60)
    print("RESUMEN")
    print("=" * 60)
    print(f"Temas totales: {len(rows)}")
    print(f"Votos {'a emitir' if DRY_RUN else 'emitidos'}: {success_count}")
    print(f"Ya votados (omitidos): {skip_count}")
    print(f"Errores: {error_count}")

    if DRY_RUN:
        print("\n** PRUEBA COMPLETADA **")
        print("Para emitir votos, establece DRY_RUN = False y vuelve a ejecutar.")

    print("")


if __name__ == '__main__':
    main()
1 me gusta