""" 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): with _conn().cursor() as cursor: cursor.execute( "SELECT COUNT(*) FROM forum_posts WHERE topic_id = %s AND deleted = 0", [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