From ce19d301e6a345a3139eb6db7f560490039f81b7 Mon Sep 17 00:00:00 2001 From: jlimolina Date: Thu, 12 Jun 2025 18:29:08 +0200 Subject: [PATCH] =?UTF-8?q?arreglada=20la=20visualizacion,=20a=C3=B1adido?= =?UTF-8?q?=20busqueda=20por=20fechas=20y=20palabras=20clave?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 65 ++++++++--- templates/_noticias_list.html | 24 ++++ templates/noticias.html | 202 +++++++++++++++++----------------- 3 files changed, 179 insertions(+), 112 deletions(-) create mode 100644 templates/_noticias_list.html diff --git a/app.py b/app.py index dc42757..9c521c7 100644 --- a/app.py +++ b/app.py @@ -43,30 +43,46 @@ def safe_html(text): allowed_attrs = {'a': ['href', 'title'], 'img': ['src', 'alt']} return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, strip=True) +# En app.py, reemplaza tu función home() con esta: + +# En app.py + @app.route("/") def home(): - noticias, categorias, continentes, paises = [], [], [], [] + # ... (el código inicial para obtener los parámetros no cambia) ... 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") + q = request.args.get("q", "").strip() # <--- NUEVO: Obtenemos el término de búsqueda + + # ... (el resto del código hasta la construcción de la consulta) ... + # (se omite por brevedad, es idéntico) try: with get_conn() as conn: with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor: + # ... (las consultas para obtener categorías, continentes y países no cambian) ... cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre") categorias = cursor.fetchall() cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre") continentes = cursor.fetchall() - if cont_id: - cursor.execute("SELECT id, nombre, continente_id FROM paises WHERE continente_id = %s ORDER BY nombre", (cont_id,)) - else: - 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() + sql_params, conditions = [], [] + # El SELECT ahora puede incluir el ranking de la búsqueda sql_base = "SELECT n.fecha, n.titulo, n.resumen, n.url, n.imagen_url, c.nombre AS categoria, p.nombre AS pais, co.nombre AS continente FROM noticias n LEFT JOIN categorias c ON n.categoria_id = c.id LEFT JOIN paises p ON n.pais_id = p.id LEFT JOIN continentes co ON p.continente_id = co.id" + # === INICIO DE LA MODIFICACIÓN DE LA CONSULTA === + if q: + # Convierte el término de búsqueda para que busque palabras completas + # Ejemplo: "guerra ucrania" -> "guerra & ucrania" + search_query = " & ".join(q.split()) + conditions.append("n.tsv @@ to_tsquery('spanish', %s)") + sql_params.append(search_query) + if cat_id: conditions.append("n.categoria_id = %s"); sql_params.append(cat_id) if pais_id: conditions.append("n.pais_id = %s"); sql_params.append(pais_id) elif cont_id: conditions.append("p.continente_id = %s"); sql_params.append(cont_id) @@ -74,25 +90,46 @@ def home(): if fecha_filtro: try: fecha_obj = datetime.strptime(fecha_filtro, '%Y-%m-%d') - fecha_inicio = fecha_obj - fecha_fin = fecha_obj + timedelta(days=1) - conditions.append("n.fecha >= %s AND n.fecha < %s") - sql_params.extend([fecha_inicio, fecha_fin]) + conditions.append("n.fecha::date = %s") + sql_params.append(fecha_obj.date()) except ValueError: flash("Formato de fecha no válido. Use AAAA-MM-DD.", "error") fecha_filtro = None - if conditions: sql_base += " WHERE " + " AND ".join(conditions) - sql_final = sql_base + " ORDER BY n.fecha DESC NULLS LAST LIMIT 50" + if conditions: + sql_base += " WHERE " + " AND ".join(conditions) + + # Si hay búsqueda, ordena por relevancia. Si no, por fecha. + if q: + search_query_ts = " & ".join(q.split()) + sql_base += " ORDER BY ts_rank(n.tsv, to_tsquery('spanish', %s)) DESC, n.fecha DESC" + sql_params.append(search_query_ts) + else: + sql_base += " ORDER BY n.fecha DESC NULLS LAST" + + sql_final = sql_base + " LIMIT 50" + # === FIN DE LA MODIFICACIÓN DE LA CONSULTA === + cursor.execute(sql_final, tuple(sql_params)) noticias = cursor.fetchall() except psycopg2.Error as db_err: app.logger.error(f"[DB ERROR] Al leer noticias: {db_err}", exc_info=True) flash("Error de base de datos al cargar las noticias.", "error") + noticias, categorias, continentes, paises = [], [], [], [] # Resetea en caso de error - return render_template("noticias.html", 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) + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return render_template('_noticias_list.html', noticias=noticias) + + return render_template("noticias.html", + 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) # <--- NUEVO: Pasamos el término de búsqueda a la plantilla @app.route("/feeds") def dashboard(): diff --git a/templates/_noticias_list.html b/templates/_noticias_list.html new file mode 100644 index 0000000..bf32c7d --- /dev/null +++ b/templates/_noticias_list.html @@ -0,0 +1,24 @@ +
+ {% for noticia in noticias %} +
+ {% if noticia.imagen_url %} +
+ {{ noticia.titulo }} +
+ {% endif %} +
+

{{ noticia.titulo }}

+
+ {{ noticia.fecha.strftime('%d-%m-%Y %H:%M') if noticia.fecha else 'N/D' }} | + {{ noticia.categoria or 'N/A' }} | + {{ noticia.pais or 'Global' }} +
+

{{ noticia.resumen | striptags | safe_html | truncate(280) }}

+
+
+ {% else %} +
+

No hay noticias que mostrar con los filtros seleccionados.

+
+ {% endfor %} +
diff --git a/templates/noticias.html b/templates/noticias.html index 851df2f..0493291 100644 --- a/templates/noticias.html +++ b/templates/noticias.html @@ -3,110 +3,116 @@ {% block title %}Últimas Noticias RSS{% endblock %} {% block content %} -
-

Agregador de Noticias

-

Tus fuentes de información, en un solo lugar.

- ⚙️ Gestionar Feeds -
+
+

Agregador de Noticias

+

Tus fuentes de información, en un solo lugar.

+ + Gestionar Feeds + +
-
-

Filtrar Noticias

-
-
-
- - -
-
- - -
-
- - -
-
- -
+
+

Filtrar Noticias

+ + +
+ + +
+
+
+ +
- -
- -
- {% for noticia in noticias %} -
- {% if noticia.imagen_url %} -
- {{ noticia.titulo }} -
- {% endif %} -
-

{{ noticia.titulo }}

-
- 🗓️ {{ noticia.fecha.strftime('%d-%m-%Y %H:%M') if noticia.fecha else 'Fecha no disponible' }} | - {{ noticia.categoria or 'N/A' }} | - {{ noticia.pais or 'Global' }} -
-

{{ noticia.resumen | striptags | safe_html | truncate(280) }}

-
-
- {% else %} -
-

No hay noticias que mostrar con los filtros seleccionados.

+
+ +
- {% endfor %} -
+
+ + +
+
+ + +
+
+ + Limpiar +
+
+ +
- - + // Lógica para enviar el formulario con AJAX + async function actualizarNoticias(event) { + if (event) event.preventDefault(); + const formData = new FormData(form); + const params = new URLSearchParams(formData); + const newUrl = `${form.action}?${params.toString()}`; + const container = document.getElementById('noticias-container'); + container.style.opacity = '0.5'; + container.innerHTML = '
'; + try { + const response = await fetch(newUrl, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + const html = await response.text(); + container.innerHTML = html; + window.history.pushState({path: newUrl}, '', newUrl); + } catch (error) { + console.error('Error al filtrar noticias:', error); + container.innerHTML = '

Error al cargar las noticias.

'; + } finally { + container.style.opacity = '1'; + } + } + + // Asignar los eventos + continenteSelect.addEventListener('change', filtrarPaises); + form.addEventListener('submit', actualizarNoticias); + filtrarPaises(); // Ejecutar al inicio +}); + {% endblock %}