From eb93758c4312490d0485b1b1c5c0d6aca70d92bf Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 17:22:05 +0000 Subject: [PATCH] =?UTF-8?q?Foro:=20paginaci=C3=B3n=20en=20la=20b=C3=BAsque?= =?UTF-8?q?da=20y=20contador=20de=20visitas=20por=20tema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - búsqueda paginada (count_search_topics + offset/limit) con controles en la plantilla - contador de visitas: columna opcional forum_topics.views con detección automática (has_views_column), increment_views al ver el tema, mostrado en lista y cabecera - sql/forum_views.sql (ALTER opcional) + docs Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/FORO.md | 8 ++-- forum/db.py | 61 +++++++++++++++++++++++++++---- forum/templates/forum/forum.html | 2 +- forum/templates/forum/search.html | 11 ++++++ forum/templates/forum/topic.html | 1 + forum/views.py | 15 +++++++- sql/forum_views.sql | 4 ++ 7 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 sql/forum_views.sql diff --git a/docs/FORO.md b/docs/FORO.md index 071592b..89a3504 100644 --- a/docs/FORO.md +++ b/docs/FORO.md @@ -23,7 +23,8 @@ Symfony *NovaWeb-main*, contra las **mismas tablas** en la base de datos - Crear tema, responder, editar y borrar el post propio. - Moderación: fijar/desfijar, bloquear/desbloquear, mover, borrar/restaurar temas y posts. -- Buscador de temas (por título y contenido) en `/es/forum/search`. +- Buscador de temas (por título y contenido) **paginado** en `/es/forum/search`. +- Contador de **visitas** por tema (columna opcional `forum_topics.views`). - Perfil público básico (`/es/profile/`). - El autor se muestra con un **nombre legible** (parte local del email de la cuenta) en vez de `#1`. @@ -68,6 +69,8 @@ es seguro. ``` (Si ya existe la BD del portal Symfony con esas tablas, apunta `DB_NAME_WEB` a ella y omite este paso.) + - Opcional (contador de visitas): `mysql acore_web < sql/forum_views.sql`. + El foro detecta la columna automáticamente y funciona sin ella. 2. Configurar variables de entorno: ``` DB_NAME_WEB=acore_web @@ -81,5 +84,4 @@ es seguro. - Sistema de nicks propio del foro (ahora el nombre visible es la parte local del email de la cuenta). -- Paginación/relevancia en los resultados de búsqueda (ahora lista los 50 temas - más recientes que coinciden). +- Avatares de usuario y firmas. diff --git a/forum/db.py b/forum/db.py index fdad3bb..4d62c2d 100644 --- a/forum/db.py +++ b/forum/db.py @@ -25,6 +25,35 @@ 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) # -------------------------------------------------------------------------- @@ -110,11 +139,12 @@ def count_topics(forum_id): 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, + 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 @@ -138,10 +168,11 @@ def get_topics(forum_id, offset, limit): # -------------------------------------------------------------------------- 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, " - "locked, deleted, moved, updated_at FROM forum_topics WHERE id = %s", + f"locked, deleted, moved, updated_at{views_sel} FROM forum_topics WHERE id = %s", [topic_id], ) topic = _dictfetchone(cursor) @@ -333,22 +364,38 @@ def get_display_name(poster_id): return None -def search_topics(query, limit=50): +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, f.name AS forum_name + 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 + LIMIT %s OFFSET %s """, - [like, like, limit], + [like, like, limit, offset], ) return _dictfetchall(cursor) diff --git a/forum/templates/forum/forum.html b/forum/templates/forum/forum.html index 721e704..1674cc8 100644 --- a/forum/templates/forum/forum.html +++ b/forum/templates/forum/forum.html @@ -32,7 +32,7 @@
por {{ topic.display_poster }} · {{ topic.created }}
-
{{ topic.post_count }} respuestas
+
{{ topic.post_count }} respuestas · {{ topic.views }} vistas
{% if topic.last_poster %}
último: {{ topic.last_poster }}
{% endif %} {% if topic.last_time %}
{{ topic.last_time }}
{% endif %}
diff --git a/forum/templates/forum/search.html b/forum/templates/forum/search.html index 3d7681f..707d223 100644 --- a/forum/templates/forum/search.html +++ b/forum/templates/forum/search.html @@ -30,6 +30,17 @@ {% empty %}

No se encontraron temas.

{% endfor %} + + {% if pagination and pagination.total_pages > 1 %} +
+ {% if pagination.has_prev %}« Anterior{% endif %} + {% for p in pagination.page_range %} + {% if p == pagination.page %}{{ p }} + {% else %}{{ p }}{% endif %} + {% endfor %} + {% if pagination.has_next %}Siguiente »{% endif %} +
+ {% endif %} {% else %}

Escribe al menos 2 caracteres para buscar.

{% endif %} diff --git a/forum/templates/forum/topic.html b/forum/templates/forum/topic.html index f74f4fc..6ead61a 100644 --- a/forum/templates/forum/topic.html +++ b/forum/templates/forum/topic.html @@ -14,6 +14,7 @@
{% if forum %}← {{ forum.name }}{% endif %} + {{ topic.views }} visitas {% if topic.locked %}Tema bloqueado{% endif %}
diff --git a/forum/views.py b/forum/views.py index d080f15..9819be7 100644 --- a/forum/views.py +++ b/forum/views.py @@ -87,14 +87,22 @@ def view_forum(request, forum_id, page=1): 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: - results = db.search_topics(query) + 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, })) @@ -106,6 +114,11 @@ def view_topic(request, topic_id, page=1): forum = db.get_forum(topic['forum_id'], include_hidden=True) + # Contador de visitas (si la columna existe) + db.increment_views(topic_id) + if topic.get('views') is not None: + topic['views'] = topic['views'] + 1 + total = db.count_posts(topic_id) pag, offset = _paginate(total, page, settings.FORUM_POSTS_PER_PAGE) include_deleted = perm.is_moderator(request) diff --git a/sql/forum_views.sql b/sql/forum_views.sql new file mode 100644 index 0000000..ed5fb5f --- /dev/null +++ b/sql/forum_views.sql @@ -0,0 +1,4 @@ +-- Contador de visitas por tema (opcional). Aplicar sobre la base `acore_web`. +-- El foro funciona sin esta columna (detección automática); aplícala para +-- activar el contador de visitas. +ALTER TABLE `forum_topics` ADD COLUMN `views` INT NOT NULL DEFAULT 0;