Añade foro de comunidad integrado (app forum) con el diseño de NovaWoW
- app 'forum': portada (categorías/foros), ver foro y tema (paginado), crear tema, responder, editar/borrar posts y moderación (fijar, bloquear, mover, borrar/restaurar) - lee/escribe la BD acore_web (5ª conexión, DB_NAME_WEB) por SQL directo, tablas forum_categories/forums/forum_topics/forum_posts/forum_reads (portado de NovaWeb-main) - identidad = cuenta de juego en sesión; permisos simplificados por gmlevel (FORUM_MOD_GMLEVEL) en vez de la matriz group_level×permission del original - saneado del HTML de posts con nh3 (evita el XSS del original); POST+CSRF en formularios - plantillas con los partials del sitio; enlace 'FOROS' del menú apunta al foro interno - sql/forum_schema.sql y docs/FORO.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+367
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
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
|
||||
|
||||
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 [],
|
||||
}))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 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,
|
||||
}))
|
||||
|
||||
|
||||
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,
|
||||
}))
|
||||
|
||||
|
||||
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'])
|
||||
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')
|
||||
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 lock_topic(request, topic_id):
|
||||
block = _mod_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_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_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_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_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,
|
||||
}))
|
||||
Reference in New Issue
Block a user