update:español por defecto
This commit is contained in:
parent
da4c59a0e1
commit
046a5ff369
6 changed files with 381 additions and 119 deletions
136
app.py
136
app.py
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import csv
|
||||
import math
|
||||
from io import StringIO, BytesIO
|
||||
|
|
@ -12,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
|
||||
from flask import Flask, render_template, request, redirect, url_for, Response, flash, make_response
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
import psycopg2.pool
|
||||
|
|
@ -41,6 +40,11 @@ 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")
|
||||
|
||||
db_pool = None
|
||||
try:
|
||||
db_pool = psycopg2.pool.SimpleConnectionPool(minconn=1, maxconn=10, **DB_CONFIG)
|
||||
|
|
@ -75,7 +79,12 @@ def shutdown_hooks():
|
|||
def safe_html(text):
|
||||
if not text:
|
||||
return ""
|
||||
return bleach.clean(text, tags={'a', 'b', 'strong', 'i', 'em', 'p', 'br'}, attributes={'a': ['href', 'title']}, strip=True)
|
||||
return bleach.clean(
|
||||
text,
|
||||
tags={'a', 'b', 'strong', 'i', 'em', 'p', 'br'},
|
||||
attributes={'a': ['href', 'title']},
|
||||
strip=True
|
||||
)
|
||||
|
||||
def _get_form_dependencies(cursor):
|
||||
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
|
||||
|
|
@ -84,13 +93,32 @@ def _get_form_dependencies(cursor):
|
|||
paises = cursor.fetchall()
|
||||
return categorias, paises
|
||||
|
||||
def _build_news_query(args, *, count=False, limit=None, offset=None):
|
||||
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
|
||||
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).
|
||||
"""
|
||||
sql_params = []
|
||||
# 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()
|
||||
|
|
@ -99,6 +127,7 @@ def _build_news_query(args, *, count=False, limit=None, offset=None):
|
|||
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
|
||||
|
|
@ -106,64 +135,93 @@ def _build_news_query(args, *, count=False, limit=None, offset=None):
|
|||
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
|
||||
FROM traducciones
|
||||
WHERE traducciones.noticia_id = n.id
|
||||
AND traducciones.lang_to = %s
|
||||
AND traducciones.status = 'done'
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
) t ON TRUE
|
||||
"""
|
||||
from_params.append(lang)
|
||||
|
||||
# WHERE dinámico
|
||||
if q:
|
||||
# plainto_tsquery para tolerar espacios/acentos
|
||||
# Buscar por relevancia en el tsvector de la noticia original
|
||||
conditions.append("n.tsv @@ plainto_tsquery('spanish', %s)")
|
||||
sql_params.append(q)
|
||||
where_params.append(q)
|
||||
|
||||
if cat_id:
|
||||
conditions.append("n.categoria_id = %s")
|
||||
sql_params.append(cat_id)
|
||||
where_params.append(cat_id)
|
||||
|
||||
if pais_id:
|
||||
conditions.append("n.pais_id = %s")
|
||||
sql_params.append(pais_id)
|
||||
where_params.append(pais_id)
|
||||
elif cont_id:
|
||||
conditions.append("p.continente_id = %s")
|
||||
sql_params.append(cont_id)
|
||||
where_params.append(cont_id)
|
||||
|
||||
if fecha_filtro:
|
||||
try:
|
||||
fecha_obj = datetime.strptime(fecha_filtro, '%Y-%m-%d')
|
||||
conditions.append("n.fecha::date = %s")
|
||||
sql_params.append(fecha_obj.date())
|
||||
where_params.append(fecha_obj.date())
|
||||
except ValueError:
|
||||
flash("Formato de fecha no válido. Use AAAA-MM-DD.", "error")
|
||||
|
||||
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
|
||||
|
||||
if count:
|
||||
# Conteo total
|
||||
# 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
|
||||
return sql_count, sql_params
|
||||
|
||||
# Selección de columnas para página
|
||||
select_cols = """
|
||||
SELECT 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
|
||||
"""
|
||||
if use_translation:
|
||||
select_cols = """
|
||||
SELECT 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,
|
||||
n.url, n.imagen_url, n.fuente_nombre,
|
||||
c.nombre AS categoria, p.nombre AS pais, co.nombre AS continente,
|
||||
FALSE AS usa_trad
|
||||
"""
|
||||
|
||||
order_clause = " ORDER BY n.fecha DESC NULLS LAST"
|
||||
|
||||
if q:
|
||||
# Ranking por relevancia cuando hay búsqueda
|
||||
# Ranking por relevancia (primer placeholder)
|
||||
select_cols = select_cols.replace(
|
||||
"SELECT",
|
||||
"SELECT ts_rank(n.tsv, plainto_tsquery('spanish', %s)) AS rank,"
|
||||
)
|
||||
# El %s de rank va al principio de los params de SELECT
|
||||
sql_params = [q] + sql_params
|
||||
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"
|
||||
sql_params.append(limit)
|
||||
tail_params.append(limit)
|
||||
if offset is not None:
|
||||
order_clause += " OFFSET %s"
|
||||
sql_params.append(offset)
|
||||
tail_params.append(offset)
|
||||
|
||||
sql_page = select_cols + sql_from + where_clause + order_clause
|
||||
sql_params = select_rank_params + from_params + where_params + tail_params
|
||||
return sql_page, sql_params
|
||||
|
||||
@app.route("/")
|
||||
|
|
@ -177,6 +235,9 @@ def home():
|
|||
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)
|
||||
|
|
@ -202,14 +263,23 @@ def home():
|
|||
cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre")
|
||||
paises = cursor.fetchall()
|
||||
|
||||
# 1) Conteo total
|
||||
sql_count, params_count = _build_news_query(request.args, count=True)
|
||||
# 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
|
||||
)
|
||||
cursor.execute(sql_count, tuple(params_count))
|
||||
total_results = cursor.fetchone()[0] or 0
|
||||
total_pages = math.ceil(total_results / per_page) if total_results else 0
|
||||
|
||||
# 2) Página actual
|
||||
sql_page, params_page = _build_news_query(request.args, count=False, limit=per_page, offset=offset)
|
||||
# 2) Página actual (con COALESCE a traducción si procede)
|
||||
sql_page, params_page = _build_news_query(
|
||||
request.args,
|
||||
count=False,
|
||||
limit=per_page,
|
||||
offset=offset,
|
||||
lang=lang,
|
||||
use_translation=use_tr
|
||||
)
|
||||
cursor.execute(sql_page, tuple(params_page))
|
||||
noticias = cursor.fetchall()
|
||||
|
||||
|
|
@ -221,15 +291,23 @@ def home():
|
|||
noticias=noticias, categorias=categorias, continentes=continentes, paises=paises,
|
||||
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, fecha_filtro=fecha_filtro, q=q,
|
||||
page=page, per_page=per_page, total_pages=total_pages, total_results=total_results
|
||||
page=page, per_page=per_page, total_pages=total_pages, total_results=total_results,
|
||||
lang=lang, use_tr=use_tr
|
||||
)
|
||||
|
||||
# Respuesta parcial para AJAX
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return render_template('_noticias_list.html', **ctx)
|
||||
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
|
||||
return render_template("noticias.html", **ctx)
|
||||
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.route("/dashboard")
|
||||
def dashboard():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue