Files
NovaWoW/forum/views.py
T
Inna 874a0c1ad9 Foro: editor CKEditor 5 en los mensajes y moderación por POST+CSRF
- forum/forms.py: TopicForm/ReplyForm/EditPostForm con CKEditor5Widget (rich text)
- vistas crear/responder/editar pasan el form; el HTML del editor se sanea con nh3
- acciones de moderación (bloquear, fijar, mover, borrar/restaurar temas y posts)
  ahora exigen POST y van con CSRF; las plantillas usan formularios en vez de enlaces GET
- docs/FORO.md actualizado

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:15:35 +00:00

386 lines
13 KiB
Python

"""
Vistas del foro. Diseño integrado con NovaWoW (usa los partials del sitio).
Identidad del autor = cuenta de juego en sesión (``username`` / ``account_id``).
Permisos: ver `permissions.py`.
"""
import math
from django.conf import settings
from django.contrib import messages
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from . import db
from . import permissions as perm
from . import sanitize
from .forms import TopicForm, ReplyForm, EditPostForm
MIN_TEXT_LEN = 5
# --------------------------------------------------------------------------
# Utilidades
# --------------------------------------------------------------------------
def _paginate(total, page, per_page):
total_pages = max(1, math.ceil(total / per_page)) if per_page else 1
page = max(1, min(page, total_pages))
offset = (page - 1) * per_page
return {
'page': page,
'total_pages': total_pages,
'has_prev': page > 1,
'has_next': page < total_pages,
'prev_page': page - 1,
'next_page': page + 1,
'page_range': range(1, total_pages + 1),
'total': total,
}, offset
def _base_context(request, extra=None):
ctx = dict(perm.forum_context(request))
if extra:
ctx.update(extra)
return ctx
def _require_login(request):
if not perm.is_authenticated(request):
return redirect('login')
return None
# --------------------------------------------------------------------------
# Navegación
# --------------------------------------------------------------------------
def index(request):
categories = db.get_index(include_hidden=perm.is_moderator(request))
return render(request, 'forum/index.html', _base_context(request, {
'categories': categories,
}))
def view_forum(request, forum_id, page=1):
is_mod = perm.is_moderator(request)
forum = db.get_forum(forum_id, include_hidden=is_mod)
if not forum:
messages.error(request, 'Foro no encontrado.')
return redirect('app_forum')
total = db.count_topics(forum_id)
pag, offset = _paginate(total, page, settings.FORUM_TOPICS_PER_PAGE)
topics = db.get_topics(forum_id, offset, settings.FORUM_TOPICS_PER_PAGE)
return render(request, 'forum/forum.html', _base_context(request, {
'forum': forum,
'topics': topics,
'pagination': pag,
}))
def view_topic(request, topic_id, page=1):
topic = db.get_topic(topic_id, include_hidden=perm.is_moderator(request))
if not topic:
messages.error(request, 'Tema no encontrado.')
return redirect('app_forum')
forum = db.get_forum(topic['forum_id'], include_hidden=True)
total = db.count_posts(topic_id)
pag, offset = _paginate(total, page, settings.FORUM_POSTS_PER_PAGE)
include_deleted = perm.is_moderator(request)
posts = db.get_posts(topic_id, offset, settings.FORUM_POSTS_PER_PAGE, include_deleted)
# Marcar como leído
db.mark_read(perm.get_account_id(request), topic_id)
# Permisos y datos de autor por post
for p in posts:
p['can_edit'] = perm.can_edit_post(request, p)
p['author'] = db.get_author_info(p['poster_id'])
return render(request, 'forum/topic.html', _base_context(request, {
'topic': topic,
'forum': forum,
'posts': posts,
'pagination': pag,
'can_reply': perm.can_post(request) and not topic['locked'],
'move_forums': db.list_forums_for_move(topic['forum_id']) if perm.is_moderator(request) else [],
'reply_form': ReplyForm(),
}))
# --------------------------------------------------------------------------
# Crear tema / responder
# --------------------------------------------------------------------------
def create_topic(request, forum_id):
login = _require_login(request)
if login:
return login
forum = db.get_forum(forum_id, include_hidden=perm.is_moderator(request))
if not forum:
messages.error(request, 'Foro no encontrado.')
return redirect('app_forum')
return render(request, 'forum/create_topic.html', _base_context(request, {
'forum': forum,
'form': TopicForm(),
}))
def submit_create_topic(request, forum_id):
login = _require_login(request)
if login:
return login
if request.method != 'POST':
return redirect('forum_create_topic', forum_id=forum_id)
forum = db.get_forum(forum_id, include_hidden=perm.is_moderator(request))
if not forum:
messages.error(request, 'Foro no encontrado.')
return redirect('app_forum')
name = (request.POST.get('name') or '').strip()
text = sanitize.clean_post_html((request.POST.get('text') or '').strip())
if not name or sanitize.plain_length(text) < MIN_TEXT_LEN:
messages.error(request, 'El título y el contenido (mínimo 5 caracteres) son obligatorios.')
return redirect('forum_create_topic', forum_id=forum_id)
if len(name) > 45:
messages.error(request, 'El título no puede superar los 45 caracteres.')
return redirect('forum_create_topic', forum_id=forum_id)
poster = request.session.get('username')
poster_id = perm.get_account_id(request)
topic_id = db.create_topic(forum_id, name, poster, poster_id, text)
messages.success(request, 'Tema creado.')
return redirect('forum_topic_p1', topic_id=topic_id)
def reply_to_topic(request, topic_id):
login = _require_login(request)
if login:
return login
if request.method != 'POST':
return redirect('forum_topic_p1', topic_id=topic_id)
topic = db.get_topic(topic_id)
if not topic:
messages.error(request, 'Tema no encontrado.')
return redirect('app_forum')
if topic['locked'] and not perm.is_moderator(request):
messages.error(request, 'El tema está bloqueado.')
return redirect('forum_topic_p1', topic_id=topic_id)
text = sanitize.clean_post_html((request.POST.get('text') or '').strip())
if sanitize.plain_length(text) < MIN_TEXT_LEN:
messages.error(request, 'El mensaje debe tener al menos 5 caracteres.')
return redirect('forum_topic_p1', topic_id=topic_id)
db.create_post(topic_id, request.session.get('username'), perm.get_account_id(request), text)
# Ir a la última página
total = db.count_posts(topic_id)
last_page = max(1, math.ceil(total / settings.FORUM_POSTS_PER_PAGE))
url = reverse('forum_topic', kwargs={'topic_id': topic_id, 'page': last_page})
return redirect(f'{url}#post-bottom')
# --------------------------------------------------------------------------
# Editar / borrar / restaurar posts
# --------------------------------------------------------------------------
def edit_post(request, post_id):
login = _require_login(request)
if login:
return login
post = db.get_post(post_id)
if not post:
messages.error(request, 'Post no encontrado.')
return redirect('app_forum')
if not perm.can_edit_post(request, post):
messages.error(request, 'No tienes permiso para editar este post.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
return render(request, 'forum/edit_post.html', _base_context(request, {
'post': post,
'form': EditPostForm(initial={'text': post['text']}),
}))
def update_post(request, post_id):
login = _require_login(request)
if login:
return login
post = db.get_post(post_id)
if not post:
messages.error(request, 'Post no encontrado.')
return redirect('app_forum')
if not perm.can_edit_post(request, post):
messages.error(request, 'No tienes permiso para editar este post.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
if request.method != 'POST':
return redirect('forum_edit_post', post_id=post_id)
text = sanitize.clean_post_html((request.POST.get('text') or '').strip())
if sanitize.plain_length(text) < MIN_TEXT_LEN:
messages.error(request, 'El mensaje debe tener al menos 5 caracteres.')
return redirect('forum_edit_post', post_id=post_id)
db.update_post(post_id, text)
messages.success(request, 'Post actualizado.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
def delete_post(request, post_id):
login = _require_login(request)
if login:
return login
post = db.get_post(post_id)
if not post:
messages.error(request, 'Post no encontrado.')
return redirect('app_forum')
if not perm.can_edit_post(request, post):
messages.error(request, 'No tienes permiso.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
if request.method != 'POST':
return redirect('forum_topic_p1', topic_id=post['topic_id'])
db.set_post_deleted(post_id, True)
messages.success(request, 'Post borrado.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
def restore_post(request, post_id):
if not perm.is_moderator(request):
return redirect('app_forum')
post = db.get_post(post_id)
if not post:
return redirect('app_forum')
if request.method != 'POST':
return redirect('forum_topic_p1', topic_id=post['topic_id'])
db.set_post_deleted(post_id, False)
messages.success(request, 'Post restaurado.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
# --------------------------------------------------------------------------
# Moderación de temas
# --------------------------------------------------------------------------
def _mod_only(request):
if not perm.is_moderator(request):
messages.error(request, 'Acción solo para moderadores.')
return redirect('app_forum')
return None
def _mod_post_only(request):
"""Exige moderador y método POST (CSRF)."""
block = _mod_only(request)
if block:
return block
if request.method != 'POST':
return redirect('app_forum')
return None
def lock_topic(request, topic_id):
block = _mod_post_only(request)
if block:
return block
db.set_topic_flag(topic_id, 'locked', True)
messages.success(request, 'Tema bloqueado.')
return redirect('forum_topic_p1', topic_id=topic_id)
def unlock_topic(request, topic_id):
block = _mod_post_only(request)
if block:
return block
db.set_topic_flag(topic_id, 'locked', False)
messages.success(request, 'Tema desbloqueado.')
return redirect('forum_topic_p1', topic_id=topic_id)
def toggle_sticky(request, topic_id):
block = _mod_post_only(request)
if block:
return block
topic = db.get_topic(topic_id, include_hidden=True)
if topic:
db.set_topic_flag(topic_id, 'sticky', not topic['sticky'])
messages.success(request, 'Tema fijado.' if not topic['sticky'] else 'Tema no fijado.')
return redirect('forum_topic_p1', topic_id=topic_id)
def delete_topic(request, topic_id):
block = _mod_post_only(request)
if block:
return block
topic = db.get_topic(topic_id, include_hidden=True)
db.set_topic_flag(topic_id, 'deleted', True)
messages.success(request, 'Tema borrado.')
if topic:
return redirect('forum_view_p1', forum_id=topic['forum_id'])
return redirect('app_forum')
def restore_topic(request, topic_id):
block = _mod_post_only(request)
if block:
return block
db.set_topic_flag(topic_id, 'deleted', False)
messages.success(request, 'Tema restaurado.')
return redirect('forum_topic_p1', topic_id=topic_id)
def move_topic(request, topic_id):
block = _mod_only(request)
if block:
return block
if request.method != 'POST':
return redirect('forum_topic_p1', topic_id=topic_id)
try:
new_forum_id = int(request.POST.get('forum_id', ''))
except (TypeError, ValueError):
messages.error(request, 'Foro destino no válido.')
return redirect('forum_topic_p1', topic_id=topic_id)
if not db.get_forum(new_forum_id, include_hidden=True):
messages.error(request, 'Foro destino no válido.')
return redirect('forum_topic_p1', topic_id=topic_id)
db.move_topic(topic_id, new_forum_id)
messages.success(request, 'Tema movido.')
return redirect('forum_topic_p1', topic_id=topic_id)
# --------------------------------------------------------------------------
# Perfil público
# --------------------------------------------------------------------------
def profile(request, username):
from django.db import connections
stats = {'username': username, 'post_count': 0, 'topic_count': 0, 'joindate': None}
with connections['acore_web'].cursor() as cursor:
cursor.execute(
"SELECT COUNT(*) FROM forum_posts WHERE poster = %s AND deleted = 0", [username]
)
stats['post_count'] = cursor.fetchone()[0]
cursor.execute(
"SELECT COUNT(*) FROM forum_topics WHERE poster = %s AND deleted = 0", [username]
)
stats['topic_count'] = cursor.fetchone()[0]
try:
with connections['acore_auth'].cursor() as cursor:
cursor.execute("SELECT joindate FROM account WHERE username = %s", [username])
row = cursor.fetchone()
if row:
stats['joindate'] = row[0]
except Exception:
pass
return render(request, 'forum/profile.html', _base_context(request, {
'profile': stats,
}))