| Краткое описание | Включение автоматических голосов за темы при их создании пользователем | |
| Репозиторий | GitHub - dereklputnam/discourse-topic-voting-auto-self-vote · GitHub | |
| Руководство по установке | Как установить тему или компонент темы | |
| Новичок в темах Discourse? | Начинающее руководство по использованию тем Discourse |
Установить этот компонент темы
Если в вашем сообществе количество голосов ограничено, как, например, здесь на Meta, логично, чтобы голос автора темы (OP) не выставлялся автоматически (см. обсуждение здесь и здесь). Однако для сообществ с неограниченным голосованием это становится проблемой, и пользователи могут даже не голосовать за свою собственную тему! Отсюда и необходимость в этом компоненте.
Настройки
- Категории автоголосования: если вы не хотите включать эту функцию для всего сайта, укажите, к каким категориям она должна применяться.
- Исключённые группы: если вы не хотите применять это к определённым группам (например, внутренним командам), добавьте их сюда.
- Автоголосование при посещении: мягкий способ исправить существующие темы, где автор не проголосовал.
Заполнение пропущенных голосов
Идентификация тем
Используйте следующий запрос в исследователе данных, чтобы найти темы, где автор не проголосовал:
-- [params]
-- null category_id :category_id
-- null string :category_slug
-- boolean :exclude_about = true
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
LEFT 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 t.deleted_at IS NULL
AND (
tvvc.votes_count IS NULL OR tvv.id IS NULL
)
AND (
:exclude_about = false
OR t.title ILIKE 'about the % category' = false
)
AND (
:category_id IS NULL OR c.id = :category_id
)
AND (
:category_slug IS NULL OR c.slug = :category_slug
)
Совет: После обновления тем запустите этот запрос повторно, чтобы убедиться, что голоса были выставлены
Заполнение через API
Далее используйте этот список и запустите скрипт API для заполнения пропущенных голосов! Чтобы использовать мой скрипт ниже, вам нужно удалить все столбцы, кроме первых двух. Я оставил их в запросе исследователя данных, чтобы было проще идентифицировать темы в вашем сообществе.
Примечание: Ваш API-ключ должен быть разрешён для всех пользователей, поскольку он имитирует каждого из них.
У меня нет прав прикрепить использованный мной скрипт, но вот его текст:
Скрипт заполнения
#!/usr/bin/env python3
"""
Заполнение самоголосования через API Discourse
Этот скрипт выставляет голоса за авторов тем, которые не голосовали за свои собственные темы.
Он использует API Discourse для имитации пользователей и выставления голосов от их имени.
Требования:
- Python 3.7+
- библиотека requests (pip install requests)
- Административный API-ключ с правами на имитацию
Использование:
1. Обновите секцию конфигурации ниже
2. Подготовьте CSV-файл со столбцами topic_id и username
3. Запустите: python backfill_votes_api.py
Формат CSV:
topic_id,username
12345,john_doe
12346,jane_smith
"""
import csv
import time
import requests
from datetime import datetime
#==============================================================================
# КОНФИГУРАЦИЯ
#==============================================================================
# URL экземпляра Discourse (без завершающего слэша)
DISCOURSE_URL = 'https://yourcommunity.com'
# Административный API-ключ (должен иметь права на имитацию)
API_KEY = 'YOUR_API_KEY_HERE'
# Имя пользователя администратора, которому принадлежит API-ключ
API_USERNAME = 'system'
# Путь к CSV-файлу со столбцами topic_id и username
CSV_FILE = 'topics_to_vote.csv'
# Режим сухой проверки — установите False, чтобы реально выставлять голоса
DRY_RUN = True
# Задержка между вызовами API (секунды) для избежания ограничений скорости
DELAY_BETWEEN_REQUESTS = 0.5
#==============================================================================
# СКРИПТ
#==============================================================================
def cast_vote(topic_id: int, username: str) -> dict:
"""
Выставить голос за тему от имени конкретного пользователя.
Аргументы:
topic_id: ID темы, за которую нужно проголосовать
username: Имя пользователя для имитации при голосовании
Возвращает:
Словарь с булевым значением 'success' и строкой 'message'
"""
url = f"{DISCOURSE_URL}/voting/vote"
headers = {
'Api-Key': API_KEY,
'Api-Username': username, # Имитация пользователя
'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': 'Голос успешно выставлен'}
elif response.status_code == 422:
# Обычно означает, что уже проголосовали или голосование не включено
return {'success': False, 'message': 'Уже проголосовали или голосование не включено'}
elif response.status_code == 403:
return {'success': False, 'message': 'Отказано в доступе — проверьте права API-ключа'}
elif response.status_code == 404:
return {'success': False, 'message': 'Тема не найдена или голосование не включено в категории'}
else:
return {'success': False, 'message': f'HTTP {response.status_code}: {response.text[:200]}'}
except requests.RequestException as e:
return {'success': False, 'message': f'Ошибка запроса: {str(e)}'}
def main():
print("\n" + "=" * 60)
print("Заполнение самоголосования через API")
print("=" * 60)
print(f"Режим: {'СУХАЯ ПРОВЕРКА (без изменений)' if DRY_RUN else 'ЖИВОЙ (голоса будут выставлены)'}")
print(f"Цель: {DISCOURSE_URL}")
print(f"CSV-файл: {CSV_FILE}")
print("=" * 60 + "\n")
# Чтение CSV-файла
try:
with open(CSV_FILE, 'r', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
rows = list(reader)
except FileNotFoundError:
print(f"ОШИБКА: CSV-файл не найден: {CSV_FILE}")
print("\nСоздайте CSV-файл со следующим форматом:")
print("topic_id,username")
print("12345,john_doe")
print("12346,jane_smith")
return
except Exception as e:
print(f"ОШИБКА: Не удалось прочитать CSV-файл: {e}")
return
if not rows:
print("ОШИБКА: CSV-файл пуст")
return
# Проверка столбцов CSV
columns = set(rows[0].keys())
if 'topic_id' not in columns or 'username' not in columns:
print(f"ОШИБКА: CSV должен содержать столбцы: topic_id и username")
print(f"Найденные столбцы: {columns}")
return
print(f"Найдено тем для обработки: {len(rows)}\n")
# Обработка каждой строки
success_count = 0
skip_count = 0
error_count = 0
for i, row in enumerate(rows, 1):
topic_id = row['topic_id'].strip()
username = row['username'].strip()
print(f"[{i}/{len(rows)}] Тема #{topic_id} от @{username}", end=" ")
if DRY_RUN:
print("-> было бы выставлено")
success_count += 1
else:
result = cast_vote(int(topic_id), username)
if result['success']:
print("-> голос выставлен!")
success_count += 1
elif 'Уже проголосовали' in result['message']:
print("-> уже проголосовали (пропущено)")
skip_count += 1
else:
print(f"-> ОШИБКА: {result['message']}")
error_count += 1
# Ограничение скорости
if i < len(rows):
time.sleep(DELAY_BETWEEN_REQUESTS)
# Итоги
print("\n" + "=" * 60)
print("ИТОГИ")
print("=" * 60)
print(f"Всего тем: {len(rows)}")
print(f"Голосов {'должно быть выставлено' if DRY_RUN else 'выставлено'}: {success_count}")
print(f"Уже проголосовали (пропущено): {skip_count}")
print(f"Ошибки: {error_count}")
if DRY_RUN:
print("\n** СУХАЯ ПРОВЕРКА ЗАВЕРШЕНА **")
print("Чтобы выставить голоса, установите DRY_RUN = False и запустите снова.")
print("")
if __name__ == '__main__':
main()
