cambios en la web
This commit is contained in:
parent
046a5ff369
commit
a9c1e16bdd
6 changed files with 283 additions and 131 deletions
101
app.py
101
app.py
|
|
@ -11,7 +11,7 @@ from contextlib import contextmanager
|
|||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from tqdm import tqdm
|
||||
|
||||
from flask import Flask, render_template, request, redirect, url_for, Response, flash, make_response
|
||||
from flask import Flask, render_template, request, redirect, url_for, Response, flash, make_response, abort
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
import psycopg2.pool
|
||||
|
|
@ -36,11 +36,7 @@ DB_CONFIG = {
|
|||
MAX_WORKERS = int(os.environ.get("RSS_MAX_WORKERS", 20))
|
||||
SINGLE_FEED_TIMEOUT = int(os.environ.get("RSS_FEED_TIMEOUT", 30))
|
||||
MAX_FALLOS = int(os.environ.get("RSS_MAX_FAILURES", 5))
|
||||
|
||||
# Tamaño de página configurable (límite en 10–100 por seguridad)
|
||||
NEWS_PER_PAGE = int(os.environ.get("NEWS_PER_PAGE", 20))
|
||||
|
||||
# Idioma/traducción por defecto
|
||||
DEFAULT_TRANSLATION_LANG = os.environ.get("DEFAULT_TRANSLATION_LANG", "es").strip().lower()
|
||||
DEFAULT_LANG = os.environ.get("DEFAULT_LANG", DEFAULT_TRANSLATION_LANG).strip().lower()
|
||||
WEB_TRANSLATED_DEFAULT = os.environ.get("WEB_TRANSLATED_DEFAULT", "1").strip().lower() in ("1", "true", "yes")
|
||||
|
|
@ -81,8 +77,8 @@ def safe_html(text):
|
|||
return ""
|
||||
return bleach.clean(
|
||||
text,
|
||||
tags={'a', 'b', 'strong', 'i', 'em', 'p', 'br'},
|
||||
attributes={'a': ['href', 'title']},
|
||||
tags={'a', 'b', 'strong', 'i', 'em', 'p', 'br', 'ul', 'ol', 'li', 'blockquote', 'h3', 'h4'},
|
||||
attributes={'a': ['href', 'title', 'rel', 'target']},
|
||||
strip=True
|
||||
)
|
||||
|
||||
|
|
@ -94,31 +90,18 @@ def _get_form_dependencies(cursor):
|
|||
return categorias, paises
|
||||
|
||||
def _get_lang_and_flags():
|
||||
"""
|
||||
Determina el idioma preferido y si se debe usar traducción por defecto.
|
||||
Permite forzar original con ?orig=1 y cambiar idioma con ?lang=xx (se guarda en cookie).
|
||||
"""
|
||||
qlang = request.args.get("lang", "").strip().lower()
|
||||
cookie_lang = (request.cookies.get("lang") or "").strip().lower()
|
||||
lang = qlang or cookie_lang or DEFAULT_LANG or "es"
|
||||
|
||||
force_orig = request.args.get("orig") == "1"
|
||||
use_translation = (not force_orig) and WEB_TRANSLATED_DEFAULT
|
||||
return lang, use_translation, bool(qlang)
|
||||
|
||||
def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es", use_translation=True):
|
||||
"""
|
||||
Construye la consulta SQL y los parámetros basados en los argumentos de la petición.
|
||||
Si count=True => SELECT COUNT(*)
|
||||
Si count=False => SELECT columnas con ORDER + LIMIT/OFFSET.
|
||||
Integra traducciones vía LEFT JOIN LATERAL cuando use_translation=True (status='done', lang_to=lang).
|
||||
"""
|
||||
# Para controlar orden de parámetros según apariciones de %s:
|
||||
select_rank_params = []
|
||||
from_params = []
|
||||
where_params = []
|
||||
tail_params = []
|
||||
|
||||
conditions = []
|
||||
|
||||
q = args.get("q", "").strip()
|
||||
|
|
@ -127,7 +110,6 @@ def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es",
|
|||
pais_id = args.get("pais_id")
|
||||
fecha_filtro = args.get("fecha")
|
||||
|
||||
# FROM base
|
||||
sql_from = """
|
||||
FROM noticias n
|
||||
LEFT JOIN categorias c ON n.categoria_id = c.id
|
||||
|
|
@ -135,11 +117,10 @@ def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es",
|
|||
LEFT JOIN continentes co ON p.continente_id = co.id
|
||||
"""
|
||||
|
||||
# LEFT JOIN LATERAL traducción (solo en SELECT de página; el conteo no la necesita)
|
||||
if (not count) and use_translation:
|
||||
sql_from += """
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT titulo_trad, resumen_trad
|
||||
SELECT id AS traduccion_id, titulo_trad, resumen_trad
|
||||
FROM traducciones
|
||||
WHERE traducciones.noticia_id = n.id
|
||||
AND traducciones.lang_to = %s
|
||||
|
|
@ -150,9 +131,7 @@ def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es",
|
|||
"""
|
||||
from_params.append(lang)
|
||||
|
||||
# WHERE dinámico
|
||||
if q:
|
||||
# Buscar por relevancia en el tsvector de la noticia original
|
||||
conditions.append("n.tsv @@ plainto_tsquery('spanish', %s)")
|
||||
where_params.append(q)
|
||||
|
||||
|
|
@ -178,24 +157,26 @@ def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es",
|
|||
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
|
||||
|
||||
if count:
|
||||
# Conteo total (sin necesidad de traducciones)
|
||||
sql_count = "SELECT COUNT(*) " + sql_from + where_clause
|
||||
sql_params = from_params + where_params # from_params estará vacío en count
|
||||
sql_params = from_params + where_params
|
||||
return sql_count, sql_params
|
||||
|
||||
# Selección de columnas para página
|
||||
if use_translation:
|
||||
select_cols = """
|
||||
SELECT n.fecha,
|
||||
COALESCE(t.titulo_trad, n.titulo) AS titulo,
|
||||
COALESCE(t.resumen_trad, n.resumen) AS resumen,
|
||||
SELECT
|
||||
COALESCE(t.traduccion_id, NULL) AS traduccion_id,
|
||||
n.fecha,
|
||||
COALESCE(t.titulo_trad, n.titulo) AS titulo,
|
||||
COALESCE(t.resumen_trad, n.resumen) AS resumen,
|
||||
n.url, n.imagen_url, n.fuente_nombre,
|
||||
c.nombre AS categoria, p.nombre AS pais, co.nombre AS continente,
|
||||
(t.titulo_trad IS NOT NULL OR t.resumen_trad IS NOT NULL) AS usa_trad
|
||||
"""
|
||||
else:
|
||||
select_cols = """
|
||||
SELECT n.fecha, n.titulo, n.resumen,
|
||||
SELECT
|
||||
NULL::int AS traduccion_id,
|
||||
n.fecha, n.titulo, n.resumen,
|
||||
n.url, n.imagen_url, n.fuente_nombre,
|
||||
c.nombre AS categoria, p.nombre AS pais, co.nombre AS continente,
|
||||
FALSE AS usa_trad
|
||||
|
|
@ -204,7 +185,6 @@ def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es",
|
|||
order_clause = " ORDER BY n.fecha DESC NULLS LAST"
|
||||
|
||||
if q:
|
||||
# Ranking por relevancia (primer placeholder)
|
||||
select_cols = select_cols.replace(
|
||||
"SELECT",
|
||||
"SELECT ts_rank(n.tsv, plainto_tsquery('spanish', %s)) AS rank,"
|
||||
|
|
@ -212,7 +192,6 @@ def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es",
|
|||
select_rank_params.append(q)
|
||||
order_clause = " ORDER BY rank DESC, n.fecha DESC NULLS LAST"
|
||||
|
||||
# Paginación
|
||||
if limit is not None:
|
||||
order_clause += " LIMIT %s"
|
||||
tail_params.append(limit)
|
||||
|
|
@ -228,20 +207,16 @@ def _build_news_query(args, *, count=False, limit=None, offset=None, lang="es",
|
|||
def home():
|
||||
noticias, categorias, continentes, paises = [], [], [], []
|
||||
|
||||
# Estado de filtros (para mantenerlos en la UI)
|
||||
q = request.args.get("q", "").strip()
|
||||
cat_id = request.args.get("categoria_id")
|
||||
cont_id = request.args.get("continente_id")
|
||||
pais_id = request.args.get("pais_id")
|
||||
fecha_filtro = request.args.get("fecha")
|
||||
|
||||
# Preferencias idioma/uso de traducción
|
||||
lang, use_tr, set_cookie = _get_lang_and_flags()
|
||||
|
||||
# Paginación
|
||||
page = request.args.get("page", default=1, type=int)
|
||||
per_page = request.args.get("per_page", default=NEWS_PER_PAGE, type=int)
|
||||
# límites de seguridad
|
||||
if per_page is None or per_page <= 0:
|
||||
per_page = NEWS_PER_PAGE
|
||||
per_page = 100 if per_page > 100 else (10 if per_page < 10 else per_page)
|
||||
|
|
@ -255,7 +230,6 @@ def home():
|
|||
try:
|
||||
with get_conn() as conn:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
||||
# Dependencias de UI
|
||||
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
|
||||
categorias = cursor.fetchall()
|
||||
cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre")
|
||||
|
|
@ -263,7 +237,6 @@ def home():
|
|||
cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre")
|
||||
paises = cursor.fetchall()
|
||||
|
||||
# 1) Conteo total (no requiere join de traducciones)
|
||||
sql_count, params_count = _build_news_query(
|
||||
request.args, count=True, lang=lang, use_translation=use_tr
|
||||
)
|
||||
|
|
@ -271,7 +244,6 @@ def home():
|
|||
total_results = cursor.fetchone()[0] or 0
|
||||
total_pages = math.ceil(total_results / per_page) if total_results else 0
|
||||
|
||||
# 2) Página actual (con COALESCE a traducción si procede)
|
||||
sql_page, params_page = _build_news_query(
|
||||
request.args,
|
||||
count=False,
|
||||
|
|
@ -295,20 +267,52 @@ def home():
|
|||
lang=lang, use_tr=use_tr
|
||||
)
|
||||
|
||||
# Respuesta parcial para AJAX
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
resp = make_response(render_template('_noticias_list.html', **ctx))
|
||||
if set_cookie:
|
||||
resp.set_cookie("lang", lang, max_age=60*60*24*365)
|
||||
return resp
|
||||
|
||||
# Render completo
|
||||
html = render_template("noticias.html", **ctx)
|
||||
resp = make_response(html)
|
||||
if set_cookie:
|
||||
resp.set_cookie("lang", lang, max_age=60*60*24*365)
|
||||
return resp
|
||||
|
||||
@app.get("/noticia/<int:tr_id>")
|
||||
def noticia(tr_id):
|
||||
with get_conn() as conn, conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
t.id,
|
||||
n.id AS noticia_id,
|
||||
n.fecha,
|
||||
n.titulo AS titulo_original,
|
||||
n.resumen AS cuerpo_original,
|
||||
t.titulo_trad AS titulo_traducido,
|
||||
t.resumen_trad AS cuerpo_traducido,
|
||||
n.url AS fuente_url,
|
||||
n.fuente_nombre,
|
||||
p.nombre AS pais,
|
||||
co.nombre AS continente,
|
||||
c.nombre AS categoria,
|
||||
t.lang_to,
|
||||
t.status
|
||||
FROM traducciones t
|
||||
JOIN noticias n ON n.id = t.noticia_id
|
||||
LEFT JOIN paises p ON n.pais_id = p.id
|
||||
LEFT JOIN continentes co ON p.continente_id = co.id
|
||||
LEFT JOIN categorias c ON n.categoria_id = c.id
|
||||
WHERE t.id = %s
|
||||
""",
|
||||
(tr_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
abort(404)
|
||||
return render_template("noticia.html", r=row)
|
||||
|
||||
@app.route("/dashboard")
|
||||
def dashboard():
|
||||
stats = {'feeds_totales': 0, 'noticias_totales': 0, 'feeds_caidos': 0}
|
||||
|
|
@ -540,8 +544,6 @@ def fetch_and_store_all():
|
|||
feeds_fallidos = []
|
||||
feeds_exitosos = []
|
||||
feeds_para_actualizar_headers = []
|
||||
|
||||
# --- Parte 1: Procesando Feeds RSS ---
|
||||
logging.info("=> Parte 1: Procesando Feeds RSS...")
|
||||
feeds_to_process = []
|
||||
try:
|
||||
|
|
@ -578,7 +580,6 @@ def fetch_and_store_all():
|
|||
noticias_desde_rss_count = len(todas_las_noticias)
|
||||
logging.info(f"=> Parte 1 Finalizada. Noticias desde RSS: {noticias_desde_rss_count}. Éxitos: {len(feeds_exitosos)}. Fallos: {len(feeds_fallidos)}.")
|
||||
|
||||
# --- Parte 2: Procesando Fuentes URL ---
|
||||
logging.info("=> Parte 2: Procesando Fuentes URL...")
|
||||
urls_to_process = []
|
||||
try:
|
||||
|
|
@ -590,7 +591,6 @@ def fetch_and_store_all():
|
|||
except Exception as e:
|
||||
logging.error(f"Error de BD al obtener fuentes URL: {e}")
|
||||
|
||||
# Paraleliza la captura desde newspaper3k
|
||||
if urls_to_process:
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
future_to_url = {
|
||||
|
|
@ -612,7 +612,6 @@ def fetch_and_store_all():
|
|||
noticias_desde_urls_count = len(todas_las_noticias) - noticias_desde_rss_count
|
||||
logging.info(f"=> Parte 2 Finalizada. Noticias encontradas desde URLs: {noticias_desde_urls_count}.")
|
||||
|
||||
# --- Parte 3: Actualizando la base de datos ---
|
||||
logging.info("=> Parte 3: Actualizando la base de datos...")
|
||||
if not any([todas_las_noticias, feeds_fallidos, feeds_exitosos, feeds_para_actualizar_headers]):
|
||||
logging.info("No se encontraron nuevas noticias ni cambios en los feeds. Nada que actualizar.")
|
||||
|
|
@ -655,8 +654,6 @@ def fetch_and_store_all():
|
|||
|
||||
logging.info("--- CICLO DE CAPTURA GLOBAL FINALIZADO ---")
|
||||
|
||||
# --- Funciones de Backup y Restore (sin cambios) ---
|
||||
|
||||
@app.route("/backup_feeds")
|
||||
def backup_feeds():
|
||||
try:
|
||||
|
|
@ -755,7 +752,6 @@ def backup_completo():
|
|||
with zipfile.ZipFile(memory_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
with get_conn() as conn:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
||||
# Backup Feeds
|
||||
cursor.execute("SELECT * FROM feeds ORDER BY id")
|
||||
feeds_data = cursor.fetchall()
|
||||
if feeds_data:
|
||||
|
|
@ -765,7 +761,6 @@ def backup_completo():
|
|||
writer_feeds.writerows([dict(f) for f in feeds_data])
|
||||
zipf.writestr("feeds.csv", output_feeds.getvalue())
|
||||
|
||||
# Backup Fuentes URL
|
||||
cursor.execute("SELECT * FROM fuentes_url ORDER BY id")
|
||||
fuentes_data = cursor.fetchall()
|
||||
if fuentes_data:
|
||||
|
|
@ -775,7 +770,6 @@ def backup_completo():
|
|||
writer_fuentes.writerows([dict(f) for f in fuentes_data])
|
||||
zipf.writestr("fuentes_url.csv", output_fuentes.getvalue())
|
||||
|
||||
# Backup Noticias
|
||||
cursor.execute("SELECT * FROM noticias ORDER BY fecha DESC")
|
||||
noticias_data = cursor.fetchall()
|
||||
if noticias_data:
|
||||
|
|
@ -892,7 +886,6 @@ def restore_urls():
|
|||
|
||||
return render_template("restore_urls.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not db_pool:
|
||||
app.logger.error("La aplicación no puede arrancar sin una conexión a la base de datos.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue