Resuelto conflicto en X
This commit is contained in:
parent
758da0ad4c
commit
e9264bc6ce
9 changed files with 348 additions and 96 deletions
94
app.py
94
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,33 +70,45 @@ 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 = [], [], [], []
|
||||
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("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 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 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()
|
||||
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 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.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("/add", methods=["POST"])
|
||||
@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:
|
||||
|
|
@ -108,7 +121,20 @@ def add_feed():
|
|||
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"))
|
||||
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/<int:feed_id>", 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/<int:feed_id>")
|
||||
|
|
@ -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/<int:feed_id>")
|
||||
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):
|
||||
|
|
|
|||
57
templates/add_feed.html
Normal file
57
templates/add_feed.html
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Añadir Nuevo Feed{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
<h1>Añadir Nuevo Feed</h1>
|
||||
<p class="subtitle">Introduce los detalles de la nueva fuente de noticias RSS.</p>
|
||||
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
|
||||
</header>
|
||||
|
||||
<div class="form-section">
|
||||
<form action="{{ url_for('add_feed') }}" method="post" autocomplete="off">
|
||||
<div>
|
||||
<label for="nombre">Nombre del feed</label>
|
||||
<input id="nombre" name="nombre" type="text" placeholder="Ej: Noticias de Tecnología" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:15px;">
|
||||
<label for="url">URL del RSS</label>
|
||||
<input id="url" name="url" type="url" placeholder="https://ejemplo.com/rss" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:15px;">
|
||||
<label for="descripcion">Descripción</label>
|
||||
<textarea id="descripcion" name="descripcion" rows="2" placeholder="Breve descripción del contenido del feed"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px;">
|
||||
<div>
|
||||
<label for="categoria_id">Categoría</label>
|
||||
<select id="categoria_id" name="categoria_id" required>
|
||||
<option value="">— Elige categoría —</option>
|
||||
{% for cat in categorias %}
|
||||
<option value="{{ cat.id }}">{{ cat.nombre }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pais_id">País</label>
|
||||
<select name="pais_id" id="pais_id">
|
||||
<option value="">— Global / No aplica —</option>
|
||||
{% for pais in paises %}
|
||||
<option value="{{ pais.id }}">{{ pais.nombre }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="idioma">Idioma (código)</label>
|
||||
<input id="idioma" name="idioma" type="text" maxlength="2" placeholder="ej: es, en">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" style="margin-top: 25px; width: 100%;">Añadir Feed</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
57
templates/add_feeds.html
Normal file
57
templates/add_feeds.html
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Añadir Nuevo Feed{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
<h1>Añadir Nuevo Feed</h1>
|
||||
<p class="subtitle">Introduce los detalles de la nueva fuente de noticias RSS.</p>
|
||||
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
|
||||
</header>
|
||||
|
||||
<div class="form-section">
|
||||
<form action="{{ url_for('add_feed') }}" method="post" autocomplete="off">
|
||||
<div>
|
||||
<label for="nombre">Nombre del feed</label>
|
||||
<input id="nombre" name="nombre" type="text" placeholder="Ej: Noticias de Tecnología" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:15px;">
|
||||
<label for="url">URL del RSS</label>
|
||||
<input id="url" name="url" type="url" placeholder="https://ejemplo.com/rss" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:15px;">
|
||||
<label for="descripcion">Descripción</label>
|
||||
<textarea id="descripcion" name="descripcion" rows="2" placeholder="Breve descripción del contenido del feed"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px;">
|
||||
<div>
|
||||
<label for="categoria_id">Categoría</label>
|
||||
<select id="categoria_id" name="categoria_id" required>
|
||||
<option value="">— Elige categoría —</option>
|
||||
{% for cat in categorias %}
|
||||
<option value="{{ cat.id }}">{{ cat.nombre }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pais_id">País</label>
|
||||
<select name="pais_id" id="pais_id">
|
||||
<option value="">— Global / No aplica —</option>
|
||||
{% for pais in paises %}
|
||||
<option value="{{ pais.id }}">{{ pais.nombre }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="idioma">Idioma (código)</label>
|
||||
<input id="idioma" name="idioma" type="text" maxlength="2" placeholder="ej: es, en">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" style="margin-top: 25px; width: 100%;">Añadir Feed</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
@ -111,7 +126,7 @@
|
|||
{% if messages %}
|
||||
<ul class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<li class="{{ category }}">{{ message }}</li>
|
||||
<li class="flash-{{ category }}">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
|
|
|||
35
templates/dashboard.html
Normal file
35
templates/dashboard.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard de Feeds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
<h1>Dashboard de Feeds</h1>
|
||||
<p class="subtitle">Un resumen del estado de tu agregador de noticias.</p>
|
||||
<a href="{{ url_for('home') }}" class="top-link" style="margin-top:15px;">← Volver a las Noticias</a>
|
||||
</header>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.feeds_totales }}</div>
|
||||
<div class="stat-label">Feeds Totales</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.noticias_totales }}</div>
|
||||
<div class="stat-label">Noticias Recopiladas</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" style="color:#c0392b;">{{ stats.feeds_caidos }}</div>
|
||||
<div class="stat-label">Feeds Caídos / Inactivos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="text-align: center; padding: 30px;">
|
||||
<h2>Gestionar Feeds</h2>
|
||||
<p style="color: var(--text-color-light);">Aquí puedes ver la lista completa, editar, añadir o eliminar tus feeds.</p>
|
||||
<div style="margin-top: 20px; display: flex; justify-content: center; gap: 15px; flex-wrap: wrap;">
|
||||
<a href="{{ url_for('manage_feeds') }}" class="btn">Ver Lista Detallada</a>
|
||||
<a href="{{ url_for('add_feed') }}" class="btn">Añadir Nuevo Feed</a>
|
||||
<a href="{{ url_for('restore_feeds') }}" class="btn btn-secondary">Importar / Restaurar</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
</header>
|
||||
|
||||
<div class="form-section">
|
||||
<form method="post" autocomplete="off">
|
||||
<form method="post" action="{{ url_for('edit_feed', feed_id=feed.id) }}" autocomplete="off">
|
||||
<div>
|
||||
<label for="nombre">Nombre del feed</label>
|
||||
<input id="nombre" name="nombre" type="text" value="{{ feed.nombre }}" required>
|
||||
|
|
@ -57,8 +57,10 @@
|
|||
</div>
|
||||
|
||||
<button class="btn" type="submit">Guardar Cambios</button>
|
||||
<a href="{{ url_for('feeds') }}" style="margin-left: 15px;">Cancelar</a>
|
||||
<a href="{{ url_for('manage_feeds') }}" style="margin-left: 15px;">Cancelar</a>
|
||||
</form>
|
||||
</div>
|
||||
<a href="{{ url_for('feeds') }}" class="top-link">← Volver a la gestión de feeds</a>
|
||||
|
||||
<a href="{{ url_for('manage_feeds') }}" class="top-link">← Volver a la lista de feeds</a>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
64
templates/feeds_list.html
Normal file
64
templates/feeds_list.html
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Lista Detallada de Feeds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
<h1>Lista de Feeds</h1>
|
||||
<p class="subtitle">Mostrando {{ feeds|length }} de {{ total_feeds }} feeds. Página {{ page }} de {{ total_pages }}.</p>
|
||||
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
|
||||
</header>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="{{ url_for('manage_feeds', page=page-1) }}" class="page-link">« Anterior</a>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
{% if p == page %}
|
||||
<a href="#" class="page-link active">{{ p }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('manage_feeds', page=p) }}" class="page-link">{{ p }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page < total_pages %}
|
||||
<a href="{{ url_for('manage_feeds', page=page+1) }}" class="page-link">Siguiente »</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% for feed in feeds %}
|
||||
<div class="card feed-detail-card">
|
||||
<div class="feed-header">
|
||||
<h2>{{ feed.nombre }}</h2>
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('edit_feed', feed_id=feed.id) }}" class="btn btn-small">Editar</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed-body">
|
||||
<dl>
|
||||
<dt>ID:</dt><dd>{{ feed.id }}</dd>
|
||||
<dt>URL:</dt><dd><a href="{{ feed.url }}" target="_blank" rel="noopener">{{ feed.url }}</a></dd>
|
||||
<dt>Descripción:</dt><dd>{{ feed.descripcion or 'N/A' }}</dd>
|
||||
<dt>Idioma:</dt><dd>{{ feed.idioma or 'N/D' }}</dd>
|
||||
<dt>Estado:</dt><dd>
|
||||
{% if feed.activo %}
|
||||
<span style="color: #27ae60; font-weight:bold;">Activo</span>
|
||||
{% else %}
|
||||
<span style="color: #c0392b; font-weight: bold;">Inactivo</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>Fallos:</dt><dd>{{ feed.fallos }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not feeds %}
|
||||
<div class="card" style="text-align:center;">
|
||||
<p>No hay feeds para mostrar.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<header>
|
||||
<h1>Agregador de Noticias</h1>
|
||||
<p class="subtitle">Tus fuentes de información, en un solo lugar.</p>
|
||||
<a href="{{ url_for('feeds') }}" class="top-link" style="margin-top:15px;">⚙️ Gestionar Feeds</a>
|
||||
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">⚙️ Gestionar Feeds</a>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
|
|
|
|||
|
|
@ -1,33 +1,37 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Restaurar Feeds RSS{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Restaurar feeds desde backup CSV</h1>
|
||||
<div class="card" style="max-width:500px;margin:auto;">
|
||||
<form method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<label for="file"><strong>Selecciona el archivo CSV exportado de feeds:</strong></label>
|
||||
<input type="file" name="file" id="file" accept=".csv" required style="margin:10px 0;">
|
||||
<button class="btn" type="submit">Restaurar</button>
|
||||
</form>
|
||||
{% if msg %}
|
||||
<div style="margin:15px 0;">
|
||||
{% if "Error" in msg or "Error en fila" in msg %}
|
||||
<div style="color:#c00; font-weight:bold;">{{ msg|safe }}</div>
|
||||
{% else %}
|
||||
<div style="color:#198754; font-weight:bold;">{{ msg|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p style="font-size:0.92em;color:#64748b;">
|
||||
El archivo debe contener las columnas:<br>
|
||||
<code>id, nombre, [descripcion,] url, categoria_id, categoria, pais_id, pais, idioma, activo, fallos</code><br>
|
||||
<small>
|
||||
Las columnas <b>descripcion</b> e <b>idioma</b> son opcionales.<br>
|
||||
<b>activo</b> puede ser: <code>True</code>, <code>False</code>, <code>1</code> o <code>0</code>.<br>
|
||||
<b>idioma</b> debe ser el código ISO 639-1 de dos letras (<i>ej:</i> <code>es</code>, <code>en</code>, <code>fr</code>...).<br>
|
||||
Si falta alguna columna, la restauración puede fallar o ignorar ese campo.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<a href="/feeds" class="top-link">← Volver a feeds</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Restaurar Feeds desde Backup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
<h1>Restaurar Feeds</h1>
|
||||
<p class="subtitle">Importa todos tus feeds desde un único archivo de backup en formato CSV.</p>
|
||||
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
|
||||
</header>
|
||||
|
||||
<div class="form-section">
|
||||
<h2>Subir Archivo de Backup</h2>
|
||||
<form method="post" enctype="multipart/form-data" action="{{ url_for('restore_feeds') }}" autocomplete="off">
|
||||
<label for="file"><strong>Selecciona el archivo <code>feeds_backup.csv</code>:</strong></label>
|
||||
<input type="file" name="file" id="file" accept=".csv" required style="margin-top:10px;">
|
||||
<button class="btn" type="submit" style="margin-top:20px; width: 100%;">Iniciar Restauración</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Formato del Archivo CSV</h2>
|
||||
<p style="color: var(--text-color-light);">
|
||||
El archivo debe ser de tipo CSV y contener las siguientes columnas para que la importación funcione correctamente:
|
||||
</p>
|
||||
<code>id,nombre,descripcion,url,categoria_id,categoria,pais_id,pais,idioma,activo,fallos</code>
|
||||
<br><br>
|
||||
<small>
|
||||
<ul>
|
||||
<li>La columna `id` debe ser única para cada feed.</li>
|
||||
<li>La columna `url` también debe ser única.</li>
|
||||
<li><b>activo</b> puede ser: <code>True</code>, <code>False</code>, <code>1</code> o <code>0</code>.</li>
|
||||
<li><b>idioma</b> debe ser el código ISO 639-1 de dos letras (ej: <code>es</code>, <code>en</code>).</li>
|
||||
</ul>
|
||||
</small>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue