Añade foro de comunidad integrado (app forum) con el diseño de NovaWoW
- app 'forum': portada (categorías/foros), ver foro y tema (paginado), crear tema, responder, editar/borrar posts y moderación (fijar, bloquear, mover, borrar/restaurar) - lee/escribe la BD acore_web (5ª conexión, DB_NAME_WEB) por SQL directo, tablas forum_categories/forums/forum_topics/forum_posts/forum_reads (portado de NovaWeb-main) - identidad = cuenta de juego en sesión; permisos simplificados por gmlevel (FORUM_MOD_GMLEVEL) en vez de la matriz group_level×permission del original - saneado del HTML de posts con nh3 (evita el XSS del original); POST+CSRF en formularios - plantillas con los partials del sitio; enlace 'FOROS' del menú apunta al foro interno - sql/forum_schema.sql y docs/FORO.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<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; }
|
||||
</style>
|
||||
{% if messages %}
|
||||
<div class="forum-messages forum-wrap">
|
||||
{% for message in messages %}
|
||||
<div class="forum-msg {{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,34 @@
|
||||
<!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 %}
|
||||
<input type="text" name="name" maxlength="45" placeholder="Título del tema" required>
|
||||
<textarea name="text" placeholder="Contenido del tema..." required></textarea>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -0,0 +1,32 @@
|
||||
<!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 %}
|
||||
<textarea name="text" required>{{ post.text|safe }}</textarea>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/social.html' %}
|
||||
{% include 'partials/footer.html' %}
|
||||
{% include 'partials/final.html' %}
|
||||
@@ -0,0 +1,63 @@
|
||||
<!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.poster }} · {{ topic.created }}</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div>{{ topic.post_count }} respuestas</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' %}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!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">
|
||||
{% 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' %}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!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' %}
|
||||
@@ -0,0 +1,120 @@
|
||||
<!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>{{ 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.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.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|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>
|
||||
· <a href="{% url 'forum_delete_post' post_id=post.id %}" onclick="return confirm('¿Borrar este mensaje?')">Borrar</a>
|
||||
{% endif %}
|
||||
{% if post.deleted and forum_is_moderator %}
|
||||
<a href="{% url 'forum_restore_post' post_id=post.id %}">Restaurar</a>
|
||||
{% 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 %}
|
||||
<textarea name="text" placeholder="Escribe tu respuesta..." required></textarea>
|
||||
<div class="forum-actions">
|
||||
<button type="submit" class="forum-btn">Publicar respuesta</button>
|
||||
</div>
|
||||
</form>
|
||||
</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 %}
|
||||
<a class="forum-btn" href="{% url 'forum_unlock_topic' topic_id=topic.id %}">Desbloquear</a>
|
||||
{% else %}
|
||||
<a class="forum-btn" href="{% url 'forum_lock_topic' topic_id=topic.id %}">Bloquear</a>
|
||||
{% endif %}
|
||||
<a class="forum-btn" href="{% url 'forum_toggle_sticky' topic_id=topic.id %}">{% if topic.sticky %}Quitar fijado{% else %}Fijar{% endif %}</a>
|
||||
{% if topic.deleted %}
|
||||
<a class="forum-btn" href="{% url 'forum_restore_topic' topic_id=topic.id %}">Restaurar tema</a>
|
||||
{% else %}
|
||||
<a class="forum-btn danger" href="{% url 'forum_delete_topic' topic_id=topic.id %}" onclick="return confirm('¿Borrar el tema completo?')">Borrar tema</a>
|
||||
{% 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' %}
|
||||
Reference in New Issue
Block a user