From e9264bc6cecaf854bfdefa28651572e64d04aeaa Mon Sep 17 00:00:00 2001 From: jlimolina Date: Mon, 9 Jun 2025 11:48:00 +0200 Subject: [PATCH] Resuelto conflicto en X --- app.py | 122 ++++++++++++++++++++--------------- templates/add_feed.html | 57 ++++++++++++++++ templates/add_feeds.html | 57 ++++++++++++++++ templates/base.html | 33 +++++++--- templates/dashboard.html | 35 ++++++++++ templates/edit_feed.html | 8 ++- templates/feeds_list.html | 64 ++++++++++++++++++ templates/noticias.html | 2 +- templates/restore_feeds.html | 66 ++++++++++--------- 9 files changed, 348 insertions(+), 96 deletions(-) create mode 100644 templates/add_feed.html create mode 100644 templates/add_feeds.html create mode 100644 templates/dashboard.html create mode 100644 templates/feeds_list.html diff --git a/app.py b/app.py index e0bd3af..0bbb2b5 100644 --- a/app.py +++ b/app.py @@ -4,6 +4,7 @@ import sys import hashlib import re import csv +import math from io import StringIO from datetime import datetime, timedelta import logging @@ -69,46 +70,71 @@ def home(): cat_id=int(cat_id) if cat_id else None, cont_id=int(cont_id) if cont_id else None, pais_id=int(pais_id) if pais_id else None) @app.route("/feeds") -def feeds(): - feeds_, categorias, continentes, paises = [], [], [], [] - try: - with get_conn() as conn: - with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor: - cursor.execute(""" - SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, f.pais_id, - f.activo, f.fallos, c.nombre as cat_nom, p.nombre as pais_nom - FROM feeds f - LEFT JOIN categorias c ON f.categoria_id = c.id - LEFT JOIN paises p ON f.pais_id = p.id - ORDER BY f.nombre - """) - feeds_ = cursor.fetchall() - cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre") - categorias = cursor.fetchall() - cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre") - continentes = cursor.fetchall() - cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre") - paises = cursor.fetchall() - except psycopg2.Error as db_err: - app.logger.error(f"[DB ERROR] Al leer feeds: {db_err}", exc_info=True) - flash("Error de base de datos al cargar la gestión de feeds.", "error") - return render_template("index.html", feeds=feeds_, categorias=categorias, continentes=continentes, paises=paises) - -@app.route("/add", methods=["POST"]) -def add_feed(): - nombre = request.form.get("nombre") +def dashboard(): + stats = {'feeds_totales': 0, 'noticias_totales': 0, 'feeds_caidos': 0} try: with get_conn() as conn: with conn.cursor() as cursor: - cursor.execute( - "INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma) VALUES (%s, %s, %s, %s, %s, %s)", - (nombre, request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), (request.form.get("idioma", "").strip() or None)) - ) - flash(f"Feed '{nombre}' añadido correctamente.", "success") + cursor.execute("SELECT COUNT(*) FROM feeds;") + stats['feeds_totales'] = cursor.fetchone()[0] + cursor.execute("SELECT COUNT(*) FROM noticias;") + stats['noticias_totales'] = cursor.fetchone()[0] + cursor.execute("SELECT COUNT(*) FROM feeds WHERE activo = FALSE;") + stats['feeds_caidos'] = cursor.fetchone()[0] except psycopg2.Error as db_err: - app.logger.error(f"[DB ERROR] Al agregar feed: {db_err}", exc_info=True) - flash(f"Error al añadir el feed: {db_err}", "error") - return redirect(url_for("feeds")) + app.logger.error(f"[DB ERROR] Al calcular estadísticas del dashboard: {db_err}") + flash("Error al conectar con la base de datos para mostrar el resumen.", "error") + return render_template("dashboard.html", stats=stats) + +@app.route("/feeds/manage") +def manage_feeds(): + page = request.args.get('page', 1, type=int) + per_page = 10 + offset = (page - 1) * per_page + feeds_list = [] + total_feeds = 0 + try: + with get_conn() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor: + cursor.execute("SELECT COUNT(*) FROM feeds") + total_feeds = cursor.fetchone()[0] + cursor.execute("SELECT * FROM feeds ORDER BY nombre LIMIT %s OFFSET %s", (per_page, offset)) + feeds_list = cursor.fetchall() + except psycopg2.Error as db_err: + app.logger.error(f"[DB ERROR] Al obtener lista de feeds: {db_err}") + flash("Error al obtener la lista de feeds.", "error") + total_pages = math.ceil(total_feeds / per_page) + return render_template("feeds_list.html", feeds=feeds_list, page=page, total_pages=total_pages, total_feeds=total_feeds) + +@app.route("/feeds/add", methods=['GET', 'POST']) +def add_feed(): + if request.method == 'POST': + nombre = request.form.get("nombre") + try: + with get_conn() as conn: + with conn.cursor() as cursor: + cursor.execute( + "INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma) VALUES (%s, %s, %s, %s, %s, %s)", + (nombre, request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), (request.form.get("idioma", "").strip() or None)) + ) + flash(f"Feed '{nombre}' añadido correctamente.", "success") + except psycopg2.Error as db_err: + app.logger.error(f"[DB ERROR] Al agregar feed: {db_err}", exc_info=True) + flash(f"Error al añadir el feed: {db_err}", "error") + return redirect(url_for("dashboard")) + + categorias, paises = [], [] + try: + with get_conn() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor: + cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre") + categorias = cursor.fetchall() + cursor.execute("SELECT id, nombre FROM paises ORDER BY nombre") + paises = cursor.fetchall() + except psycopg2.Error as db_err: + app.logger.error(f"[DB ERROR] Al cargar formulario para añadir feed: {db_err}") + flash("No se pudieron cargar las categorías o países.", "error") + return render_template("add_feed.html", categorias=categorias, paises=paises) @app.route("/edit/", methods=["GET", "POST"]) def edit_feed(feed_id): @@ -124,8 +150,7 @@ def edit_feed(feed_id): (request.form.get("nombre"), request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), idioma, activo, feed_id) ) flash("Feed actualizado correctamente.", "success") - return redirect(url_for("feeds")) - + return redirect(url_for("manage_feeds")) cursor.execute("SELECT * FROM feeds WHERE id = %s", (feed_id,)) feed = cursor.fetchone() cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre") @@ -135,12 +160,10 @@ def edit_feed(feed_id): except psycopg2.Error as db_err: app.logger.error(f"[DB ERROR] Al editar feed: {db_err}", exc_info=True) flash(f"Error al editar el feed: {db_err}", "error") - return redirect(url_for("feeds")) - + return redirect(url_for("manage_feeds")) if not feed: flash("No se encontró el feed solicitado.", "error") - return redirect(url_for("feeds")) - + return redirect(url_for("manage_feeds")) return render_template("edit_feed.html", feed=feed, categorias=categorias, paises=paises) @app.route("/delete/") @@ -153,7 +176,7 @@ def delete_feed(feed_id): except psycopg2.Error as db_err: app.logger.error(f"[DB ERROR] Al eliminar feed: {db_err}", exc_info=True) flash(f"Error al eliminar el feed: {db_err}", "error") - return redirect(url_for("feeds")) + return redirect(url_for("manage_feeds")) @app.route("/reactivar_feed/") def reactivar_feed(feed_id): @@ -165,7 +188,7 @@ def reactivar_feed(feed_id): except psycopg2.Error as db_err: app.logger.error(f"[DB ERROR] Al reactivar feed: {db_err}", exc_info=True) flash(f"Error al reactivar el feed: {db_err}", "error") - return redirect(url_for("feeds")) + return redirect(url_for("manage_feeds")) @app.route("/backup_feeds") def backup_feeds(): @@ -183,21 +206,18 @@ def backup_feeds(): feeds_ = cursor.fetchall() if not feeds_: flash("No hay feeds para exportar.", "warning") - return redirect(url_for("feeds")) - + return redirect(url_for("dashboard")) si = StringIO() writer = csv.DictWriter(si, fieldnames=[desc[0] for desc in cursor.description]) writer.writeheader() writer.writerows([dict(row) for row in feeds_]) - output = si.getvalue() si.close() - return Response(output, mimetype="text/csv", headers={"Content-Disposition": "attachment;filename=feeds_backup.csv"}) except Exception as e: app.logger.error(f"[ERROR] Al hacer backup de feeds: {e}", exc_info=True) flash("Error al generar el backup.", "error") - return redirect(url_for("feeds")) + return redirect(url_for("dashboard")) @app.route("/restore_feeds", methods=["GET", "POST"]) def restore_feeds(): @@ -211,7 +231,6 @@ def restore_feeds(): reader = csv.DictReader(file_stream) rows = list(reader) n_ok, n_err = 0, 0 - with get_conn() as conn: with conn.cursor() as cursor: for row in rows: @@ -242,8 +261,7 @@ def restore_feeds(): except Exception as e: app.logger.error(f"Error al restaurar feeds desde CSV: {e}", exc_info=True) flash(f"Ocurrió un error general al procesar el archivo: {e}", "error") - return redirect(url_for("feeds")) - + return redirect(url_for("dashboard")) return render_template("restore_feeds.html") def sumar_fallo_feed(cursor, feed_id): diff --git a/templates/add_feed.html b/templates/add_feed.html new file mode 100644 index 0000000..1665a3c --- /dev/null +++ b/templates/add_feed.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block title %}Añadir Nuevo Feed{% endblock %} + +{% block content %} +
+

Añadir Nuevo Feed

+

Introduce los detalles de la nueva fuente de noticias RSS.

+ ← Volver al Dashboard +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock %} diff --git a/templates/add_feeds.html b/templates/add_feeds.html new file mode 100644 index 0000000..1665a3c --- /dev/null +++ b/templates/add_feeds.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block title %}Añadir Nuevo Feed{% endblock %} + +{% block content %} +
+

Añadir Nuevo Feed

+

Introduce los detalles de la nueva fuente de noticias RSS.

+ ← Volver al Dashboard +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 7710b9f..713116e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -67,6 +67,7 @@ .btn, button { padding: 12px 25px; background: var(--gradiente-principal); color: white !important; border: none; border-radius: var(--border-radius-sm); font-size: 1rem; font-weight: 600; cursor: pointer; transition: all var(--transition-speed) ease; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); text-decoration: none; display: inline-block; text-align: center; } .btn:hover, button:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); text-decoration: none; } .btn-secondary { background: #34495e; } .btn-secondary:hover { background: #2c3e50; } + .btn-small { padding: 6px 14px; font-size: 0.9rem; } a { color: var(--secondary-color); text-decoration: none; font-weight: 500; } a:hover { text-decoration: underline; } .top-link { display: inline-block; margin-bottom: 25px; font-weight: 500; color: var(--primary-color); } .top-link:hover { text-decoration: underline; } @@ -82,14 +83,6 @@ .noticia-texto h3 a:hover { color: var(--primary-color); } .noticia-meta { font-size: 0.8rem; color: var(--text-color-light); margin-bottom: 8px; } - /* --- Tabla de Gestión de Feeds --- */ - .table-wrapper { overflow-x: auto; } - table { width: 100%; border-collapse: collapse; margin-top: 20px; } - th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid var(--border-color); } - th { background-color: rgba(106, 27, 203, 0.05); font-weight: 600; } - tr:last-child td { border-bottom: none; } - td .actions a { margin-right: 10px; } - /* --- Alertas y Mensajes Flash --- */ .flash-messages { list-style: none; padding: 0; margin-bottom: 20px; } .flash-messages li { padding: 15px 20px; border-radius: var(--border-radius-sm); border-left: 5px solid; } @@ -97,11 +90,33 @@ .flash-messages .success { background-color: #e6fcf5; color: #00b894; border-color: #00b894; } .flash-messages .warning { background-color: #fffbeb; color: #f39c12; border-color: #f39c12; } + /* --- INICIO DE ESTILOS AÑADIDOS PARA DASHBOARD Y PAGINACIÓN --- */ + .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; } + .stat-card { background: rgba(255, 255, 255, 0.8); padding: 20px; border-radius: var(--border-radius-md); text-align: center; border: 1px solid var(--border-color); transition: all 0.3s ease; } + .stat-card:hover { transform: translateY(-5px); box-shadow: 0 4px 15px rgba(0,0,0,0.08); } + .stat-card .stat-number { font-size: 2.5rem; font-weight: 600; background: var(--gradiente-principal); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.2; } + .stat-card .stat-label { font-size: 0.9rem; color: var(--text-color-light); font-weight: 500; margin-top: 5px; } + + .pagination { display: flex; justify-content: center; align-items: center; gap: 5px; margin: 30px 0; flex-wrap: wrap; } + .page-link { display: inline-block; padding: 8px 14px; background: rgba(255, 255, 255, 0.6); border: 1px solid var(--border-color); border-radius: var(--border-radius-sm); color: var(--primary-color); text-decoration: none; transition: all 0.2s ease; } + .page-link:hover { background: white; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } + .page-link.active { background: var(--gradiente-principal); color: white; border-color: transparent; cursor: default; } + + .feed-detail-card { padding: 0; } + .feed-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; background: rgba(233, 236, 239, 0.5); padding: 15px 25px; border-bottom: 1px solid var(--border-color); } + .feed-header h2 { margin: 0; font-size: 1.4rem; } + .feed-body { padding: 25px; } + .feed-body dl { display: grid; grid-template-columns: 120px 1fr; gap: 10px 20px; } + .feed-body dt { font-weight: 600; color: var(--text-color-light); } + .feed-body dd { margin: 0; word-break: break-all; } + /* --- FIN DE ESTILOS AÑADIDOS --- */ + /* --- Responsividad --- */ @media (max-width: 768px) { .container { padding: 20px; margin: 15px; } h1 { font-size: 2rem; } .noticia-item { flex-direction: column; } + .feed-body dl { grid-template-columns: 100px 1fr; } } @@ -111,7 +126,7 @@ {% if messages %}
    {% for category, message in messages %} -
  • {{ message }}
  • +
  • {{ message }}
  • {% endfor %}
{% endif %} diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..e41321e --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block title %}Dashboard de Feeds{% endblock %} + +{% block content %} +
+

Dashboard de Feeds

+

Un resumen del estado de tu agregador de noticias.

+ ← Volver a las Noticias +
+ +
+
+
{{ stats.feeds_totales }}
+
Feeds Totales
+
+
+
{{ stats.noticias_totales }}
+
Noticias Recopiladas
+
+
+
{{ stats.feeds_caidos }}
+
Feeds Caídos / Inactivos
+
+
+ +
+

Gestionar Feeds

+

Aquí puedes ver la lista completa, editar, añadir o eliminar tus feeds.

+ +
+{% endblock %} diff --git a/templates/edit_feed.html b/templates/edit_feed.html index 764eda4..a9a3bc8 100644 --- a/templates/edit_feed.html +++ b/templates/edit_feed.html @@ -9,7 +9,7 @@
-
+
@@ -57,8 +57,10 @@
- Cancelar + Cancelar
- ← Volver a la gestión de feeds + + ← Volver a la lista de feeds + {% endblock %} diff --git a/templates/feeds_list.html b/templates/feeds_list.html new file mode 100644 index 0000000..1442f7f --- /dev/null +++ b/templates/feeds_list.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} +{% block title %}Lista Detallada de Feeds{% endblock %} + +{% block content %} +
+

Lista de Feeds

+

Mostrando {{ feeds|length }} de {{ total_feeds }} feeds. Página {{ page }} de {{ total_pages }}.

+ ← Volver al Dashboard +
+ +{% if total_pages > 1 %} + +{% endif %} + +{% for feed in feeds %} +
+
+

{{ feed.nombre }}

+
+ Editar +
+
+
+
+
ID:
{{ feed.id }}
+
URL:
{{ feed.url }}
+
Descripción:
{{ feed.descripcion or 'N/A' }}
+
Idioma:
{{ feed.idioma or 'N/D' }}
+
Estado:
+ {% if feed.activo %} + Activo + {% else %} + Inactivo + {% endif %} +
+
Fallos:
{{ feed.fallos }}
+
+
+
+{% endfor %} + +{% if not feeds %} +
+

No hay feeds para mostrar.

+
+{% endif %} + +{% endblock %} diff --git a/templates/noticias.html b/templates/noticias.html index 98d57ba..851df2f 100644 --- a/templates/noticias.html +++ b/templates/noticias.html @@ -6,7 +6,7 @@

Agregador de Noticias

Tus fuentes de información, en un solo lugar.

- ⚙️ Gestionar Feeds + ⚙️ Gestionar Feeds
diff --git a/templates/restore_feeds.html b/templates/restore_feeds.html index 8175cc4..17e1686 100755 --- a/templates/restore_feeds.html +++ b/templates/restore_feeds.html @@ -1,33 +1,37 @@ {% extends "base.html" %} -{% block title %}Restaurar Feeds RSS{% endblock %} -{% block content %} -

Restaurar feeds desde backup CSV

-
-
- - - -
- {% if msg %} -
- {% if "Error" in msg or "Error en fila" in msg %} -
{{ msg|safe }}
- {% else %} -
{{ msg|safe }}
- {% endif %} -
- {% endif %} -

- El archivo debe contener las columnas:
- id, nombre, [descripcion,] url, categoria_id, categoria, pais_id, pais, idioma, activo, fallos
- - Las columnas descripcion e idioma son opcionales.
- activo puede ser: True, False, 1 o 0.
- idioma debe ser el código ISO 639-1 de dos letras (ej: es, en, fr...).
- Si falta alguna columna, la restauración puede fallar o ignorar ese campo. -
-

-
- ← Volver a feeds -{% endblock %} +{% block title %}Restaurar Feeds desde Backup{% endblock %} + +{% block content %} +
+

Restaurar Feeds

+

Importa todos tus feeds desde un único archivo de backup en formato CSV.

+ ← Volver al Dashboard +
+ +
+

Subir Archivo de Backup

+
+ + + +
+
+ +
+

Formato del Archivo CSV

+

+ El archivo debe ser de tipo CSV y contener las siguientes columnas para que la importación funcione correctamente: +

+ id,nombre,descripcion,url,categoria_id,categoria,pais_id,pais,idioma,activo,fallos +

+ +
    +
  • La columna `id` debe ser única para cada feed.
  • +
  • La columna `url` también debe ser única.
  • +
  • activo puede ser: True, False, 1 o 0.
  • +
  • idioma debe ser el código ISO 639-1 de dos letras (ej: es, en).
  • +
+
+
+{% endblock %}