Foro: buscador de temas y nombre de autor legible

- forum_search: busca temas por título y contenido (foros visibles)
- db.get_display_name: nombre visible del autor = parte local del email de la cuenta
  (en vez de <bnetId>#1), usado en el panel de posts y la lista de temas
- buscador en la portada del foro + plantilla search.html
- docs/FORO.md actualizado

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 17:18:34 +00:00
parent 874a0c1ad9
commit fd49d6cd96
8 changed files with 125 additions and 5 deletions
+7 -2
View File
@@ -23,7 +23,10 @@ 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`.
- 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
cuenta) en vez de `<bnetId>#1`.
Rutas bajo `/es/forum...` (idénticas al original: `app_forum`, `forum_view`, Rutas bajo `/es/forum...` (idénticas al original: `app_forum`, `forum_view`,
`forum_topic`, `forum_create_topic`, etc.). `forum_topic`, `forum_create_topic`, etc.).
@@ -76,5 +79,7 @@ es seguro.
## Notas / posibles mejoras ## Notas / posibles mejoras
- El autor se muestra como el nombre de cuenta de juego (`<bnetId>#1`); se podría - Sistema de nicks propio del foro (ahora el nombre visible es la parte local del
mapear a un nick o al email como en «Mi cuenta». email de la cuenta).
- Paginación/relevancia en los resultados de búsqueda (ahora lista los 50 temas
más recientes que coinciden).
+46 -1
View File
@@ -231,9 +231,11 @@ def get_author_info(poster_id):
Datos del autor para mostrar en los posts: nivel GM, fecha de registro y 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. nº de mensajes. Cruza acore_auth (account/account_access) y acore_web.
""" """
info = {'gmlevel': 0, 'joindate': None, 'post_count': 0, 'status': 'Miembro'} info = {'gmlevel': 0, 'joindate': None, 'post_count': 0, 'status': 'Miembro',
'display_name': None}
if not poster_id: if not poster_id:
return info return info
info['display_name'] = get_display_name(poster_id)
try: try:
with connections['acore_auth'].cursor() as cursor: with connections['acore_auth'].cursor() as cursor:
cursor.execute("SELECT MAX(gmlevel) FROM account_access WHERE id = %s", [poster_id]) cursor.execute("SELECT MAX(gmlevel) FROM account_access WHERE id = %s", [poster_id])
@@ -308,6 +310,49 @@ def mark_read(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 search_topics(query, limit=50):
"""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
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
""",
[like, like, limit],
)
return _dictfetchall(cursor)
def list_forums_for_move(exclude_forum_id=None): def list_forums_for_move(exclude_forum_id=None):
"""Lista de foros visibles para el desplegable de 'mover tema'.""" """Lista de foros visibles para el desplegable de 'mover tema'."""
with _conn().cursor() as cursor: with _conn().cursor() as cursor:
+1 -1
View File
@@ -29,7 +29,7 @@
{% if topic.moved %}<span class="forum-badge">Movido</span>{% endif %} {% if topic.moved %}<span class="forum-badge">Movido</span>{% endif %}
{{ topic.name }} {{ topic.name }}
</a> </a>
<div class="desc">por {{ topic.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</div>
+5
View File
@@ -12,6 +12,11 @@
<div class="body-box-content"> <div class="body-box-content">
{% include 'forum/_style.html' %} {% include 'forum/_style.html' %}
<div class="forum-wrap"> <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 %} {% for category in categories %}
<div class="forum-cat"> <div class="forum-cat">
<div class="forum-cat-title">{{ category.name }}</div> <div class="forum-cat-title">{{ category.name }}</div>
+49
View File
@@ -0,0 +1,49 @@
<!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 %}
{% 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 -1
View File
@@ -21,7 +21,7 @@
<div class="forum-post {% if post.deleted %}forum-deleted{% endif %}" id="post-{{ post.id }}"> <div class="forum-post {% if post.deleted %}forum-deleted{% endif %}" id="post-{{ post.id }}">
<div class="left"> <div class="left">
<div class="pname"> <div class="pname">
<a href="{% url 'forum_profile' username=post.poster %}" style="color:#e0c07a">{{ post.poster }}</a> <a href="{% url 'forum_profile' username=post.poster %}" style="color:#e0c07a">{{ post.author.display_name|default:post.poster }}</a>
</div> </div>
<div class="prole">{{ post.author.status }}</div> <div class="prole">{{ post.author.status }}</div>
<div class="pmeta"> <div class="pmeta">
+1
View File
@@ -6,6 +6,7 @@ from . import views
# que la genérica forum/<int:forum_id>/... # que la genérica forum/<int:forum_id>/...
urlpatterns = [ urlpatterns = [
path('forum', views.index, name='app_forum'), path('forum', views.index, name='app_forum'),
path('forum/search', views.search, name='forum_search'),
# Temas # Temas
path('forum/topic/<int:topic_id>/reply', views.reply_to_topic, name='forum_post_reply'), path('forum/topic/<int:topic_id>/reply', views.reply_to_topic, name='forum_post_reply'),
+15
View File
@@ -75,6 +75,8 @@ def view_forum(request, forum_id, page=1):
total = db.count_topics(forum_id) total = db.count_topics(forum_id)
pag, offset = _paginate(total, page, settings.FORUM_TOPICS_PER_PAGE) pag, offset = _paginate(total, page, settings.FORUM_TOPICS_PER_PAGE)
topics = db.get_topics(forum_id, offset, 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, { return render(request, 'forum/forum.html', _base_context(request, {
'forum': forum, 'forum': forum,
@@ -83,6 +85,19 @@ def view_forum(request, forum_id, page=1):
})) }))
def search(request):
query = (request.GET.get('q') or '').strip()
results = []
if len(query) >= 2:
results = db.search_topics(query)
for r in results:
r['display_poster'] = db.get_display_name(r['poster_id']) or r['poster']
return render(request, 'forum/search.html', _base_context(request, {
'query': query,
'results': results,
}))
def view_topic(request, topic_id, page=1): def view_topic(request, topic_id, page=1):
topic = db.get_topic(topic_id, include_hidden=perm.is_moderator(request)) topic = db.get_topic(topic_id, include_hidden=perm.is_moderator(request))
if not topic: if not topic: