Jubilar Django: borrar el portal antiguo, la web ya la sirve Next
El cutover se hizo hoy: Caddy manda www.nightspire.gg a :3001 (Next), así que Django se quedó sin dominio y sin uso. Se comprobó antes de borrar que nada dependía de él: ninguna referencia a :8001 en Caddy, el timer de reconciliación llama a Next, DJANGO_API_BASE no lo leía ni una línea del código (se quita también de la unidad de systemd), web-next/public es autónomo (736 ficheros reales, 0 symlinks) y no hay ni una referencia a /static/. Con Django parado la web siguió dando 200 en todas las rutas. Se va: home/, novawow/, forum/, wotlk_db/, frontend/ (las islas React que Next sustituyó), static/, staticfiles/, manage.py, requirements.txt, db.sqlite3 y el Docker de Django. Se quedan docs/ y sql/: no son código Django sino documentación y esquemas, y sql/forum_schema.sql describe la BD acore_web que Next usa hoy. La BASE DE DATOS no se toca. django_wow y sus 41 tablas home_* son ahora de Next, que las consulta con SQL directo. El nombre se queda por historia. README reescrito: describía cómo montar un Django que ya no existe (venv, manage.py migrate, gunicorn, Docker). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ForumConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'forum'
|
||||
verbose_name = 'Foro'
|
||||
-414
@@ -1,414 +0,0 @@
|
||||
"""
|
||||
Acceso a datos del foro (BD ``acore_web``), con SQL directo — mismo estilo que
|
||||
el resto del proyecto (``connections['acore_web']``).
|
||||
|
||||
Tablas: forum_categories, forums, forum_topics, forum_posts, forum_reads.
|
||||
"""
|
||||
|
||||
from django.db import connections
|
||||
|
||||
|
||||
def _dictfetchall(cursor):
|
||||
cols = [c[0] for c in cursor.description]
|
||||
return [dict(zip(cols, row)) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def _dictfetchone(cursor):
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
cols = [c[0] for c in cursor.description]
|
||||
return dict(zip(cols, row))
|
||||
|
||||
|
||||
def _conn():
|
||||
return connections['acore_web']
|
||||
|
||||
|
||||
# Detección perezosa de la columna opcional forum_topics.views (contador de visitas)
|
||||
_views_supported = None
|
||||
|
||||
|
||||
def has_views_column():
|
||||
global _views_supported
|
||||
if _views_supported is None:
|
||||
try:
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute("SELECT views FROM forum_topics LIMIT 1")
|
||||
_views_supported = True
|
||||
except Exception:
|
||||
_views_supported = False
|
||||
return _views_supported
|
||||
|
||||
|
||||
def increment_views(topic_id):
|
||||
"""Suma 1 a las visitas del tema (si existe la columna)."""
|
||||
if not has_views_column():
|
||||
return
|
||||
try:
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"UPDATE forum_topics SET views = views + 1 WHERE id = %s", [topic_id]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Portada: categorías + foros (con estadísticas)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_index(include_hidden=False):
|
||||
"""
|
||||
Devuelve las categorías ordenadas, cada una con sus foros (visibles y no
|
||||
borrados salvo include_hidden) y estadísticas: nº de temas, nº de posts y
|
||||
último tema.
|
||||
"""
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id, `order`, name FROM forum_categories ORDER BY `order`, id"
|
||||
)
|
||||
categories = _dictfetchall(cursor)
|
||||
|
||||
vis = "" if include_hidden else "AND f.visibility = 1 AND f.deleted = 0"
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT f.id, f.category_id, f.`order`, f.name, f.description, f.icon,
|
||||
f.colortitle, f.type, f.visibility, f.deleted,
|
||||
(SELECT COUNT(*) FROM forum_topics t
|
||||
WHERE t.forum_id = f.id AND t.deleted = 0) AS topic_count,
|
||||
(SELECT COUNT(*) FROM forum_posts p
|
||||
JOIN forum_topics t ON p.topic_id = t.id
|
||||
WHERE t.forum_id = f.id AND p.deleted = 0 AND t.deleted = 0) AS post_count,
|
||||
(SELECT t2.id FROM forum_topics t2
|
||||
WHERE t2.forum_id = f.id AND t2.deleted = 0
|
||||
ORDER BY t2.updated_at DESC, t2.id DESC LIMIT 1) AS last_topic_id,
|
||||
(SELECT t3.name FROM forum_topics t3
|
||||
WHERE t3.forum_id = f.id AND t3.deleted = 0
|
||||
ORDER BY t3.updated_at DESC, t3.id DESC LIMIT 1) AS last_topic_name,
|
||||
(SELECT t4.poster FROM forum_topics t4
|
||||
WHERE t4.forum_id = f.id AND t4.deleted = 0
|
||||
ORDER BY t4.updated_at DESC, t4.id DESC LIMIT 1) AS last_topic_poster,
|
||||
(SELECT t5.updated_at FROM forum_topics t5
|
||||
WHERE t5.forum_id = f.id AND t5.deleted = 0
|
||||
ORDER BY t5.updated_at DESC, t5.id DESC LIMIT 1) AS last_topic_time
|
||||
FROM forums f
|
||||
WHERE 1=1 {vis}
|
||||
ORDER BY f.`order`, f.id
|
||||
"""
|
||||
)
|
||||
forums = _dictfetchall(cursor)
|
||||
|
||||
by_cat = {}
|
||||
for f in forums:
|
||||
by_cat.setdefault(f['category_id'], []).append(f)
|
||||
for c in categories:
|
||||
c['forums'] = by_cat.get(c['id'], [])
|
||||
# Solo categorías con foros visibles (salvo include_hidden)
|
||||
if not include_hidden:
|
||||
categories = [c for c in categories if c['forums']]
|
||||
return categories
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Foro individual y sus temas (paginado)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_forum(forum_id, include_hidden=False):
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id, category_id, `order`, name, description, icon, colortitle, "
|
||||
"type, visibility, deleted FROM forums WHERE id = %s",
|
||||
[forum_id],
|
||||
)
|
||||
forum = _dictfetchone(cursor)
|
||||
if not forum:
|
||||
return None
|
||||
if not include_hidden and (forum['deleted'] or not forum['visibility']):
|
||||
return None
|
||||
return forum
|
||||
|
||||
|
||||
def count_topics(forum_id):
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM forum_topics WHERE forum_id = %s AND deleted = 0",
|
||||
[forum_id],
|
||||
)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
|
||||
def get_topics(forum_id, offset, limit):
|
||||
views_sel = "t.views," if has_views_column() else "0 AS views,"
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT t.id, t.name, t.poster, t.poster_id, t.created, t.sticky,
|
||||
t.locked, t.moved, t.updated_at, {views_sel}
|
||||
(SELECT COUNT(*) FROM forum_posts p
|
||||
WHERE p.topic_id = t.id AND p.deleted = 0) AS post_count,
|
||||
(SELECT p2.poster FROM forum_posts p2
|
||||
WHERE p2.topic_id = t.id AND p2.deleted = 0
|
||||
ORDER BY p2.time DESC, p2.id DESC LIMIT 1) AS last_poster,
|
||||
(SELECT p3.time FROM forum_posts p3
|
||||
WHERE p3.topic_id = t.id AND p3.deleted = 0
|
||||
ORDER BY p3.time DESC, p3.id DESC LIMIT 1) AS last_time
|
||||
FROM forum_topics t
|
||||
WHERE t.forum_id = %s AND t.deleted = 0
|
||||
ORDER BY t.sticky DESC, t.updated_at DESC, t.id DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
[forum_id, limit, offset],
|
||||
)
|
||||
return _dictfetchall(cursor)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Tema individual y sus posts (paginado)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_topic(topic_id, include_hidden=False):
|
||||
views_sel = ", views" if has_views_column() else ", 0 AS views"
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id, forum_id, name, poster, poster_id, created, sticky, "
|
||||
f"locked, deleted, moved, updated_at{views_sel} FROM forum_topics WHERE id = %s",
|
||||
[topic_id],
|
||||
)
|
||||
topic = _dictfetchone(cursor)
|
||||
if not topic:
|
||||
return None
|
||||
if not include_hidden and topic['deleted']:
|
||||
return None
|
||||
return topic
|
||||
|
||||
|
||||
def count_posts(topic_id, include_deleted=False):
|
||||
delf = "" if include_deleted else "AND deleted = 0"
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
f"SELECT COUNT(*) FROM forum_posts WHERE topic_id = %s {delf}",
|
||||
[topic_id],
|
||||
)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
|
||||
def get_posts(topic_id, offset, limit, include_deleted=False):
|
||||
delf = "" if include_deleted else "AND deleted = 0"
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT id, topic_id, poster, poster_id, text, time, deleted, updated_at
|
||||
FROM forum_posts
|
||||
WHERE topic_id = %s {delf}
|
||||
ORDER BY time ASC, id ASC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
[topic_id, limit, offset],
|
||||
)
|
||||
return _dictfetchall(cursor)
|
||||
|
||||
|
||||
def get_post(post_id):
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id, topic_id, poster, poster_id, text, time, deleted, updated_at "
|
||||
"FROM forum_posts WHERE id = %s",
|
||||
[post_id],
|
||||
)
|
||||
return _dictfetchone(cursor)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Escritura: crear tema, responder, editar, borrar/restaurar
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def create_topic(forum_id, name, poster, poster_id, text):
|
||||
"""Crea un tema y su primer post. Devuelve el id del tema."""
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"INSERT INTO forum_topics "
|
||||
"(forum_id, name, poster, poster_id, created, created_at, updated_at, locked, sticky, deleted) "
|
||||
"VALUES (%s, %s, %s, %s, NOW(), NOW(), NOW(), 0, 0, 0)",
|
||||
[forum_id, name, poster, poster_id],
|
||||
)
|
||||
cursor.execute("SELECT LAST_INSERT_ID()")
|
||||
topic_id = cursor.fetchone()[0]
|
||||
cursor.execute(
|
||||
"INSERT INTO forum_posts (topic_id, poster, poster_id, text, time, created_at, deleted) "
|
||||
"VALUES (%s, %s, %s, %s, NOW(), NOW(), 0)",
|
||||
[topic_id, poster, poster_id, text],
|
||||
)
|
||||
return topic_id
|
||||
|
||||
|
||||
def create_post(topic_id, poster, poster_id, text):
|
||||
"""Añade una respuesta y actualiza la marca de tiempo del tema."""
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"INSERT INTO forum_posts (topic_id, poster, poster_id, text, time, created_at, deleted) "
|
||||
"VALUES (%s, %s, %s, %s, NOW(), NOW(), 0)",
|
||||
[topic_id, poster, poster_id, text],
|
||||
)
|
||||
cursor.execute("SELECT LAST_INSERT_ID()")
|
||||
post_id = cursor.fetchone()[0]
|
||||
cursor.execute(
|
||||
"UPDATE forum_topics SET updated_at = NOW() WHERE id = %s", [topic_id]
|
||||
)
|
||||
return post_id
|
||||
|
||||
|
||||
def get_author_info(poster_id):
|
||||
"""
|
||||
Datos del autor para mostrar en los posts: nivel GM, fecha de registro y
|
||||
nº de mensajes. Cruza acore_auth (account/account_access) y acore_web.
|
||||
"""
|
||||
info = {'gmlevel': 0, 'joindate': None, 'post_count': 0, 'status': 'Miembro',
|
||||
'display_name': None}
|
||||
if not poster_id:
|
||||
return info
|
||||
info['display_name'] = get_display_name(poster_id)
|
||||
try:
|
||||
with connections['acore_auth'].cursor() as cursor:
|
||||
cursor.execute("SELECT MAX(gmlevel) FROM account_access WHERE id = %s", [poster_id])
|
||||
row = cursor.fetchone()
|
||||
info['gmlevel'] = int(row[0]) if row and row[0] is not None else 0
|
||||
cursor.execute("SELECT joindate FROM account WHERE id = %s", [poster_id])
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
info['joindate'] = row[0]
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM forum_posts WHERE poster_id = %s AND deleted = 0",
|
||||
[poster_id],
|
||||
)
|
||||
info['post_count'] = cursor.fetchone()[0]
|
||||
except Exception:
|
||||
pass
|
||||
if info['gmlevel'] >= 16:
|
||||
info['status'] = 'Administrador'
|
||||
elif info['gmlevel'] >= 1:
|
||||
info['status'] = 'GM'
|
||||
return info
|
||||
|
||||
|
||||
def update_post(post_id, text):
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"UPDATE forum_posts SET text = %s, updated_at = NOW() WHERE id = %s",
|
||||
[text, post_id],
|
||||
)
|
||||
|
||||
|
||||
def set_post_deleted(post_id, deleted):
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"UPDATE forum_posts SET deleted = %s WHERE id = %s",
|
||||
[1 if deleted else 0, post_id],
|
||||
)
|
||||
|
||||
|
||||
def set_topic_flag(topic_id, field, value):
|
||||
"""field ∈ {sticky, locked, deleted, moved}."""
|
||||
if field not in ('sticky', 'locked', 'deleted', 'moved'):
|
||||
raise ValueError('campo no permitido')
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
f"UPDATE forum_topics SET {field} = %s WHERE id = %s",
|
||||
[1 if value else 0, topic_id],
|
||||
)
|
||||
|
||||
|
||||
def move_topic(topic_id, new_forum_id):
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"UPDATE forum_topics SET forum_id = %s, moved = 1 WHERE id = %s",
|
||||
[new_forum_id, topic_id],
|
||||
)
|
||||
|
||||
|
||||
def mark_read(user_id, topic_id):
|
||||
if not user_id:
|
||||
return
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"INSERT INTO forum_reads (user_id, topic_id, read_at) "
|
||||
"VALUES (%s, %s, NOW()) "
|
||||
"ON DUPLICATE KEY UPDATE read_at = NOW()",
|
||||
[user_id, topic_id],
|
||||
)
|
||||
|
||||
|
||||
def get_display_name(poster_id):
|
||||
"""
|
||||
Nombre legible del autor a partir de su cuenta: parte local del email
|
||||
(antes de la @). Si no hay email, devuelve None (la plantilla usa el poster).
|
||||
"""
|
||||
if not poster_id:
|
||||
return None
|
||||
try:
|
||||
with connections['acore_auth'].cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT email, reg_mail FROM account WHERE id = %s", [poster_id]
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
except Exception:
|
||||
return None
|
||||
if not row:
|
||||
return None
|
||||
email = (row[0] or row[1] or '').strip()
|
||||
if '@' in email:
|
||||
return email.split('@', 1)[0]
|
||||
return None
|
||||
|
||||
|
||||
def count_search_topics(query):
|
||||
like = f"%{query}%"
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(DISTINCT t.id)
|
||||
FROM forum_topics t
|
||||
JOIN forums f ON f.id = t.forum_id AND f.visibility = 1 AND f.deleted = 0
|
||||
LEFT JOIN forum_posts p ON p.topic_id = t.id AND p.deleted = 0
|
||||
WHERE t.deleted = 0 AND (t.name LIKE %s OR p.text LIKE %s)
|
||||
""",
|
||||
[like, like],
|
||||
)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
|
||||
def search_topics(query, offset=0, limit=20):
|
||||
"""Busca temas por título o por contenido de sus posts (foros visibles)."""
|
||||
like = f"%{query}%"
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT DISTINCT t.id, t.name, t.poster, t.poster_id, t.forum_id,
|
||||
t.created, t.updated_at, f.name AS forum_name
|
||||
FROM forum_topics t
|
||||
JOIN forums f ON f.id = t.forum_id AND f.visibility = 1 AND f.deleted = 0
|
||||
LEFT JOIN forum_posts p ON p.topic_id = t.id AND p.deleted = 0
|
||||
WHERE t.deleted = 0 AND (t.name LIKE %s OR p.text LIKE %s)
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
[like, like, limit, offset],
|
||||
)
|
||||
return _dictfetchall(cursor)
|
||||
|
||||
|
||||
def list_forums_for_move(exclude_forum_id=None):
|
||||
"""Lista de foros visibles para el desplegable de 'mover tema'."""
|
||||
with _conn().cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id, name FROM forums WHERE visibility = 1 AND deleted = 0 "
|
||||
"ORDER BY `order`, id"
|
||||
)
|
||||
forums = _dictfetchall(cursor)
|
||||
if exclude_forum_id:
|
||||
forums = [f for f in forums if f['id'] != exclude_forum_id]
|
||||
return forums
|
||||
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
Formularios del foro con el editor enriquecido CKEditor 5 (el mismo que usa el
|
||||
resto de NovaWoW). El HTML resultante se sanea con nh3 al guardar (ver views).
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django_ckeditor_5.widgets import CKEditor5Widget
|
||||
|
||||
|
||||
class TopicForm(forms.Form):
|
||||
name = forms.CharField(
|
||||
max_length=45,
|
||||
widget=forms.TextInput(attrs={'placeholder': 'Título del tema', 'maxlength': 45}),
|
||||
)
|
||||
text = forms.CharField(widget=CKEditor5Widget(config_name='default'))
|
||||
|
||||
|
||||
class ReplyForm(forms.Form):
|
||||
text = forms.CharField(widget=CKEditor5Widget(config_name='default'))
|
||||
|
||||
|
||||
class EditPostForm(forms.Form):
|
||||
text = forms.CharField(widget=CKEditor5Widget(config_name='default'))
|
||||
@@ -1,71 +0,0 @@
|
||||
"""
|
||||
Modelo de permisos del foro (versión simplificada y mantenible).
|
||||
|
||||
En lugar de la matriz group_level×permission del proyecto original, usamos
|
||||
reglas claras basadas en:
|
||||
* si el usuario ha iniciado sesión y tiene una cuenta de juego seleccionada
|
||||
(``request.session['account_id']``),
|
||||
* su nivel GM (``acore_auth.account_access.gmlevel``),
|
||||
* y la propiedad del contenido (autor == usuario).
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connections
|
||||
|
||||
|
||||
def get_account_id(request):
|
||||
"""Id de la cuenta de juego seleccionada en sesión, o None."""
|
||||
return request.session.get('account_id')
|
||||
|
||||
|
||||
def is_authenticated(request):
|
||||
return bool(request.session.get('account_id'))
|
||||
|
||||
|
||||
def get_gmlevel(account_id):
|
||||
"""Nivel GM máximo de una cuenta (acore_auth.account_access). 0 si no tiene."""
|
||||
if not account_id:
|
||||
return 0
|
||||
try:
|
||||
with connections['acore_auth'].cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT MAX(gmlevel) FROM account_access WHERE id = %s",
|
||||
[account_id],
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return int(row[0]) if row and row[0] is not None else 0
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def is_moderator(request):
|
||||
"""¿El usuario puede moderar el foro?"""
|
||||
account_id = get_account_id(request)
|
||||
if not account_id:
|
||||
return False
|
||||
return get_gmlevel(account_id) >= settings.FORUM_MOD_GMLEVEL
|
||||
|
||||
|
||||
def can_post(request):
|
||||
"""¿Puede crear temas / responder? Cualquier usuario autenticado."""
|
||||
return is_authenticated(request)
|
||||
|
||||
|
||||
def can_edit_post(request, post):
|
||||
"""Puede editar/borrar un post: su autor o un moderador."""
|
||||
account_id = get_account_id(request)
|
||||
if not account_id:
|
||||
return False
|
||||
if post and int(post.get('poster_id') or 0) == int(account_id):
|
||||
return True
|
||||
return is_moderator(request)
|
||||
|
||||
|
||||
def forum_context(request):
|
||||
"""Datos comunes de permisos para las plantillas."""
|
||||
return {
|
||||
'forum_is_authenticated': is_authenticated(request),
|
||||
'forum_is_moderator': is_moderator(request),
|
||||
'forum_account_id': get_account_id(request),
|
||||
'forum_username': request.session.get('username'),
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"""
|
||||
Saneado del HTML de los posts del foro (el editor del cliente puede enviar HTML).
|
||||
|
||||
El proyecto original guardaba el HTML sin sanear y lo renderizaba con |raw
|
||||
(XSS almacenado). Aquí lo saneamos con nh3 (allowlist) al guardar, de modo que
|
||||
la plantilla puede renderizarlo con |safe sin riesgo.
|
||||
"""
|
||||
|
||||
import nh3
|
||||
|
||||
# Etiquetas permitidas (formato básico de foro)
|
||||
_ALLOWED_TAGS = {
|
||||
"p", "br", "hr", "span", "div",
|
||||
"strong", "b", "em", "i", "u", "s", "strike", "sub", "sup",
|
||||
"ul", "ol", "li", "blockquote", "code", "pre",
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"a", "img",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
}
|
||||
|
||||
_ALLOWED_ATTRIBUTES = {
|
||||
"a": {"href", "title", "target"},
|
||||
"img": {"src", "alt", "title", "width", "height"},
|
||||
"span": {"style"},
|
||||
"div": {"style"},
|
||||
"td": {"colspan", "rowspan"},
|
||||
"th": {"colspan", "rowspan"},
|
||||
}
|
||||
|
||||
# Solo esquemas de URL seguros
|
||||
_ALLOWED_URL_SCHEMES = {"http", "https", "mailto"}
|
||||
|
||||
|
||||
def clean_post_html(html):
|
||||
"""Devuelve el HTML saneado y seguro para renderizar con |safe."""
|
||||
if not html:
|
||||
return ""
|
||||
return nh3.clean(
|
||||
html,
|
||||
tags=_ALLOWED_TAGS,
|
||||
attributes=_ALLOWED_ATTRIBUTES,
|
||||
url_schemes=_ALLOWED_URL_SCHEMES,
|
||||
link_rel="noopener noreferrer nofollow",
|
||||
)
|
||||
|
||||
|
||||
def plain_length(html):
|
||||
"""Longitud del texto plano (sin etiquetas), para validar longitud mínima."""
|
||||
if not html:
|
||||
return 0
|
||||
text = nh3.clean(html, tags=set(), attributes={})
|
||||
return len(text.strip())
|
||||
@@ -1,45 +0,0 @@
|
||||
<style>
|
||||
.forum-wrap { max-width: 1000px; margin: 0 auto; }
|
||||
.forum-cat { margin-bottom: 24px; }
|
||||
.forum-cat-title { font-size: 1.2rem; color: #d79602; border-bottom: 1px solid rgba(215,150,2,.4); padding: 6px 4px; margin-bottom: 8px; }
|
||||
.forum-row, .topic-row { display: flex; align-items: center; gap: 12px; padding: 12px 10px; border-bottom: 1px solid rgba(255,255,255,.08); }
|
||||
.forum-row:hover, .topic-row:hover { background: rgba(255,255,255,.04); }
|
||||
.forum-row .main, .topic-row .main { flex: 1; min-width: 0; }
|
||||
.forum-row .name, .topic-row .name { color: #e0c07a; font-weight: bold; text-decoration: none; }
|
||||
.forum-row .desc { color: #b1997f; font-size: .85rem; }
|
||||
.forum-row .stats, .topic-row .meta { text-align: right; font-size: .8rem; color: #b1997f; white-space: nowrap; }
|
||||
.forum-badge { display: inline-block; font-size: .7rem; padding: 1px 6px; border-radius: 3px; background: #7a5c1e; color: #fff; margin-right: 4px; }
|
||||
.forum-post { display: flex; gap: 0; border: 1px solid rgba(255,255,255,.1); margin-bottom: 16px; background: rgba(0,0,0,.15); }
|
||||
.forum-post .left { width: 180px; padding: 14px; background: rgba(0,0,0,.25); text-align: center; }
|
||||
.forum-post .left .pname { color: #e0c07a; font-weight: bold; }
|
||||
.forum-post .left .prole { font-size: .8rem; color: #d79602; }
|
||||
.forum-post .left .pmeta { font-size: .75rem; color: #b1997f; margin-top: 6px; }
|
||||
.forum-post .right { flex: 1; padding: 14px; min-width: 0; }
|
||||
.forum-post .pbody { white-space: pre-wrap; word-wrap: break-word; color: #e8e0d0; }
|
||||
.forum-post .pbody img { max-width: 100%; height: auto; }
|
||||
.forum-post .pfoot { margin-top: 12px; font-size: .75rem; color: #8a7a63; display: flex; justify-content: space-between; }
|
||||
.forum-actions { margin: 14px 0; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.forum-btn { display: inline-block; padding: 6px 14px; background: #7a5c1e; color: #fff !important; border: none; border-radius: 3px; cursor: pointer; text-decoration: none; font-size: .85rem; }
|
||||
.forum-btn:hover { background: #d79602; }
|
||||
.forum-btn.danger { background: #7a2020; }
|
||||
.forum-btn.danger:hover { background: #c41e3a; }
|
||||
.forum-pagination { margin: 16px 0; text-align: center; }
|
||||
.forum-pagination a, .forum-pagination span { display: inline-block; padding: 4px 9px; margin: 0 2px; border: 1px solid rgba(215,150,2,.4); color: #d79602; text-decoration: none; border-radius: 3px; }
|
||||
.forum-pagination .current { background: #7a5c1e; color: #fff; }
|
||||
.forum-form textarea { width: 100%; min-height: 180px; background: rgba(0,0,0,.3); color: #e8e0d0; border: 1px solid rgba(215,150,2,.4); padding: 10px; border-radius: 3px; }
|
||||
.forum-form input[type=text] { width: 100%; background: rgba(0,0,0,.3); color: #e8e0d0; border: 1px solid rgba(215,150,2,.4); padding: 8px; border-radius: 3px; margin-bottom: 10px; }
|
||||
.forum-msg { padding: 8px 12px; border-radius: 3px; margin-bottom: 10px; }
|
||||
.forum-msg.error { background: rgba(196,30,58,.2); color: #ff9a9a; }
|
||||
.forum-msg.success { background: rgba(46,160,67,.2); color: #9affb0; }
|
||||
.forum-deleted { opacity: .55; }
|
||||
.forum-inline { display: inline; }
|
||||
.link-btn { background: none; border: none; color: #d79602; cursor: pointer; padding: 0; font: inherit; text-decoration: underline; }
|
||||
.link-btn.danger { color: #ff9a9a; }
|
||||
</style>
|
||||
{% if messages %}
|
||||
<div class="forum-messages forum-wrap">
|
||||
{% for message in messages %}
|
||||
<div class="forum-msg {{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% include 'partials/head.html' %}
|
||||
{% include 'partials/header.html' %}
|
||||
{% include 'partials/video.html' %}
|
||||
|
||||
<div class="main-page">
|
||||
<div class="middle-content">
|
||||
<div class="body-content">
|
||||
<div class="title-content"><h1>Nuevo tema</h1></div>
|
||||
<div class="box-content">
|
||||
<div class="body-box-content">
|
||||
{% include 'forum/_style.html' %}
|
||||
<div class="forum-wrap forum-form">
|
||||
<p style="color:#b1997f">Foro: <strong>{{ forum.name }}</strong></p>
|
||||
<form action="{% url 'forum_create_topic_submit' forum_id=forum.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.name }}
|
||||
{{ form.text }}
|
||||
<div class="forum-actions">
|
||||
<button type="submit" class="forum-btn">Publicar tema</button>
|
||||
<a class="forum-btn" href="{% url 'forum_view_p1' forum_id=forum.id %}">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
{{ form.media }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% include 'partials/head.html' %}
|
||||
{% include 'partials/header.html' %}
|
||||
{% include 'partials/video.html' %}
|
||||
|
||||
<div class="main-page">
|
||||
<div class="middle-content">
|
||||
<div class="body-content">
|
||||
<div class="title-content"><h1>Editar mensaje</h1></div>
|
||||
<div class="box-content">
|
||||
<div class="body-box-content">
|
||||
{% include 'forum/_style.html' %}
|
||||
<div class="forum-wrap forum-form">
|
||||
<form action="{% url 'forum_update_post' post_id=post.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.text }}
|
||||
<div class="forum-actions">
|
||||
<button type="submit" class="forum-btn">Guardar cambios</button>
|
||||
<a class="forum-btn" href="{% url 'forum_topic_p1' topic_id=post.topic_id %}">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
{{ form.media }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% include 'partials/head.html' %}
|
||||
{% include 'partials/header.html' %}
|
||||
{% include 'partials/video.html' %}
|
||||
|
||||
<div class="main-page">
|
||||
<div class="middle-content">
|
||||
<div class="body-content">
|
||||
<div class="title-content"><h1>{{ forum.name }}</h1></div>
|
||||
<div class="box-content">
|
||||
<div class="body-box-content">
|
||||
{% include 'forum/_style.html' %}
|
||||
<div class="forum-wrap">
|
||||
<p class="desc" style="color:#b1997f">{{ forum.description }}</p>
|
||||
<div class="forum-actions">
|
||||
<a class="forum-btn" href="{% url 'app_forum' %}">← Volver al foro</a>
|
||||
{% if forum_is_authenticated %}
|
||||
<a class="forum-btn" href="{% url 'forum_create_topic' forum_id=forum.id %}">Crear nuevo tema</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for topic in topics %}
|
||||
<div class="topic-row">
|
||||
<div class="main">
|
||||
<a class="name" href="{% url 'forum_topic_p1' topic_id=topic.id %}">
|
||||
{% if topic.sticky %}<span class="forum-badge">Fijado</span>{% endif %}
|
||||
{% if topic.locked %}<span class="forum-badge">Bloqueado</span>{% endif %}
|
||||
{% if topic.moved %}<span class="forum-badge">Movido</span>{% endif %}
|
||||
{{ topic.name }}
|
||||
</a>
|
||||
<div class="desc">por {{ topic.display_poster }} · {{ topic.created }}</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div>{{ topic.post_count }} respuestas{% if has_views %} · {{ topic.views }} vistas{% endif %}</div>
|
||||
{% if topic.last_poster %}<div>último: {{ topic.last_poster }}</div>{% endif %}
|
||||
{% if topic.last_time %}<div>{{ topic.last_time }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>No hay temas todavía. ¡Sé el primero en publicar!</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.total_pages > 1 %}
|
||||
<div class="forum-pagination">
|
||||
{% if pagination.has_prev %}<a href="{% url 'forum_view' forum_id=forum.id page=pagination.prev_page %}">« Anterior</a>{% endif %}
|
||||
{% for p in pagination.page_range %}
|
||||
{% if p == pagination.page %}<span class="current">{{ p }}</span>
|
||||
{% else %}<a href="{% url 'forum_view' forum_id=forum.id page=p %}">{{ p }}</a>{% endif %}
|
||||
{% endfor %}
|
||||
{% if pagination.has_next %}<a href="{% url 'forum_view' forum_id=forum.id page=pagination.next_page %}">Siguiente »</a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -1,55 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% include 'partials/head.html' %}
|
||||
{% include 'partials/header.html' %}
|
||||
{% include 'partials/video.html' %}
|
||||
|
||||
<div class="main-page">
|
||||
<div class="middle-content">
|
||||
<div class="body-content">
|
||||
<div class="title-content"><h1>Foro de la comunidad</h1></div>
|
||||
<div class="box-content">
|
||||
<div class="body-box-content">
|
||||
{% include 'forum/_style.html' %}
|
||||
<div class="forum-wrap">
|
||||
<form class="forum-search" action="{% url 'forum_search' %}" method="GET" style="margin-bottom:16px">
|
||||
<input type="text" name="q" placeholder="Buscar en el foro..."
|
||||
style="width:70%;background:rgba(0,0,0,.3);color:#e8e0d0;border:1px solid rgba(215,150,2,.4);padding:8px;border-radius:3px">
|
||||
<button type="submit" class="forum-btn">Buscar</button>
|
||||
</form>
|
||||
{% for category in categories %}
|
||||
<div class="forum-cat">
|
||||
<div class="forum-cat-title">{{ category.name }}</div>
|
||||
{% for forum in category.forums %}
|
||||
<div class="forum-row {% if forum.deleted %}forum-deleted{% endif %}">
|
||||
<div class="main">
|
||||
<a class="name" href="{% url 'forum_view_p1' forum_id=forum.id %}"
|
||||
{% if forum.colortitle and forum.colortitle != '0' %}style="color:{{ forum.colortitle }}"{% endif %}>
|
||||
{{ forum.name }}
|
||||
</a>
|
||||
{% if not forum.visibility %}<span class="forum-badge">Oculto</span>{% endif %}
|
||||
<div class="desc">{{ forum.description }}</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div>{{ forum.topic_count }} temas</div>
|
||||
<div>{{ forum.post_count }} mensajes</div>
|
||||
{% if forum.last_topic_name %}
|
||||
<div>Último: <a href="{% url 'forum_topic_p1' topic_id=forum.last_topic_id %}">{{ forum.last_topic_name|truncatechars:30 }}</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>Todavía no hay foros disponibles.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -1,30 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% include 'partials/head.html' %}
|
||||
{% include 'partials/header.html' %}
|
||||
{% include 'partials/video.html' %}
|
||||
|
||||
<div class="main-page">
|
||||
<div class="middle-content">
|
||||
<div class="body-content">
|
||||
<div class="title-content"><h1>Perfil de {{ profile.username }}</h1></div>
|
||||
<div class="box-content">
|
||||
<div class="body-box-content">
|
||||
{% include 'forum/_style.html' %}
|
||||
<div class="forum-wrap">
|
||||
<p>Temas creados: <strong>{{ profile.topic_count }}</strong></p>
|
||||
<p>Mensajes publicados: <strong>{{ profile.post_count }}</strong></p>
|
||||
{% if profile.joindate %}<p>Registro: <strong>{{ profile.joindate|date:"d/m/Y" }}</strong></p>{% endif %}
|
||||
<div class="forum-actions">
|
||||
<a class="forum-btn" href="{% url 'app_forum' %}">← Volver al foro</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -1,60 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% include 'partials/head.html' %}
|
||||
{% include 'partials/header.html' %}
|
||||
{% include 'partials/video.html' %}
|
||||
|
||||
<div class="main-page">
|
||||
<div class="middle-content">
|
||||
<div class="body-content">
|
||||
<div class="title-content"><h1>Buscar en el foro</h1></div>
|
||||
<div class="box-content">
|
||||
<div class="body-box-content">
|
||||
{% include 'forum/_style.html' %}
|
||||
<div class="forum-wrap">
|
||||
<form class="forum-search" action="{% url 'forum_search' %}" method="GET" style="margin-bottom:16px">
|
||||
<input type="text" name="q" value="{{ query }}" placeholder="Buscar en el foro..."
|
||||
style="width:70%;background:rgba(0,0,0,.3);color:#e8e0d0;border:1px solid rgba(215,150,2,.4);padding:8px;border-radius:3px">
|
||||
<button type="submit" class="forum-btn">Buscar</button>
|
||||
</form>
|
||||
|
||||
{% if query %}
|
||||
<p style="color:#b1997f">Resultados para «{{ query }}»: {{ results|length }}</p>
|
||||
{% for topic in results %}
|
||||
<div class="topic-row">
|
||||
<div class="main">
|
||||
<a class="name" href="{% url 'forum_topic_p1' topic_id=topic.id %}">{{ topic.name }}</a>
|
||||
<div class="desc">en {{ topic.forum_name }} · por {{ topic.display_poster }} · {{ topic.created }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>No se encontraron temas.</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination and pagination.total_pages > 1 %}
|
||||
<div class="forum-pagination">
|
||||
{% if pagination.has_prev %}<a href="?q={{ query|urlencode }}&page={{ pagination.prev_page }}">« Anterior</a>{% endif %}
|
||||
{% for p in pagination.page_range %}
|
||||
{% if p == pagination.page %}<span class="current">{{ p }}</span>
|
||||
{% else %}<a href="?q={{ query|urlencode }}&page={{ p }}">{{ p }}</a>{% endif %}
|
||||
{% endfor %}
|
||||
{% if pagination.has_next %}<a href="?q={{ query|urlencode }}&page={{ pagination.next_page }}">Siguiente »</a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>Escribe al menos 2 caracteres para buscar.</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="forum-actions" style="margin-top:16px">
|
||||
<a class="forum-btn" href="{% url 'app_forum' %}">← Volver al foro</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -1,122 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
{% load forum_extras %}
|
||||
{% include 'partials/head.html' %}
|
||||
{% include 'partials/header.html' %}
|
||||
{% include 'partials/video.html' %}
|
||||
|
||||
<div class="main-page">
|
||||
<div class="middle-content">
|
||||
<div class="body-content">
|
||||
<div class="title-content"><h1>{{ topic.name }}</h1></div>
|
||||
<div class="box-content">
|
||||
<div class="body-box-content">
|
||||
{% include 'forum/_style.html' %}
|
||||
<div class="forum-wrap">
|
||||
<div class="forum-actions">
|
||||
{% if forum %}<a class="forum-btn" href="{% url 'forum_view_p1' forum_id=forum.id %}">← {{ forum.name }}</a>{% endif %}
|
||||
{% if topic.has_views %}<span style="color:#b1997f; align-self:center">{{ topic.views }} visitas</span>{% endif %}
|
||||
{% if topic.locked %}<span class="forum-badge">Tema bloqueado</span>{% endif %}
|
||||
</div>
|
||||
|
||||
{% for post in posts %}
|
||||
<div class="forum-post {% if post.deleted %}forum-deleted{% endif %}" id="post-{{ post.id }}">
|
||||
<div class="left">
|
||||
<div class="pname">
|
||||
<a href="{% url 'forum_profile' username=post.poster %}" style="color:#e0c07a">{{ post.author.display_name|default:post.poster }}</a>
|
||||
</div>
|
||||
<div class="prole">{{ post.author.status }}</div>
|
||||
<div class="pmeta">
|
||||
{% if post.author.joindate %}Registro: {{ post.author.joindate|date:"d/m/Y" }}<br>{% endif %}
|
||||
Mensajes: {{ post.author.post_count }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
{% if post.deleted %}
|
||||
<div class="pbody"><em>[Mensaje eliminado]</em></div>
|
||||
{% else %}
|
||||
<div class="pbody">{{ post.text|forum_safe }}</div>
|
||||
{% endif %}
|
||||
<div class="pfoot">
|
||||
<span>{{ post.time }}{% if post.updated_at and post.updated_at != post.time %} · editado {{ post.updated_at }}{% endif %}</span>
|
||||
<span>
|
||||
{% if post.can_edit and not post.deleted %}
|
||||
<a href="{% url 'forum_edit_post' post_id=post.id %}">Editar</a>
|
||||
· <form class="forum-inline" action="{% url 'forum_delete_post' post_id=post.id %}" method="POST" onsubmit="return confirm('¿Borrar este mensaje?')">{% csrf_token %}<button type="submit" class="link-btn danger">Borrar</button></form>
|
||||
{% endif %}
|
||||
{% if post.deleted and forum_is_moderator %}
|
||||
<form class="forum-inline" action="{% url 'forum_restore_post' post_id=post.id %}" method="POST">{% csrf_token %}<button type="submit" class="link-btn">Restaurar</button></form>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.total_pages > 1 %}
|
||||
<div class="forum-pagination">
|
||||
{% if pagination.has_prev %}<a href="{% url 'forum_topic' topic_id=topic.id page=pagination.prev_page %}">« Anterior</a>{% endif %}
|
||||
{% for p in pagination.page_range %}
|
||||
{% if p == pagination.page %}<span class="current">{{ p }}</span>
|
||||
{% else %}<a href="{% url 'forum_topic' topic_id=topic.id page=p %}">{{ p }}</a>{% endif %}
|
||||
{% endfor %}
|
||||
{% if pagination.has_next %}<a href="{% url 'forum_topic' topic_id=topic.id page=pagination.next_page %}">Siguiente »</a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<span id="post-bottom"></span>
|
||||
|
||||
{% if can_reply %}
|
||||
<div class="forum-form" style="margin-top:20px">
|
||||
<h3 style="color:#d79602">Responder</h3>
|
||||
<form action="{% url 'forum_post_reply' topic_id=topic.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ reply_form.text }}
|
||||
<div class="forum-actions">
|
||||
<button type="submit" class="forum-btn">Publicar respuesta</button>
|
||||
</div>
|
||||
</form>
|
||||
{{ reply_form.media }}
|
||||
</div>
|
||||
{% elif topic.locked %}
|
||||
<p style="color:#b1997f">Este tema está bloqueado. No se admiten nuevas respuestas.</p>
|
||||
{% elif not forum_is_authenticated %}
|
||||
<p><a href="{% url 'login' %}">Inicia sesión</a> para responder.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if forum_is_moderator %}
|
||||
<div class="forum-form" style="margin-top:24px; border-top:1px solid rgba(255,255,255,.1); padding-top:14px">
|
||||
<h3 style="color:#d79602">Moderación</h3>
|
||||
<div class="forum-actions">
|
||||
{% if topic.locked %}
|
||||
<form class="forum-inline" action="{% url 'forum_unlock_topic' topic_id=topic.id %}" method="POST">{% csrf_token %}<button type="submit" class="forum-btn">Desbloquear</button></form>
|
||||
{% else %}
|
||||
<form class="forum-inline" action="{% url 'forum_lock_topic' topic_id=topic.id %}" method="POST">{% csrf_token %}<button type="submit" class="forum-btn">Bloquear</button></form>
|
||||
{% endif %}
|
||||
<form class="forum-inline" action="{% url 'forum_toggle_sticky' topic_id=topic.id %}" method="POST">{% csrf_token %}<button type="submit" class="forum-btn">{% if topic.sticky %}Quitar fijado{% else %}Fijar{% endif %}</button></form>
|
||||
{% if topic.deleted %}
|
||||
<form class="forum-inline" action="{% url 'forum_restore_topic' topic_id=topic.id %}" method="POST">{% csrf_token %}<button type="submit" class="forum-btn">Restaurar tema</button></form>
|
||||
{% else %}
|
||||
<form class="forum-inline" action="{% url 'forum_delete_topic' topic_id=topic.id %}" method="POST" onsubmit="return confirm('¿Borrar el tema completo?')">{% csrf_token %}<button type="submit" class="forum-btn danger">Borrar tema</button></form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if move_forums %}
|
||||
<form action="{% url 'forum_move_topic' topic_id=topic.id %}" method="POST" style="margin-top:8px">
|
||||
{% csrf_token %}
|
||||
<select name="forum_id" style="padding:6px;background:rgba(0,0,0,.3);color:#e8e0d0;border:1px solid rgba(215,150,2,.4)">
|
||||
{% for f in move_forums %}<option value="{{ f.id }}">{{ f.name }}</option>{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="forum-btn">Mover tema</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -1,18 +0,0 @@
|
||||
"""Filtros de plantilla del foro."""
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .. import sanitize
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def forum_safe(value):
|
||||
"""
|
||||
Renderiza el HTML de un post saneándolo con nh3 en el momento (defensa en
|
||||
profundidad). Aunque el contenido nuevo ya se sanea al guardar, esto protege
|
||||
frente a filas heredadas/importadas que pudieran contener HTML sin sanear.
|
||||
"""
|
||||
return mark_safe(sanitize.clean_post_html(value or ''))
|
||||
@@ -1,38 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
# Se mantienen los nombres de ruta del proyecto original para coherencia.
|
||||
# El orden importa: las rutas con literales (topic/create/edit/post) van antes
|
||||
# que la genérica forum/<int:forum_id>/...
|
||||
urlpatterns = [
|
||||
path('forum', views.index, name='app_forum'),
|
||||
path('forum/search', views.search, name='forum_search'),
|
||||
|
||||
# Temas
|
||||
path('forum/topic/<int:topic_id>/reply', views.reply_to_topic, name='forum_post_reply'),
|
||||
path('forum/topic/<int:topic_id>/move', views.move_topic, name='forum_move_topic'),
|
||||
path('forum/topic/<int:topic_id>/lock', views.lock_topic, name='forum_lock_topic'),
|
||||
path('forum/topic/<int:topic_id>/unlock', views.unlock_topic, name='forum_unlock_topic'),
|
||||
path('forum/topic/<int:topic_id>/sticky', views.toggle_sticky, name='forum_toggle_sticky'),
|
||||
path('forum/topic/<int:topic_id>/restore', views.restore_topic, name='forum_restore_topic'),
|
||||
path('forum/topic/delete/<int:topic_id>', views.delete_topic, name='forum_delete_topic'),
|
||||
path('forum/topic/<int:topic_id>/<int:page>', views.view_topic, name='forum_topic'),
|
||||
path('forum/topic/<int:topic_id>', views.view_topic, name='forum_topic_p1'),
|
||||
|
||||
# Posts
|
||||
path('forum/post/update/<int:post_id>', views.update_post, name='forum_update_post'),
|
||||
path('forum/post/delete/<int:post_id>', views.delete_post, name='forum_delete_post'),
|
||||
path('forum/post/<int:post_id>/restore', views.restore_post, name='forum_restore_post'),
|
||||
path('forum/edit/<int:post_id>', views.edit_post, name='forum_edit_post'),
|
||||
|
||||
# Crear tema
|
||||
path('forum/create/<int:forum_id>/submit', views.submit_create_topic, name='forum_create_topic_submit'),
|
||||
path('forum/create/<int:forum_id>', views.create_topic, name='forum_create_topic'),
|
||||
|
||||
# Ver foro (paginado) — genérica, al final
|
||||
path('forum/<int:forum_id>/<int:page>', views.view_forum, name='forum_view'),
|
||||
path('forum/<int:forum_id>', views.view_forum, name='forum_view_p1'),
|
||||
|
||||
# Perfil público
|
||||
path('profile/<str:username>', views.profile, name='forum_profile'),
|
||||
]
|
||||
-453
@@ -1,453 +0,0 @@
|
||||
"""
|
||||
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 django.views.decorators.csrf import csrf_protect
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _topic_locked_for(request, topic_id):
|
||||
"""True si el tema está bloqueado y el usuario no es moderador."""
|
||||
if perm.is_moderator(request):
|
||||
return False
|
||||
topic = db.get_topic(topic_id, include_hidden=True)
|
||||
return bool(topic and topic['locked'])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 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,
|
||||
'has_views': db.has_views_column(),
|
||||
}))
|
||||
|
||||
|
||||
def search(request):
|
||||
query = (request.GET.get('q') or '').strip()
|
||||
try:
|
||||
page = int(request.GET.get('page', 1))
|
||||
except (TypeError, ValueError):
|
||||
page = 1
|
||||
results = []
|
||||
pag = None
|
||||
if len(query) >= 2:
|
||||
total = db.count_search_topics(query)
|
||||
pag, offset = _paginate(total, page, settings.FORUM_TOPICS_PER_PAGE)
|
||||
results = db.search_topics(query, offset, settings.FORUM_TOPICS_PER_PAGE)
|
||||
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,
|
||||
'pagination': pag,
|
||||
}))
|
||||
|
||||
|
||||
def view_topic(request, topic_id, page=1):
|
||||
is_mod = perm.is_moderator(request)
|
||||
topic = db.get_topic(topic_id, include_hidden=is_mod)
|
||||
if not topic:
|
||||
messages.error(request, 'Tema no encontrado.')
|
||||
return redirect('app_forum')
|
||||
|
||||
# El foro padre debe ser visible (salvo moderador); si no, no se accede al
|
||||
# tema por URL directa aunque no aparezca en los listados.
|
||||
forum = db.get_forum(topic['forum_id'], include_hidden=is_mod)
|
||||
if not forum:
|
||||
messages.error(request, 'Tema no disponible.')
|
||||
return redirect('app_forum')
|
||||
|
||||
# Contador de visitas (solo si la columna existe)
|
||||
if db.has_views_column():
|
||||
db.increment_views(topic_id)
|
||||
topic['views'] = (topic.get('views') or 0) + 1
|
||||
topic['has_views'] = True
|
||||
else:
|
||||
topic['has_views'] = False
|
||||
|
||||
total = db.count_posts(topic_id, include_deleted=is_mod)
|
||||
pag, offset = _paginate(total, page, settings.FORUM_POSTS_PER_PAGE)
|
||||
posts = db.get_posts(topic_id, offset, settings.FORUM_POSTS_PER_PAGE, is_mod)
|
||||
|
||||
# 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'] or is_mod),
|
||||
'move_forums': db.list_forums_for_move(topic['forum_id']) if is_mod 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(),
|
||||
}))
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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)
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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)
|
||||
|
||||
is_mod = perm.is_moderator(request)
|
||||
topic = db.get_topic(topic_id)
|
||||
if not topic:
|
||||
messages.error(request, 'Tema no encontrado.')
|
||||
return redirect('app_forum')
|
||||
# El foro padre debe ser visible (salvo moderador)
|
||||
if not db.get_forum(topic['forum_id'], include_hidden=is_mod):
|
||||
messages.error(request, 'Tema no disponible.')
|
||||
return redirect('app_forum')
|
||||
if topic['locked'] and not is_mod:
|
||||
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']}),
|
||||
}))
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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 _topic_locked_for(request, post['topic_id']):
|
||||
messages.error(request, 'El tema está bloqueado.')
|
||||
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'])
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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 _topic_locked_for(request, post['topic_id']):
|
||||
messages.error(request, 'El tema está bloqueado.')
|
||||
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'])
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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)
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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)
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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)
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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')
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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)
|
||||
|
||||
|
||||
@csrf_protect
|
||||
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