Initial clean commit
This commit is contained in:
commit
6784d81c2c
141 changed files with 25219 additions and 0 deletions
428
routers/feeds.py
Normal file
428
routers/feeds.py
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
from flask import Blueprint, render_template, request, redirect, flash, url_for, jsonify
|
||||
from db import get_conn
|
||||
from psycopg2 import extras
|
||||
from models.categorias import get_categorias
|
||||
from models.paises import get_paises
|
||||
from utils.feed_discovery import discover_feeds, validate_feed, get_feed_metadata
|
||||
|
||||
# Blueprint correcto
|
||||
feeds_bp = Blueprint("feeds", __name__, url_prefix="/feeds")
|
||||
|
||||
|
||||
@feeds_bp.route("/")
|
||||
def list_feeds():
|
||||
"""Listado con filtros"""
|
||||
page = max(int(request.args.get("page", 1)), 1)
|
||||
per_page = 50
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
pais_id = request.args.get("pais_id")
|
||||
categoria_id = request.args.get("categoria_id")
|
||||
estado = request.args.get("estado") or ""
|
||||
|
||||
where = []
|
||||
params = []
|
||||
|
||||
if pais_id:
|
||||
where.append("f.pais_id = %s")
|
||||
params.append(int(pais_id))
|
||||
|
||||
if categoria_id:
|
||||
where.append("f.categoria_id = %s")
|
||||
params.append(int(categoria_id))
|
||||
|
||||
if estado == "activos":
|
||||
where.append("f.activo = TRUE")
|
||||
elif estado == "inactivos":
|
||||
where.append("f.activo = FALSE")
|
||||
elif estado == "errores":
|
||||
where.append("COALESCE(f.fallos, 0) > 0")
|
||||
|
||||
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
||||
|
||||
with get_conn() as conn, conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
|
||||
# Total
|
||||
# Total
|
||||
cur.execute(f"SELECT COUNT(*) FROM feeds f {where_sql}", params)
|
||||
total_feeds = cur.fetchone()[0]
|
||||
|
||||
# Caídos (Inactivos o con max fallos logic check, usually inactive is enough if logic works)
|
||||
# Using the same filter context to see how many of THESE are fallen
|
||||
# Caídos (Inactivos o con max fallos logic check)
|
||||
# Using the same filter context to see how many of THESE are fallen
|
||||
|
||||
caidos_condition = "(f.activo = FALSE OR f.fallos >= 5)"
|
||||
|
||||
if where_sql:
|
||||
# where_sql ya incluye "WHERE ..."
|
||||
caidos_sql = f"SELECT COUNT(*) FROM feeds f {where_sql} AND {caidos_condition}"
|
||||
else:
|
||||
caidos_sql = f"SELECT COUNT(*) FROM feeds f WHERE {caidos_condition}"
|
||||
|
||||
cur.execute(caidos_sql, params)
|
||||
feeds_caidos = cur.fetchone()[0]
|
||||
|
||||
total_pages = (total_feeds // per_page) + (1 if total_feeds % per_page else 0)
|
||||
|
||||
# Lista paginada
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT
|
||||
f.id, f.nombre, f.descripcion, f.url,
|
||||
f.activo, f.fallos, f.last_error,
|
||||
c.nombre AS categoria,
|
||||
p.nombre AS pais,
|
||||
(SELECT COUNT(*) FROM noticias n WHERE n.fuente_nombre = f.nombre) as noticias_count
|
||||
FROM feeds f
|
||||
LEFT JOIN categorias c ON c.id = f.categoria_id
|
||||
LEFT JOIN paises p ON p.id = f.pais_id
|
||||
{where_sql}
|
||||
ORDER BY p.nombre NULLS LAST, f.activo DESC, f.fallos ASC, c.nombre NULLS LAST, f.nombre
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
params + [per_page, offset],
|
||||
)
|
||||
feeds = cur.fetchall()
|
||||
|
||||
# Selects
|
||||
cur.execute("SELECT id, nombre FROM categorias ORDER BY nombre;")
|
||||
categorias = cur.fetchall()
|
||||
|
||||
cur.execute("SELECT id, nombre FROM paises ORDER BY nombre;")
|
||||
paises = cur.fetchall()
|
||||
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return render_template(
|
||||
"_feeds_table.html",
|
||||
feeds=feeds,
|
||||
total_feeds=total_feeds,
|
||||
feeds_caidos=feeds_caidos,
|
||||
total_pages=total_pages,
|
||||
page=page,
|
||||
filtro_pais_id=pais_id,
|
||||
filtro_categoria_id=categoria_id,
|
||||
filtro_estado=estado,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"feeds_list.html",
|
||||
feeds=feeds,
|
||||
total_feeds=total_feeds,
|
||||
feeds_caidos=feeds_caidos,
|
||||
total_pages=total_pages,
|
||||
page=page,
|
||||
categorias=categorias,
|
||||
paises=paises,
|
||||
filtro_pais_id=pais_id,
|
||||
filtro_categoria_id=categoria_id,
|
||||
filtro_estado=estado,
|
||||
)
|
||||
|
||||
|
||||
@feeds_bp.route("/add", methods=["GET", "POST"])
|
||||
def add_feed():
|
||||
"""Añadir feed"""
|
||||
with get_conn() as conn:
|
||||
categorias = get_categorias(conn)
|
||||
paises = get_paises(conn)
|
||||
|
||||
if request.method == "POST":
|
||||
nombre = request.form.get("nombre")
|
||||
descripcion = request.form.get("descripcion") or None
|
||||
url = request.form.get("url")
|
||||
categoria_id = request.form.get("categoria_id")
|
||||
pais_id = request.form.get("pais_id")
|
||||
idioma = (request.form.get("idioma") or "").strip().lower()[:2] or None
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
nombre,
|
||||
descripcion,
|
||||
url,
|
||||
int(categoria_id) if categoria_id else None,
|
||||
int(pais_id) if pais_id else None,
|
||||
idioma,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
flash("Feed añadido correctamente.", "success")
|
||||
return redirect(url_for("feeds.list_feeds"))
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Error al añadir feed: {e}", "error")
|
||||
|
||||
return render_template("add_feed.html", categorias=categorias, paises=paises)
|
||||
|
||||
|
||||
@feeds_bp.route("/<int:feed_id>/edit", methods=["GET", "POST"])
|
||||
def edit_feed(feed_id):
|
||||
"""Editar feed"""
|
||||
with get_conn() as conn, conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
|
||||
cur.execute("SELECT * FROM feeds WHERE id = %s;", (feed_id,))
|
||||
feed = cur.fetchone()
|
||||
|
||||
if not feed:
|
||||
flash("Feed no encontrado.", "error")
|
||||
return redirect(url_for("feeds.list_feeds"))
|
||||
|
||||
categorias = get_categorias(conn)
|
||||
paises = get_paises(conn)
|
||||
|
||||
if request.method == "POST":
|
||||
nombre = request.form.get("nombre")
|
||||
descripcion = request.form.get("descripcion") or None
|
||||
url = request.form.get("url")
|
||||
categoria_id = request.form.get("categoria_id")
|
||||
pais_id = request.form.get("pais_id")
|
||||
idioma = (request.form.get("idioma") or "").strip().lower()[:2] or None
|
||||
activo = bool(request.form.get("activo"))
|
||||
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE feeds
|
||||
SET nombre=%s, descripcion=%s, url=%s,
|
||||
categoria_id=%s, pais_id=%s, idioma=%s, activo=%s
|
||||
WHERE id=%s;
|
||||
""",
|
||||
(
|
||||
nombre,
|
||||
descripcion,
|
||||
url,
|
||||
int(categoria_id) if categoria_id else None,
|
||||
int(pais_id) if pais_id else None,
|
||||
idioma,
|
||||
activo,
|
||||
feed_id,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
flash("Feed actualizado.", "success")
|
||||
return redirect(url_for("feeds.list_feeds"))
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Error al actualizar: {e}", "error")
|
||||
|
||||
return render_template("edit_feed.html", feed=feed, categorias=categorias, paises=paises)
|
||||
|
||||
|
||||
@feeds_bp.route("/<int:feed_id>/delete")
|
||||
def delete_feed(feed_id):
|
||||
"""Eliminar feed"""
|
||||
with get_conn() as conn, conn.cursor() as cur:
|
||||
try:
|
||||
cur.execute("DELETE FROM feeds WHERE id=%s;", (feed_id,))
|
||||
conn.commit()
|
||||
flash("Feed eliminado.", "success")
|
||||
except Exception as e:
|
||||
flash(f"No se pudo eliminar: {e}", "error")
|
||||
|
||||
return redirect(url_for("feeds.list_feeds"))
|
||||
|
||||
|
||||
@feeds_bp.route("/<int:feed_id>/reactivar")
|
||||
def reactivar_feed(feed_id):
|
||||
"""Reactivar feed KO"""
|
||||
with get_conn() as conn, conn.cursor() as cur:
|
||||
try:
|
||||
cur.execute(
|
||||
"UPDATE feeds SET activo=TRUE, fallos=0 WHERE id=%s;",
|
||||
(feed_id,),
|
||||
)
|
||||
conn.commit()
|
||||
flash("Feed reactivado.", "success")
|
||||
except Exception as e:
|
||||
flash(f"No se pudo reactivar: {e}", "error")
|
||||
|
||||
return redirect(url_for("feeds.list_feeds"))
|
||||
|
||||
|
||||
@feeds_bp.route("/discover", methods=["GET", "POST"])
|
||||
def discover_feed():
|
||||
"""Descubrir feeds RSS desde una URL"""
|
||||
discovered_feeds = []
|
||||
source_url = ""
|
||||
|
||||
with get_conn() as conn:
|
||||
categorias = get_categorias(conn)
|
||||
paises = get_paises(conn)
|
||||
|
||||
if request.method == "POST":
|
||||
source_url = request.form.get("source_url", "").strip()
|
||||
|
||||
if not source_url:
|
||||
flash("Por favor, ingresa una URL válida.", "error")
|
||||
else:
|
||||
try:
|
||||
# Discover feeds from the URL
|
||||
discovered_feeds = discover_feeds(source_url, timeout=15)
|
||||
|
||||
if not discovered_feeds:
|
||||
flash(f"No se encontraron feeds RSS en la URL: {source_url}", "warning")
|
||||
else:
|
||||
# Check which feeds already exist in DB
|
||||
found_urls = [f['url'] for f in discovered_feeds]
|
||||
existing_urls = set()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT url FROM feeds WHERE url = ANY(%s)", (found_urls,))
|
||||
rows = cur.fetchall()
|
||||
existing_urls = {r[0] for r in rows}
|
||||
except Exception as db_e:
|
||||
# Fallback if DB fails, though unlikely
|
||||
print(f"Error checking existing feeds: {db_e}")
|
||||
|
||||
for feed in discovered_feeds:
|
||||
feed['exists'] = feed['url'] in existing_urls
|
||||
|
||||
new_count = len(discovered_feeds) - len(existing_urls)
|
||||
flash(f"Feeds disponibles: {new_count} de {len(discovered_feeds)} encontrados.", "success")
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Error al descubrir feeds: {e}", "error")
|
||||
|
||||
return render_template(
|
||||
"discover_feeds.html",
|
||||
discovered_feeds=discovered_feeds,
|
||||
source_url=source_url,
|
||||
categorias=categorias,
|
||||
paises=paises
|
||||
)
|
||||
|
||||
|
||||
@feeds_bp.route("/discover_and_add", methods=["POST"])
|
||||
def discover_and_add():
|
||||
"""Añadir múltiples feeds descubiertos"""
|
||||
selected_feeds = request.form.getlist("selected_feeds")
|
||||
categoria_id = request.form.get("categoria_id")
|
||||
pais_id = request.form.get("pais_id")
|
||||
idioma = (request.form.get("idioma") or "").strip().lower()[:2] or None
|
||||
|
||||
if not selected_feeds:
|
||||
flash("No se seleccionó ningún feed.", "warning")
|
||||
return redirect(url_for("feeds.discover_feed"))
|
||||
|
||||
added_count = 0
|
||||
errors = []
|
||||
|
||||
with get_conn() as conn:
|
||||
for feed_url in selected_feeds:
|
||||
try:
|
||||
# Get individual settings for this feed
|
||||
# The form uses the feed URL as part of the field name
|
||||
item_cat_id = request.form.get(f"cat_{feed_url}")
|
||||
item_country_id = request.form.get(f"country_{feed_url}")
|
||||
item_lang = request.form.get(f"lang_{feed_url}")
|
||||
|
||||
# Get feed metadata
|
||||
metadata = get_feed_metadata(feed_url, timeout=10)
|
||||
|
||||
if not metadata:
|
||||
errors.append(f"No se pudo obtener metadata del feed: {feed_url}")
|
||||
continue
|
||||
|
||||
# Use context title from discovery if available, otherwise use metadata title
|
||||
context_title = request.form.get(f"context_{feed_url}")
|
||||
nombre = context_title if context_title else metadata.get('title', 'Feed sin título')
|
||||
|
||||
descripcion = metadata.get('description', '')
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (url) DO NOTHING
|
||||
""",
|
||||
(
|
||||
nombre,
|
||||
descripcion[:500] if descripcion else None,
|
||||
feed_url,
|
||||
int(item_cat_id) if item_cat_id else None,
|
||||
int(item_country_id) if item_country_id else None,
|
||||
(item_lang or "").strip().lower()[:2] or None,
|
||||
),
|
||||
)
|
||||
if cur.rowcount > 0:
|
||||
added_count += 1
|
||||
|
||||
conn.commit()
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Error al añadir {feed_url}: {e}")
|
||||
|
||||
is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||
|
||||
if added_count > 0:
|
||||
msg = f"Se añadieron {added_count} feeds correctamente."
|
||||
if not is_ajax:
|
||||
flash(msg, "success")
|
||||
else:
|
||||
msg = "No se añadieron feeds nuevos."
|
||||
if not is_ajax:
|
||||
# Only flash warning if not ajax, or handle differently
|
||||
if not errors:
|
||||
flash(msg, "warning")
|
||||
|
||||
if errors:
|
||||
for error in errors[:5]: # Mostrar solo los primeros 5 errores
|
||||
if not is_ajax:
|
||||
flash(error, "error")
|
||||
|
||||
if is_ajax:
|
||||
return jsonify({
|
||||
"success": added_count > 0,
|
||||
"added_count": added_count,
|
||||
"message": msg,
|
||||
"errors": errors
|
||||
})
|
||||
|
||||
return redirect(url_for("feeds.list_feeds"))
|
||||
|
||||
|
||||
@feeds_bp.route("/api/validate", methods=["POST"])
|
||||
def api_validate_feed():
|
||||
"""API endpoint para validar una URL de feed"""
|
||||
data = request.get_json()
|
||||
feed_url = data.get("url", "").strip()
|
||||
|
||||
if not feed_url:
|
||||
return jsonify({"error": "URL no proporcionada"}), 400
|
||||
|
||||
try:
|
||||
feed_info = validate_feed(feed_url, timeout=10)
|
||||
|
||||
if not feed_info:
|
||||
return jsonify({"error": "No se pudo validar el feed"}), 400
|
||||
|
||||
return jsonify(feed_info), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@feeds_bp.route("/api/discover", methods=["POST"])
|
||||
def api_discover_feeds():
|
||||
"""API endpoint para descubrir feeds desde una URL"""
|
||||
data = request.get_json()
|
||||
source_url = data.get("url", "").strip()
|
||||
|
||||
if not source_url:
|
||||
return jsonify({"error": "URL no proporcionada"}), 400
|
||||
|
||||
try:
|
||||
discovered = discover_feeds(source_url, timeout=15)
|
||||
return jsonify({"feeds": discovered, "count": len(discovered)}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue