Initial clean commit
This commit is contained in:
commit
6784d81c2c
141 changed files with 25219 additions and 0 deletions
353
routers/backup.py
Normal file
353
routers/backup.py
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
from flask import Blueprint, send_file, render_template, request, flash, redirect, url_for
|
||||
import csv
|
||||
import io
|
||||
from psycopg2 import extras
|
||||
from db import get_conn
|
||||
|
||||
backup_bp = Blueprint("backup", __name__)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# EXPORTAR FEEDS → CSV (OK)
|
||||
# ============================================================
|
||||
|
||||
@backup_bp.route("/backup_feeds")
|
||||
def backup_feeds():
|
||||
with get_conn() as conn, conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT f.id, f.nombre, f.descripcion, f.url,
|
||||
f.categoria_id, c.nombre AS categoria,
|
||||
f.pais_id, p.nombre AS pais,
|
||||
f.idioma, f.activo, f.fallos
|
||||
FROM feeds f
|
||||
LEFT JOIN categorias c ON c.id=f.categoria_id
|
||||
LEFT JOIN paises p ON p.id=f.pais_id
|
||||
ORDER BY f.id;
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
writer.writerow([
|
||||
"id", "nombre", "descripcion", "url",
|
||||
"categoria_id", "categoria",
|
||||
"pais_id", "pais",
|
||||
"idioma", "activo", "fallos"
|
||||
])
|
||||
|
||||
for r in rows:
|
||||
writer.writerow([
|
||||
r["id"],
|
||||
r["nombre"],
|
||||
r["descripcion"] or "",
|
||||
r["url"],
|
||||
r["categoria_id"] or "",
|
||||
r["categoria"] or "",
|
||||
r["pais_id"] or "",
|
||||
r["pais"] or "",
|
||||
r["idioma"] or "",
|
||||
r["activo"],
|
||||
r["fallos"],
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode("utf-8")),
|
||||
mimetype="text/csv",
|
||||
as_attachment=True,
|
||||
download_name="feeds_backup.csv",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# EXPORTAR FEEDS FILTRADOS → CSV
|
||||
# ============================================================
|
||||
|
||||
@backup_bp.route("/export_feeds_filtered")
|
||||
def export_feeds_filtered():
|
||||
"""Exportar feeds con filtros opcionales (país, categoría, estado)."""
|
||||
pais_id = request.args.get("pais_id")
|
||||
categoria_id = request.args.get("categoria_id")
|
||||
estado = request.args.get("estado") or ""
|
||||
|
||||
# Construir filtros WHERE (misma lógica que list_feeds)
|
||||
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 ""
|
||||
|
||||
# Query SQL con filtros
|
||||
with get_conn() as conn, conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute(f"""
|
||||
SELECT f.id, f.nombre, f.descripcion, f.url,
|
||||
f.categoria_id, c.nombre AS categoria,
|
||||
f.pais_id, p.nombre AS pais,
|
||||
f.idioma, f.activo, f.fallos
|
||||
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, c.nombre NULLS LAST, f.nombre;
|
||||
""", params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
# Obtener nombres para el archivo
|
||||
pais_nombre = None
|
||||
categoria_nombre = None
|
||||
|
||||
if pais_id:
|
||||
cur.execute("SELECT nombre FROM paises WHERE id = %s", (int(pais_id),))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
pais_nombre = result["nombre"]
|
||||
|
||||
if categoria_id:
|
||||
cur.execute("SELECT nombre FROM categorias WHERE id = %s", (int(categoria_id),))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
categoria_nombre = result["nombre"]
|
||||
|
||||
# Generar CSV
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
writer.writerow([
|
||||
"id", "nombre", "descripcion", "url",
|
||||
"categoria_id", "categoria",
|
||||
"pais_id", "pais",
|
||||
"idioma", "activo", "fallos"
|
||||
])
|
||||
|
||||
for r in rows:
|
||||
writer.writerow([
|
||||
r["id"],
|
||||
r["nombre"],
|
||||
r["descripcion"] or "",
|
||||
r["url"],
|
||||
r["categoria_id"] or "",
|
||||
r["categoria"] or "",
|
||||
r["pais_id"] or "",
|
||||
r["pais"] or "",
|
||||
r["idioma"] or "",
|
||||
r["activo"],
|
||||
r["fallos"],
|
||||
])
|
||||
|
||||
# Generar nombre de archivo dinámico
|
||||
filename_parts = ["feeds"]
|
||||
|
||||
if pais_nombre:
|
||||
# Limpiar nombre de país para usar en archivo
|
||||
clean_pais = pais_nombre.lower().replace(" ", "_").replace("/", "_")
|
||||
filename_parts.append(clean_pais)
|
||||
|
||||
if categoria_nombre:
|
||||
clean_cat = categoria_nombre.lower().replace(" ", "_").replace("/", "_")
|
||||
filename_parts.append(clean_cat)
|
||||
|
||||
if estado:
|
||||
filename_parts.append(estado)
|
||||
|
||||
filename = "_".join(filename_parts) + ".csv"
|
||||
|
||||
output.seek(0)
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode("utf-8")),
|
||||
mimetype="text/csv",
|
||||
as_attachment=True,
|
||||
download_name=filename,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# RESTAURAR FEEDS → CSV (VERSIÓN PROFESIONAL)
|
||||
# ============================================================
|
||||
|
||||
@backup_bp.route("/restore_feeds", methods=["GET", "POST"])
|
||||
def restore_feeds():
|
||||
if request.method == "GET":
|
||||
return render_template("restore_feeds.html")
|
||||
|
||||
file = request.files.get("file")
|
||||
if not file:
|
||||
flash("Debes seleccionar un archivo CSV.", "error")
|
||||
return redirect(url_for("backup.restore_feeds"))
|
||||
|
||||
# 1) Leer CSV
|
||||
try:
|
||||
raw = file.read().decode("utf-8-sig").replace("\ufeff", "")
|
||||
reader = csv.DictReader(io.StringIO(raw))
|
||||
except Exception as e:
|
||||
flash(f"Error al procesar CSV: {e}", "error")
|
||||
return redirect(url_for("backup.restore_feeds"))
|
||||
|
||||
expected_fields = [
|
||||
"id", "nombre", "descripcion", "url",
|
||||
"categoria_id", "categoria",
|
||||
"pais_id", "pais",
|
||||
"idioma", "activo", "fallos"
|
||||
]
|
||||
|
||||
if reader.fieldnames != expected_fields:
|
||||
flash("El CSV no tiene el encabezado correcto.", "error")
|
||||
return redirect(url_for("backup.restore_feeds"))
|
||||
|
||||
# Contadores
|
||||
imported = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
|
||||
# Vaciar tabla ELIMINADO para no borrar feeds existentes
|
||||
# cur.execute("TRUNCATE feeds RESTART IDENTITY CASCADE;")
|
||||
|
||||
for row in reader:
|
||||
# Limpieza general
|
||||
row = {k: (v.strip().rstrip("ç") if isinstance(v, str) else v) for k, v in row.items()}
|
||||
|
||||
# Validaciones mínimas
|
||||
if not row["url"] or not row["nombre"]:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
# Creating a savepoint to isolate this row's transaction
|
||||
cur.execute("SAVEPOINT row_savepoint")
|
||||
|
||||
# Normalizar valores
|
||||
categoria_id = int(row["categoria_id"]) if row["categoria_id"] else None
|
||||
pais_id = int(row["pais_id"]) if row["pais_id"] else None
|
||||
|
||||
idioma = (row["idioma"] or "").lower().strip()
|
||||
idioma = idioma[:2] if idioma else None
|
||||
|
||||
activo = str(row["activo"]).lower() in ("true", "1", "t", "yes", "y")
|
||||
fallos = int(row["fallos"] or 0)
|
||||
|
||||
# Buscar si ya existe un feed con esta URL
|
||||
cur.execute("SELECT id FROM feeds WHERE url = %s", (row["url"],))
|
||||
existing_feed = cur.fetchone()
|
||||
|
||||
if existing_feed:
|
||||
# URL ya existe -> ACTUALIZAR el feed existente
|
||||
cur.execute("""
|
||||
UPDATE feeds SET
|
||||
nombre=%s,
|
||||
descripcion=%s,
|
||||
categoria_id=%s,
|
||||
pais_id=%s,
|
||||
idioma=%s,
|
||||
activo=%s,
|
||||
fallos=%s
|
||||
WHERE id=%s
|
||||
""", (
|
||||
row["nombre"],
|
||||
row["descripcion"] or None,
|
||||
categoria_id,
|
||||
pais_id,
|
||||
idioma,
|
||||
activo,
|
||||
fallos,
|
||||
existing_feed[0]
|
||||
))
|
||||
else:
|
||||
# URL no existe -> INSERTAR NUEVO feed (ignorar ID del CSV, usar auto-increment)
|
||||
cur.execute("""
|
||||
INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma, activo, fallos)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
""", (
|
||||
row["nombre"],
|
||||
row["descripcion"] or None,
|
||||
row["url"],
|
||||
categoria_id,
|
||||
pais_id,
|
||||
idioma,
|
||||
activo,
|
||||
fallos
|
||||
))
|
||||
|
||||
cur.execute("RELEASE SAVEPOINT row_savepoint")
|
||||
imported += 1
|
||||
|
||||
except Exception as e:
|
||||
# If any error happens, rollback to the savepoint so the main transaction isn't aborted
|
||||
cur.execute("ROLLBACK TO SAVEPOINT row_savepoint")
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
# No need to reset sequence - auto-increment handles it
|
||||
conn.commit()
|
||||
|
||||
flash(
|
||||
f"Restauración completada. "
|
||||
f"Importados: {imported} | Saltados: {skipped} | Fallidos: {failed}",
|
||||
"success"
|
||||
)
|
||||
|
||||
return redirect(url_for("feeds.list_feeds"))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# EXPORTAR METADATOS (PAISES / CATEGORIAS)
|
||||
# ============================================================
|
||||
|
||||
@backup_bp.route("/export_paises")
|
||||
def export_paises():
|
||||
"""Exportar listado de países a CSV."""
|
||||
with get_conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("SELECT id, nombre FROM paises ORDER BY id;")
|
||||
rows = cur.fetchall()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["id", "nombre"])
|
||||
for r in rows:
|
||||
writer.writerow([r[0], r[1]])
|
||||
|
||||
output.seek(0)
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode("utf-8")),
|
||||
mimetype="text/csv",
|
||||
as_attachment=True,
|
||||
download_name="paises.csv",
|
||||
)
|
||||
|
||||
|
||||
@backup_bp.route("/export_categorias")
|
||||
def export_categorias():
|
||||
"""Exportar listado de categorías a CSV."""
|
||||
with get_conn() as conn, conn.cursor() as cur:
|
||||
cur.execute("SELECT id, nombre FROM categorias ORDER BY id;")
|
||||
rows = cur.fetchall()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["id", "nombre"])
|
||||
for r in rows:
|
||||
writer.writerow([r[0], r[1]])
|
||||
|
||||
output.seek(0)
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode("utf-8")),
|
||||
mimetype="text/csv",
|
||||
as_attachment=True,
|
||||
download_name="categorias.csv",
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue