Foro: paginación en la búsqueda y contador de visitas por tema
- 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) <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -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.
|
- Crear tema, responder, editar y borrar el post propio.
|
||||||
- Moderación: fijar/desfijar, bloquear/desbloquear, mover, borrar/restaurar temas
|
- Moderación: fijar/desfijar, bloquear/desbloquear, mover, borrar/restaurar temas
|
||||||
y posts.
|
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/<usuario>`).
|
- Perfil público básico (`/es/profile/<usuario>`).
|
||||||
- El autor se muestra con un **nombre legible** (parte local del email de la
|
- El autor se muestra con un **nombre legible** (parte local del email de la
|
||||||
cuenta) en vez de `<bnetId>#1`.
|
cuenta) en vez de `<bnetId>#1`.
|
||||||
@@ -68,6 +69,8 @@ es seguro.
|
|||||||
```
|
```
|
||||||
(Si ya existe la BD del portal Symfony con esas tablas, apunta `DB_NAME_WEB`
|
(Si ya existe la BD del portal Symfony con esas tablas, apunta `DB_NAME_WEB`
|
||||||
a ella y omite este paso.)
|
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:
|
2. Configurar variables de entorno:
|
||||||
```
|
```
|
||||||
DB_NAME_WEB=acore_web
|
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
|
- Sistema de nicks propio del foro (ahora el nombre visible es la parte local del
|
||||||
email de la cuenta).
|
email de la cuenta).
|
||||||
- Paginación/relevancia en los resultados de búsqueda (ahora lista los 50 temas
|
- Avatares de usuario y firmas.
|
||||||
más recientes que coinciden).
|
|
||||||
|
|||||||
+54
-7
@@ -25,6 +25,35 @@ def _conn():
|
|||||||
return connections['acore_web']
|
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)
|
# Portada: categorías + foros (con estadísticas)
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -110,11 +139,12 @@ def count_topics(forum_id):
|
|||||||
|
|
||||||
|
|
||||||
def get_topics(forum_id, offset, limit):
|
def get_topics(forum_id, offset, limit):
|
||||||
|
views_sel = "t.views," if has_views_column() else "0 AS views,"
|
||||||
with _conn().cursor() as cursor:
|
with _conn().cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT t.id, t.name, t.poster, t.poster_id, t.created, t.sticky,
|
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
|
(SELECT COUNT(*) FROM forum_posts p
|
||||||
WHERE p.topic_id = t.id AND p.deleted = 0) AS post_count,
|
WHERE p.topic_id = t.id AND p.deleted = 0) AS post_count,
|
||||||
(SELECT p2.poster FROM forum_posts p2
|
(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):
|
def get_topic(topic_id, include_hidden=False):
|
||||||
|
views_sel = ", views" if has_views_column() else ", 0 AS views"
|
||||||
with _conn().cursor() as cursor:
|
with _conn().cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT id, forum_id, name, poster, poster_id, created, sticky, "
|
"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_id],
|
||||||
)
|
)
|
||||||
topic = _dictfetchone(cursor)
|
topic = _dictfetchone(cursor)
|
||||||
@@ -333,22 +364,38 @@ def get_display_name(poster_id):
|
|||||||
return None
|
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)."""
|
"""Busca temas por título o por contenido de sus posts (foros visibles)."""
|
||||||
like = f"%{query}%"
|
like = f"%{query}%"
|
||||||
with _conn().cursor() as cursor:
|
with _conn().cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT DISTINCT t.id, t.name, t.poster, t.poster_id, t.forum_id,
|
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
|
FROM forum_topics t
|
||||||
JOIN forums f ON f.id = t.forum_id AND f.visibility = 1 AND f.deleted = 0
|
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
|
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)
|
WHERE t.deleted = 0 AND (t.name LIKE %s OR p.text LIKE %s)
|
||||||
ORDER BY t.updated_at DESC
|
ORDER BY t.updated_at DESC
|
||||||
LIMIT %s
|
LIMIT %s OFFSET %s
|
||||||
""",
|
""",
|
||||||
[like, like, limit],
|
[like, like, limit, offset],
|
||||||
)
|
)
|
||||||
return _dictfetchall(cursor)
|
return _dictfetchall(cursor)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="desc">por {{ topic.display_poster }} · {{ topic.created }}</div>
|
<div class="desc">por {{ topic.display_poster }} · {{ topic.created }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div>{{ topic.post_count }} respuestas</div>
|
<div>{{ topic.post_count }} respuestas · {{ topic.views }} vistas</div>
|
||||||
{% if topic.last_poster %}<div>último: {{ topic.last_poster }}</div>{% endif %}
|
{% if topic.last_poster %}<div>último: {{ topic.last_poster }}</div>{% endif %}
|
||||||
{% if topic.last_time %}<div>{{ topic.last_time }}</div>{% endif %}
|
{% if topic.last_time %}<div>{{ topic.last_time }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,17 @@
|
|||||||
{% empty %}
|
{% empty %}
|
||||||
<p>No se encontraron temas.</p>
|
<p>No se encontraron temas.</p>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% else %}
|
||||||
<p>Escribe al menos 2 caracteres para buscar.</p>
|
<p>Escribe al menos 2 caracteres para buscar.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<div class="forum-wrap">
|
<div class="forum-wrap">
|
||||||
<div class="forum-actions">
|
<div class="forum-actions">
|
||||||
{% if forum %}<a class="forum-btn" href="{% url 'forum_view_p1' forum_id=forum.id %}">← {{ forum.name }}</a>{% endif %}
|
{% if forum %}<a class="forum-btn" href="{% url 'forum_view_p1' forum_id=forum.id %}">← {{ forum.name }}</a>{% endif %}
|
||||||
|
<span style="color:#b1997f; align-self:center">{{ topic.views }} visitas</span>
|
||||||
{% if topic.locked %}<span class="forum-badge">Tema bloqueado</span>{% endif %}
|
{% if topic.locked %}<span class="forum-badge">Tema bloqueado</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -87,14 +87,22 @@ def view_forum(request, forum_id, page=1):
|
|||||||
|
|
||||||
def search(request):
|
def search(request):
|
||||||
query = (request.GET.get('q') or '').strip()
|
query = (request.GET.get('q') or '').strip()
|
||||||
|
try:
|
||||||
|
page = int(request.GET.get('page', 1))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
page = 1
|
||||||
results = []
|
results = []
|
||||||
|
pag = None
|
||||||
if len(query) >= 2:
|
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:
|
for r in results:
|
||||||
r['display_poster'] = db.get_display_name(r['poster_id']) or r['poster']
|
r['display_poster'] = db.get_display_name(r['poster_id']) or r['poster']
|
||||||
return render(request, 'forum/search.html', _base_context(request, {
|
return render(request, 'forum/search.html', _base_context(request, {
|
||||||
'query': query,
|
'query': query,
|
||||||
'results': results,
|
'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)
|
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)
|
total = db.count_posts(topic_id)
|
||||||
pag, offset = _paginate(total, page, settings.FORUM_POSTS_PER_PAGE)
|
pag, offset = _paginate(total, page, settings.FORUM_POSTS_PER_PAGE)
|
||||||
include_deleted = perm.is_moderator(request)
|
include_deleted = perm.is_moderator(request)
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user