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_USER=rss
|
||||
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 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
|
||||
if use_translation:
|
||||
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
|
||||
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():
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@ services:
|
|||
- DB_USER=${DB_USER}
|
||||
- DB_PASS=${DB_PASS}
|
||||
- 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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
@ -75,11 +80,11 @@ services:
|
|||
|
||||
# --- Worker ---
|
||||
- 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
|
||||
- TRANSLATOR_SLEEP_IDLE=5
|
||||
|
||||
# Tokens (equilibrio calidad/VRAM)
|
||||
# Tokens (equilibrio calidad/VRAM ~<7GB)
|
||||
- MAX_SRC_TOKENS=512
|
||||
- MAX_NEW_TOKENS=256
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,27 @@
|
|||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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">
|
||||
<span><i class="far fa-calendar-alt"></i>
|
||||
{{ noticia.fecha.strftime('%d-%m-%Y %H:%M') if noticia.fecha else 'N/D' }}
|
||||
<span>
|
||||
<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>
|
||||
{% if noticia.fuente_nombre %}
|
||||
| <span><i class="fas fa-newspaper"></i> <strong>{{ noticia.fuente_nombre }}</strong></span>
|
||||
|
|
@ -26,14 +42,15 @@
|
|||
</div>
|
||||
|
||||
<div class="resumen-container">
|
||||
{% set resumen_txt = noticia.resumen | safe_html %}
|
||||
<div class="resumen-corto">
|
||||
{{ noticia.resumen | safe_html | truncate(280, True) }}
|
||||
{{ resumen_txt | truncate(280, True) }}
|
||||
</div>
|
||||
<div class="resumen-completo" style="display: none;">
|
||||
{{ noticia.resumen | safe_html }}
|
||||
<div class="resumen-completo" style="display:none;">
|
||||
{{ resumen_txt }}
|
||||
</div>
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -47,7 +64,7 @@
|
|||
|
||||
{# Resumen y paginación #}
|
||||
{% 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 end_i = (page - 1) * per_page + (noticias|length) %}
|
||||
Mostrando {{ start_i }}–{{ end_i }} de {{ total_results }}
|
||||
|
|
@ -55,8 +72,9 @@
|
|||
{% endif %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{# Anterior #}
|
||||
{% if current > 1 %}
|
||||
<a href="#" class="page-link" data-page="{{ current - 1 }}">« Anterior</a>
|
||||
|
|
@ -91,3 +109,29 @@
|
|||
</nav>
|
||||
{% 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="{{ 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>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
|
@ -41,13 +64,14 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle "Ver más / Ver menos" en resúmenes
|
||||
document.addEventListener('click', function(event) {
|
||||
if (event.target.classList.contains('ver-mas-btn')) {
|
||||
const container = event.target.closest('.resumen-container');
|
||||
const corto = container.querySelector('.resumen-corto');
|
||||
const completo = container.querySelector('.resumen-completo');
|
||||
|
||||
if (completo.style.display === 'none') {
|
||||
if (completo.style.display === 'none' || completo.style.display === '') {
|
||||
corto.style.display = 'none';
|
||||
completo.style.display = 'block';
|
||||
event.target.textContent = 'Ver menos';
|
||||
|
|
@ -61,3 +85,4 @@
|
|||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@
|
|||
<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 }}">
|
||||
|
||||
<!-- 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-search-box">
|
||||
<label for="q">Buscar por palabra clave</label>
|
||||
|
|
@ -60,7 +68,22 @@
|
|||
</form>
|
||||
</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 #}
|
||||
{% include '_noticias_list.html' %}
|
||||
</div>
|
||||
|
|
@ -71,6 +94,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const continenteSelect = document.getElementById('continente_id');
|
||||
const paisSelect = document.getElementById('pais_id');
|
||||
const pageInput = document.getElementById('page');
|
||||
const origInput = document.getElementById('orig');
|
||||
const langInput = document.getElementById('lang');
|
||||
|
||||
function filtrarPaises() {
|
||||
const continenteId = continenteSelect.value;
|
||||
|
|
@ -119,13 +144,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// Clic en enlaces de paginación (delegación)
|
||||
document.addEventListener('click', function(e) {
|
||||
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();
|
||||
pageInput.value = link.dataset.page;
|
||||
pageInput.value = nextPage;
|
||||
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
|
||||
continenteSelect.addEventListener('change', function() {
|
||||
filtrarPaises();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue