update:español por defecto
This commit is contained in:
parent
da4c59a0e1
commit
046a5ff369
6 changed files with 381 additions and 119 deletions
58
.env
58
.env
|
|
@ -1,7 +1,59 @@
|
||||||
# Variables para la base de datos
|
# =========================
|
||||||
|
# Base de datos
|
||||||
|
# =========================
|
||||||
DB_NAME=rss
|
DB_NAME=rss
|
||||||
DB_USER=rss
|
DB_USER=rss
|
||||||
DB_PASS=lalalilo
|
DB_PASS=lalalilo
|
||||||
|
# DB_HOST y DB_PORT los inyecta docker-compose (DB_HOST=db).
|
||||||
|
# Si ejecutas la app fuera de Docker, puedes descomentar:
|
||||||
|
# DB_HOST=localhost
|
||||||
|
# DB_PORT=5432
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Flask / Web
|
||||||
|
# =========================
|
||||||
|
# ¡Pon aquí una clave larga y aleatoria!
|
||||||
|
SECRET_KEY=CAMBIA_ESTA_CLAVE_POR_ALGO_LARGO_Y_ALEATORIO
|
||||||
|
|
||||||
|
# Idioma por defecto de la web y traducción activada por defecto
|
||||||
|
DEFAULT_LANG=es
|
||||||
|
DEFAULT_TRANSLATION_LANG=es
|
||||||
|
WEB_TRANSLATED_DEFAULT=1
|
||||||
|
|
||||||
|
# Paginación por defecto (app.py limita entre 10 y 100)
|
||||||
|
NEWS_PER_PAGE=20
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Ingesta / Scheduler
|
||||||
|
# =========================
|
||||||
|
RSS_MAX_WORKERS=20
|
||||||
|
RSS_FEED_TIMEOUT=30
|
||||||
|
RSS_MAX_FAILURES=5
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Worker de traducción (NLLB 1.3B)
|
||||||
|
# =========================
|
||||||
|
TARGET_LANGS=es
|
||||||
|
TRANSLATOR_BATCH=4
|
||||||
|
ENQUEUE=200
|
||||||
|
TRANSLATOR_SLEEP_IDLE=5
|
||||||
|
|
||||||
|
# Límites de tokens (equilibrio calidad/VRAM para 12 GB)
|
||||||
|
MAX_SRC_TOKENS=512
|
||||||
|
MAX_NEW_TOKENS=256
|
||||||
|
|
||||||
|
# Beams (más calidad en títulos)
|
||||||
|
NUM_BEAMS_TITLE=3
|
||||||
|
NUM_BEAMS_BODY=2
|
||||||
|
|
||||||
|
# Modelo y dispositivo
|
||||||
|
UNIVERSAL_MODEL=facebook/nllb-200-1.3B
|
||||||
|
DEVICE=cuda
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Runtime (estabilidad/VRAM)
|
||||||
|
# =========================
|
||||||
|
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,max_split_size_mb:64,garbage_collection_threshold:0.9
|
||||||
|
TOKENIZERS_PARALLELISM=false
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Variable para Flask
|
|
||||||
SECRET_KEY=genera_una_clave_aleatoria_larga_aqui
|
|
||||||
|
|
|
||||||
132
app.py
132
app.py
|
|
@ -1,6 +1,5 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import hashlib
|
|
||||||
import csv
|
import csv
|
||||||
import math
|
import math
|
||||||
from io import StringIO, BytesIO
|
from io import StringIO, BytesIO
|
||||||
|
|
@ -12,7 +11,7 @@ from contextlib import contextmanager
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from tqdm import tqdm
|
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
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
import psycopg2.pool
|
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)
|
# Tamaño de página configurable (límite en 10–100 por seguridad)
|
||||||
NEWS_PER_PAGE = int(os.environ.get("NEWS_PER_PAGE", 20))
|
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
|
db_pool = None
|
||||||
try:
|
try:
|
||||||
db_pool = psycopg2.pool.SimpleConnectionPool(minconn=1, maxconn=10, **DB_CONFIG)
|
db_pool = psycopg2.pool.SimpleConnectionPool(minconn=1, maxconn=10, **DB_CONFIG)
|
||||||
|
|
@ -75,7 +79,12 @@ def shutdown_hooks():
|
||||||
def safe_html(text):
|
def safe_html(text):
|
||||||
if not text:
|
if not text:
|
||||||
return ""
|
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):
|
def _get_form_dependencies(cursor):
|
||||||
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
|
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
|
||||||
|
|
@ -84,13 +93,32 @@ def _get_form_dependencies(cursor):
|
||||||
paises = cursor.fetchall()
|
paises = cursor.fetchall()
|
||||||
return categorias, paises
|
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.
|
Construye la consulta SQL y los parámetros basados en los argumentos de la petición.
|
||||||
Si count=True => SELECT COUNT(*)
|
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 = []
|
conditions = []
|
||||||
|
|
||||||
q = args.get("q", "").strip()
|
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")
|
pais_id = args.get("pais_id")
|
||||||
fecha_filtro = args.get("fecha")
|
fecha_filtro = args.get("fecha")
|
||||||
|
|
||||||
|
# FROM base
|
||||||
sql_from = """
|
sql_from = """
|
||||||
FROM noticias n
|
FROM noticias n
|
||||||
LEFT JOIN categorias c ON n.categoria_id = c.id
|
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 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
|
# WHERE dinámico
|
||||||
if q:
|
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)")
|
conditions.append("n.tsv @@ plainto_tsquery('spanish', %s)")
|
||||||
sql_params.append(q)
|
where_params.append(q)
|
||||||
|
|
||||||
if cat_id:
|
if cat_id:
|
||||||
conditions.append("n.categoria_id = %s")
|
conditions.append("n.categoria_id = %s")
|
||||||
sql_params.append(cat_id)
|
where_params.append(cat_id)
|
||||||
|
|
||||||
if pais_id:
|
if pais_id:
|
||||||
conditions.append("n.pais_id = %s")
|
conditions.append("n.pais_id = %s")
|
||||||
sql_params.append(pais_id)
|
where_params.append(pais_id)
|
||||||
elif cont_id:
|
elif cont_id:
|
||||||
conditions.append("p.continente_id = %s")
|
conditions.append("p.continente_id = %s")
|
||||||
sql_params.append(cont_id)
|
where_params.append(cont_id)
|
||||||
|
|
||||||
if fecha_filtro:
|
if fecha_filtro:
|
||||||
try:
|
try:
|
||||||
fecha_obj = datetime.strptime(fecha_filtro, '%Y-%m-%d')
|
fecha_obj = datetime.strptime(fecha_filtro, '%Y-%m-%d')
|
||||||
conditions.append("n.fecha::date = %s")
|
conditions.append("n.fecha::date = %s")
|
||||||
sql_params.append(fecha_obj.date())
|
where_params.append(fecha_obj.date())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash("Formato de fecha no válido. Use AAAA-MM-DD.", "error")
|
flash("Formato de fecha no válido. Use AAAA-MM-DD.", "error")
|
||||||
|
|
||||||
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
|
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
|
||||||
|
|
||||||
if count:
|
if count:
|
||||||
# Conteo total
|
# Conteo total (sin necesidad de traducciones)
|
||||||
sql_count = "SELECT COUNT(*) " + sql_from + where_clause
|
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
|
return sql_count, sql_params
|
||||||
|
|
||||||
# Selección de columnas para página
|
# Selección de columnas para página
|
||||||
|
if use_translation:
|
||||||
select_cols = """
|
select_cols = """
|
||||||
SELECT n.fecha, n.titulo, n.resumen, n.url, n.imagen_url, n.fuente_nombre,
|
SELECT n.fecha,
|
||||||
c.nombre AS categoria, p.nombre AS pais, co.nombre AS continente
|
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"
|
order_clause = " ORDER BY n.fecha DESC NULLS LAST"
|
||||||
|
|
||||||
if q:
|
if q:
|
||||||
# Ranking por relevancia cuando hay búsqueda
|
# Ranking por relevancia (primer placeholder)
|
||||||
select_cols = select_cols.replace(
|
select_cols = select_cols.replace(
|
||||||
"SELECT",
|
"SELECT",
|
||||||
"SELECT ts_rank(n.tsv, plainto_tsquery('spanish', %s)) AS rank,"
|
"SELECT ts_rank(n.tsv, plainto_tsquery('spanish', %s)) AS rank,"
|
||||||
)
|
)
|
||||||
# El %s de rank va al principio de los params de SELECT
|
select_rank_params.append(q)
|
||||||
sql_params = [q] + sql_params
|
|
||||||
order_clause = " ORDER BY rank DESC, n.fecha DESC NULLS LAST"
|
order_clause = " ORDER BY rank DESC, n.fecha DESC NULLS LAST"
|
||||||
|
|
||||||
# Paginación
|
# Paginación
|
||||||
if limit is not None:
|
if limit is not None:
|
||||||
order_clause += " LIMIT %s"
|
order_clause += " LIMIT %s"
|
||||||
sql_params.append(limit)
|
tail_params.append(limit)
|
||||||
if offset is not None:
|
if offset is not None:
|
||||||
order_clause += " OFFSET %s"
|
order_clause += " OFFSET %s"
|
||||||
sql_params.append(offset)
|
tail_params.append(offset)
|
||||||
|
|
||||||
sql_page = select_cols + sql_from + where_clause + order_clause
|
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
|
return sql_page, sql_params
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
@ -177,6 +235,9 @@ def home():
|
||||||
pais_id = request.args.get("pais_id")
|
pais_id = request.args.get("pais_id")
|
||||||
fecha_filtro = request.args.get("fecha")
|
fecha_filtro = request.args.get("fecha")
|
||||||
|
|
||||||
|
# Preferencias idioma/uso de traducción
|
||||||
|
lang, use_tr, set_cookie = _get_lang_and_flags()
|
||||||
|
|
||||||
# Paginación
|
# Paginación
|
||||||
page = request.args.get("page", default=1, type=int)
|
page = request.args.get("page", default=1, type=int)
|
||||||
per_page = request.args.get("per_page", default=NEWS_PER_PAGE, 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")
|
cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre")
|
||||||
paises = cursor.fetchall()
|
paises = cursor.fetchall()
|
||||||
|
|
||||||
# 1) Conteo total
|
# 1) Conteo total (no requiere join de traducciones)
|
||||||
sql_count, params_count = _build_news_query(request.args, count=True)
|
sql_count, params_count = _build_news_query(
|
||||||
|
request.args, count=True, lang=lang, use_translation=use_tr
|
||||||
|
)
|
||||||
cursor.execute(sql_count, tuple(params_count))
|
cursor.execute(sql_count, tuple(params_count))
|
||||||
total_results = cursor.fetchone()[0] or 0
|
total_results = cursor.fetchone()[0] or 0
|
||||||
total_pages = math.ceil(total_results / per_page) if total_results else 0
|
total_pages = math.ceil(total_results / per_page) if total_results else 0
|
||||||
|
|
||||||
# 2) Página actual
|
# 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)
|
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))
|
cursor.execute(sql_page, tuple(params_page))
|
||||||
noticias = cursor.fetchall()
|
noticias = cursor.fetchall()
|
||||||
|
|
||||||
|
|
@ -221,15 +291,23 @@ def home():
|
||||||
noticias=noticias, categorias=categorias, continentes=continentes, paises=paises,
|
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,
|
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,
|
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
|
# Respuesta parcial para AJAX
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
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
|
# 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")
|
@app.route("/dashboard")
|
||||||
def dashboard():
|
def dashboard():
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,12 @@ services:
|
||||||
- DB_USER=${DB_USER}
|
- DB_USER=${DB_USER}
|
||||||
- DB_PASS=${DB_PASS}
|
- DB_PASS=${DB_PASS}
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
# - NEWS_PER_PAGE=20 # opcional
|
# Opcionales UI
|
||||||
|
# - NEWS_PER_PAGE=20
|
||||||
|
# Mostrar traducciones por defecto en la web
|
||||||
|
- WEB_TRANSLATED_DEFAULT=1
|
||||||
|
- DEFAULT_LANG=es
|
||||||
|
- TRANSLATION_PREFERRED_LANGS=es
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -75,11 +80,11 @@ services:
|
||||||
|
|
||||||
# --- Worker ---
|
# --- Worker ---
|
||||||
- TARGET_LANGS=es
|
- TARGET_LANGS=es
|
||||||
- TRANSLATOR_BATCH=4 # 1.3B: más seguro en 12 GB (sube a 4 si ves VRAM libre)
|
- TRANSLATOR_BATCH=4 # estable con 1.3B en 12 GB; ajusta si cambia la VRAM disponible
|
||||||
- ENQUEUE=200
|
- ENQUEUE=200
|
||||||
- TRANSLATOR_SLEEP_IDLE=5
|
- TRANSLATOR_SLEEP_IDLE=5
|
||||||
|
|
||||||
# Tokens (equilibrio calidad/VRAM)
|
# Tokens (equilibrio calidad/VRAM ~<7GB)
|
||||||
- MAX_SRC_TOKENS=512
|
- MAX_SRC_TOKENS=512
|
||||||
- MAX_NEW_TOKENS=256
|
- MAX_NEW_TOKENS=256
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,27 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="noticia-texto">
|
<div class="noticia-texto">
|
||||||
<h3><a href="{{ noticia.url }}" target="_blank" rel="noopener noreferrer">{{ noticia.titulo }}</a></h3>
|
<h3 style="margin:0 0 6px 0;">
|
||||||
|
<a href="{{ noticia.url }}" target="_blank" rel="noopener noreferrer">{{ noticia.titulo }}</a>
|
||||||
|
{% if use_tr %}
|
||||||
|
<span class="badge" title="Mostrando traducciones por defecto" style="margin-left:8px;">Traducido</span>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div class="noticia-meta">
|
<div class="noticia-meta">
|
||||||
<span><i class="far fa-calendar-alt"></i>
|
<span>
|
||||||
{{ noticia.fecha.strftime('%d-%m-%Y %H:%M') if noticia.fecha else 'N/D' }}
|
<i class="far fa-calendar-alt"></i>
|
||||||
|
{% if noticia.fecha %}
|
||||||
|
{% if noticia.fecha is string %}
|
||||||
|
{{ noticia.fecha }}
|
||||||
|
{% else %}
|
||||||
|
{{ noticia.fecha.strftime('%d-%m-%Y %H:%M') }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
N/D
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% if noticia.fuente_nombre %}
|
{% if noticia.fuente_nombre %}
|
||||||
| <span><i class="fas fa-newspaper"></i> <strong>{{ noticia.fuente_nombre }}</strong></span>
|
| <span><i class="fas fa-newspaper"></i> <strong>{{ noticia.fuente_nombre }}</strong></span>
|
||||||
|
|
@ -26,14 +42,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="resumen-container">
|
<div class="resumen-container">
|
||||||
|
{% set resumen_txt = noticia.resumen | safe_html %}
|
||||||
<div class="resumen-corto">
|
<div class="resumen-corto">
|
||||||
{{ noticia.resumen | safe_html | truncate(280, True) }}
|
{{ resumen_txt | truncate(280, True) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="resumen-completo" style="display: none;">
|
<div class="resumen-completo" style="display:none;">
|
||||||
{{ noticia.resumen | safe_html }}
|
{{ resumen_txt }}
|
||||||
</div>
|
</div>
|
||||||
{% if noticia.resumen and noticia.resumen|length > 280 %}
|
{% if noticia.resumen and noticia.resumen|length > 280 %}
|
||||||
<button class="ver-mas-btn">Ver más</button>
|
<button class="ver-mas-btn" type="button">Ver más</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,7 +64,7 @@
|
||||||
|
|
||||||
{# Resumen y paginación #}
|
{# Resumen y paginación #}
|
||||||
{% if total_results and total_results > 0 %}
|
{% if total_results and total_results > 0 %}
|
||||||
<div style="text-align:center; margin-top: 10px; color: var(--text-color-light);">
|
<div style="text-align:center; margin-top:10px; color: var(--text-color-light);">
|
||||||
{% set start_i = (page - 1) * per_page + 1 %}
|
{% set start_i = (page - 1) * per_page + 1 %}
|
||||||
{% set end_i = (page - 1) * per_page + (noticias|length) %}
|
{% set end_i = (page - 1) * per_page + (noticias|length) %}
|
||||||
Mostrando {{ start_i }}–{{ end_i }} de {{ total_results }}
|
Mostrando {{ start_i }}–{{ end_i }} de {{ total_results }}
|
||||||
|
|
@ -55,8 +72,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if total_pages and total_pages > 1 %}
|
{% if total_pages and total_pages > 1 %}
|
||||||
<nav class="pagination" aria-label="Paginación de noticias" style="margin-top: 15px;">
|
<nav class="pagination" aria-label="Paginación de noticias" style="margin-top:15px;">
|
||||||
{% set current = page %}
|
{% set current = page %}
|
||||||
|
|
||||||
{# Anterior #}
|
{# Anterior #}
|
||||||
{% if current > 1 %}
|
{% if current > 1 %}
|
||||||
<a href="#" class="page-link" data-page="{{ current - 1 }}">« Anterior</a>
|
<a href="#" class="page-link" data-page="{{ current - 1 }}">« Anterior</a>
|
||||||
|
|
@ -91,3 +109,29 @@
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Toggle "Ver más / Ver menos" con delegación; se liga una sola vez #}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (window.__noticiasToggleBound) return;
|
||||||
|
window.__noticiasToggleBound = true;
|
||||||
|
|
||||||
|
const container = document.getElementById('noticias-container') || document;
|
||||||
|
container.addEventListener('click', function (e) {
|
||||||
|
const btn = e.target.closest('.ver-mas-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const wrap = btn.closest('.resumen-container');
|
||||||
|
if (!wrap) return;
|
||||||
|
|
||||||
|
const corto = wrap.querySelector('.resumen-corto');
|
||||||
|
const completo = wrap.querySelector('.resumen-completo');
|
||||||
|
if (!corto || !completo) return;
|
||||||
|
|
||||||
|
const expanded = completo.style.display === 'block';
|
||||||
|
completo.style.display = expanded ? 'none' : 'block';
|
||||||
|
corto.style.display = expanded ? 'block' : 'none';
|
||||||
|
btn.textContent = expanded ? 'Ver más' : 'Ver menos';
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,29 @@
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
|
||||||
|
<!-- Estilos mínimos para chapa "Traducido" y (opcional) switch -->
|
||||||
|
<style>
|
||||||
|
.badge{
|
||||||
|
display:inline-block; font-size:.75rem; line-height:1;
|
||||||
|
padding:.35rem .5rem; border-radius:.5rem;
|
||||||
|
background: var(--secondary-color, #6c63ff); color:#fff; vertical-align: middle;
|
||||||
|
margin-left:.4rem;
|
||||||
|
}
|
||||||
|
/* Toggle switch opcional (si lo usas en alguna vista) */
|
||||||
|
.switch { position: relative; display: inline-block; width: 42px; height: 22px; vertical-align: middle; }
|
||||||
|
.switch input { opacity: 0; width: 0; height: 0; }
|
||||||
|
.slider {
|
||||||
|
position: absolute; cursor: pointer; top:0; left:0; right:0; bottom:0;
|
||||||
|
background:#ccc; transition:.2s; border-radius:999px;
|
||||||
|
}
|
||||||
|
.slider:before {
|
||||||
|
position: absolute; content:""; height:16px; width:16px; left:3px; bottom:3px;
|
||||||
|
background:#fff; transition:.2s; border-radius:50%;
|
||||||
|
}
|
||||||
|
.switch input:checked + .slider { background: var(--secondary-color, #6c63ff); }
|
||||||
|
.switch input:checked + .slider:before { transform: translateX(20px); }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
@ -41,13 +64,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Toggle "Ver más / Ver menos" en resúmenes
|
||||||
document.addEventListener('click', function(event) {
|
document.addEventListener('click', function(event) {
|
||||||
if (event.target.classList.contains('ver-mas-btn')) {
|
if (event.target.classList.contains('ver-mas-btn')) {
|
||||||
const container = event.target.closest('.resumen-container');
|
const container = event.target.closest('.resumen-container');
|
||||||
const corto = container.querySelector('.resumen-corto');
|
const corto = container.querySelector('.resumen-corto');
|
||||||
const completo = container.querySelector('.resumen-completo');
|
const completo = container.querySelector('.resumen-completo');
|
||||||
|
|
||||||
if (completo.style.display === 'none') {
|
if (completo.style.display === 'none' || completo.style.display === '') {
|
||||||
corto.style.display = 'none';
|
corto.style.display = 'none';
|
||||||
completo.style.display = 'block';
|
completo.style.display = 'block';
|
||||||
event.target.textContent = 'Ver menos';
|
event.target.textContent = 'Ver menos';
|
||||||
|
|
@ -61,3 +85,4 @@
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@
|
||||||
<input type="hidden" name="page" id="page" value="{{ page or 1 }}">
|
<input type="hidden" name="page" id="page" value="{{ page or 1 }}">
|
||||||
<input type="hidden" name="per_page" id="per_page" value="{{ per_page or 20 }}">
|
<input type="hidden" name="per_page" id="per_page" value="{{ per_page or 20 }}">
|
||||||
|
|
||||||
|
<!-- Idioma/traducción por defecto -->
|
||||||
|
<input type="hidden" name="lang" id="lang" value="{{ (lang or 'es') }}">
|
||||||
|
{% if not use_tr %}
|
||||||
|
<input type="hidden" name="orig" id="orig" value="1">
|
||||||
|
{% else %}
|
||||||
|
<input type="hidden" name="orig" id="orig" value="">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="filter-main-row">
|
<div class="filter-main-row">
|
||||||
<div class="filter-search-box">
|
<div class="filter-search-box">
|
||||||
<label for="q">Buscar por palabra clave</label>
|
<label for="q">Buscar por palabra clave</label>
|
||||||
|
|
@ -60,7 +68,22 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="noticias-container">
|
<!-- Barra de estado de traducción -->
|
||||||
|
<div class="card" style="margin-top: 16px; padding: 12px;">
|
||||||
|
{% if use_tr %}
|
||||||
|
<div class="alert alert-info" style="margin:0;">
|
||||||
|
Mostrando <strong>traducciones</strong> a <strong>{{ (lang or 'es')|upper }}</strong>.
|
||||||
|
<a href="#" id="toggle-orig" class="ms-2">Ver originales</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-secondary" style="margin:0;">
|
||||||
|
Mostrando <strong>texto original</strong>.
|
||||||
|
<a href="#" id="toggle-tr" class="ms-2">Usar traducciones</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="noticias-container" style="margin-top:16px;">
|
||||||
{# El parcial incluye la lista + la paginación #}
|
{# El parcial incluye la lista + la paginación #}
|
||||||
{% include '_noticias_list.html' %}
|
{% include '_noticias_list.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,6 +94,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const continenteSelect = document.getElementById('continente_id');
|
const continenteSelect = document.getElementById('continente_id');
|
||||||
const paisSelect = document.getElementById('pais_id');
|
const paisSelect = document.getElementById('pais_id');
|
||||||
const pageInput = document.getElementById('page');
|
const pageInput = document.getElementById('page');
|
||||||
|
const origInput = document.getElementById('orig');
|
||||||
|
const langInput = document.getElementById('lang');
|
||||||
|
|
||||||
function filtrarPaises() {
|
function filtrarPaises() {
|
||||||
const continenteId = continenteSelect.value;
|
const continenteId = continenteSelect.value;
|
||||||
|
|
@ -119,13 +144,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Clic en enlaces de paginación (delegación)
|
// Clic en enlaces de paginación (delegación)
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
const link = e.target.closest('a.page-link');
|
const link = e.target.closest('a.page-link');
|
||||||
if (link && link.dataset.page) {
|
if (link) {
|
||||||
|
// soporta data-page o parseo del href
|
||||||
|
let nextPage = link.dataset.page;
|
||||||
|
if (!nextPage) {
|
||||||
|
try {
|
||||||
|
const u = new URL(link.href);
|
||||||
|
nextPage = u.searchParams.get('page');
|
||||||
|
} catch(_) {}
|
||||||
|
}
|
||||||
|
if (nextPage) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
pageInput.value = link.dataset.page;
|
pageInput.value = nextPage;
|
||||||
cargarNoticias(true);
|
cargarNoticias(true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Conmutar traducciones <-> originales manteniendo filtros
|
||||||
|
const toggleOrig = document.getElementById('toggle-orig');
|
||||||
|
const toggleTr = document.getElementById('toggle-tr');
|
||||||
|
|
||||||
|
if (toggleOrig) {
|
||||||
|
toggleOrig.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// orig=1 fuerza originales
|
||||||
|
origInput.value = '1';
|
||||||
|
cargarNoticias(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (toggleTr) {
|
||||||
|
toggleTr.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// orig vacío => usar traducciones
|
||||||
|
origInput.value = '';
|
||||||
|
// asegúrate de tener lang (por si el back usa default)
|
||||||
|
if (!langInput.value) langInput.value = 'es';
|
||||||
|
cargarNoticias(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Inicializaciones
|
// Inicializaciones
|
||||||
continenteSelect.addEventListener('change', function() {
|
continenteSelect.addEventListener('change', function() {
|
||||||
filtrarPaises();
|
filtrarPaises();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue