""" 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) for t in topics: t['display_poster'] = db.get_display_name(t['poster_id']) or t['poster'] return render(request, 'forum/forum.html', _base_context(request, { 'forum': forum, 'topics': topics, 'pagination': pag, })) def search(request): query = (request.GET.get('q') or '').strip() results = [] if len(query) >= 2: results = db.search_topics(query) for r in results: r['display_poster'] = db.get_display_name(r['poster_id']) or r['poster'] return render(request, 'forum/search.html', _base_context(request, { 'query': query, 'results': results, })) 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, }))