From 396df4293fd3f3fe8b1256da4812be232e668b71 Mon Sep 17 00:00:00 2001 From: David Itehua Xalamihua Date: Sat, 3 May 2025 15:47:24 -0600 Subject: [PATCH] test --- Documents_ref/conf.sql | 20 +- .../__pycache__/cls_db_usr.cpython-312.pyc | Bin 8143 -> 9076 bytes forms_py/cls_db_usr.py | 15 +- main.py | 120 ++--- static/e_blog/b_share_btn.css | 62 +++ static/e_blog/copy_url.js | 21 +- .../d_read_post/iframe_fullscreen.js | 0 static/h_tmp_user/d_read_post/read_post.css | 21 + templates/e_blog/a_all_posts copy.html | 132 ++++++ templates/e_blog/a_all_posts.html | 434 +++++++++++++----- templates/e_blog/b_post.html | 83 +--- templates/h_tmp_usr/d_read_post.html | 44 +- templates/z_comps/read_progress.html | 72 +++ 13 files changed, 753 insertions(+), 271 deletions(-) create mode 100644 static/e_blog/b_share_btn.css create mode 100644 static/h_tmp_user/d_read_post/iframe_fullscreen.js create mode 100644 templates/e_blog/a_all_posts copy.html create mode 100644 templates/z_comps/read_progress.html diff --git a/Documents_ref/conf.sql b/Documents_ref/conf.sql index 89f69c8..a9470bf 100644 --- a/Documents_ref/conf.sql +++ b/Documents_ref/conf.sql @@ -8,11 +8,11 @@ -- psql -h 127.0.0.1 -U postgres -a -f conf.sql -DROP DATABASE IF EXISTS forma; +DROP DATABASE IF EXISTS formha; -CREATE DATABASE forma; +CREATE DATABASE formha; -\c forma; +\c formha; CREATE TABLE contact ( @@ -63,4 +63,16 @@ CREATE TABLE posts ( CREATE TABLE posts_visited( id_post INT, viewed TIMESTAMP WITH TIME ZONE -); \ No newline at end of file +); + + +CREATE TABLE visited_from ( + id SERIAL PRIMARY KEY, + post_id INT NOT NULL, + source_name VARCHAR(50), + visit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + + + +ALTER DATABASE formha SET timezone TO 'America/Mexico_City'; \ No newline at end of file diff --git a/forms_py/__pycache__/cls_db_usr.cpython-312.pyc b/forms_py/__pycache__/cls_db_usr.cpython-312.pyc index dd487b8d4a8646480fc069daffe41ad46107e145..52cfa4eac768add31a857ab64100a238d7a4718a 100644 GIT binary patch delta 1752 zcmaJ>Urbw77(eI!YYX(YrG@gJvaPZ*K!GtgR@apwOVse7FRm=Q|L>?M^(Q`SXnAE9* zDKy8x3?j7BIw^GFfRz)@VsG(|@HK~Pt#@1lhaiTA8Q9{bJfQ_!fFiUJESMN@(ugCh z#2>DD++6gy-{KnUA*}%*!k|2CsbW9wvA(NV#?j)H$~W+{{}arTZE2jC=?V3+JTy40 z(`=@DYaO@xyvtG)_Gl~q;=PUWruBs{(z#jvJf%8~Oyr0$NoYpZ2O=>Kt%X4N+q4Ow z86af+<7-Z|g4)6$=5ks>+rZEPun)jFt3-*B*G#&fSqvA0ty4qi&iGsLaIxV3jU%1R z@x)%XdyJUMg^Z@r5Y&4b;2D6OouyqnKi-t~K#;RzS)XqQ+oe~S%lOus^&hdiLVsJc zm9==@s*(>{zX!rirog<8e2#Cz2WHZbX85c8MVFPV4_fWjGWJ+Kz6R@HRd&u95{S82 z1z0Wz7?EZK6A0#r%3T#cLN~bWx^FUS&j_=^6gGi>jy#dW|5wfwn&#)upeY;^VA^EG z8K(4DMlp3-pk1sS?PgHcR1vJsS3C^bPE&XKDel=%rW!qhDw~x*t6>>0+^ zd!Z9*0$mR$`}n1{WV5(Z;bH1Z(34D{m1Z&dlxJn13oIROu=I(^xNGGo4|?3eL~t;{ zuU5;05q>pN1$w7EIKZzCh)lN`naw=SDZF*9GE_WO^AkQ>J|4E4>QjoI&zZKQ=-KRq z=?usYYh$|!TRtj#5Oz8o#jh1V4nGv)$64=aoWb_6odg41Fk54{L;gJ`?Z>7lzEXVH zl*H_+damVA8u;o=HoK=yN)N!Y9U#WQ7R~&mMrbb>`T>rDRXDHf8co1@kiin7UeGH6 zV6Ytk#rVKlZF$wT(clq}m1?b~*5h+dB>33*9oq3sw|(0;Y&V%UX~v*1e$-*9Q}6{? xKHg;09nb1&ULz;yOUwb|X#jCcJjVEE{36~!&I<})^e>fDbXfoZ delta 1024 zcmaKr%WKq76o+%tOlCUMBx7sK*s)Ifvc|r!qUf|(Y9B6i<4PHdw4K~OATt?HGRPDJ zg}N2SyB2XNQa9Pujo?-h-MESLKM<`V6zs-(QtShZfqc35cV73Lx$|r0S za3{6g(OTCtaH-zcn~|k!lC7*scI>50W=Se2HL%orC0Bpm1`p~Rh5iIn?M9Zgo0zt! z+S-~@&~064igZ}Kn>o?3oxMz3mSwp;TWM= zz-kB+6|YuwD@!9`yy%w8tmtEu2e45%cpzY6lH1B482pJc3r)dy<&6xj{8cU%86@?00IU47VGgV|Pb0c$u9TTqVK{HQ zdDCjdR&u06W0r7|(84>@9pk5H*&$%Xqk*7Ff<@>gM4hV0ysG14mry*K-fqt{xBKZF zOa)KVUuD)W4FA*$6N}6*F3&p-wotwS!XRP4vU;HMUQrw(r>Nq=Df4eFeQ+~qJANL5 zjm$R89&Rm6pnI?AR?B{<7Tr>5#gCfXuV@`CcDT(Wilwr>a3?sI-BHFa&~Yvb{2f%D z1iBi#Bildt5mQ<=d!ZaWHYWhm{8MLtAw_X?^svG65xZ%7nsA1+m|v|p42MY=AwgMi zT?a4IeoWwngiW-jJBKu#2r;qn>b%f(Ti$Btue(OI$cgM$O#Ix?ox2e^Pf^5Pg5Qa; pl51BTb_HjIf&LI4U68E?0Qe#qn^I;|I=ih*%5WWiO3^2Z{sVTj%&GtY diff --git a/forms_py/cls_db_usr.py b/forms_py/cls_db_usr.py index ba0a9f9..d73604b 100644 --- a/forms_py/cls_db_usr.py +++ b/forms_py/cls_db_usr.py @@ -1,6 +1,8 @@ import psycopg2 from psycopg2 import sql from psycopg2.extras import execute_values +from psycopg2.extras import RealDictCursor + class DBForma: def __init__(self, db_obj: dict): @@ -90,7 +92,18 @@ class DBForma: raise RuntimeError(f"Error al ejecutar la consulta: {e}") except Exception as e: raise RuntimeError(f"Error inesperado: {e}") - + + def get_all_data_dict(self, query): + try: + with self._get_connection() as conn: + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + cursor.execute(query) + return cursor.fetchall() + except psycopg2.DatabaseError as e: + raise RuntimeError(f"Error al ejecutar la consulta: {e}") + except Exception as e: + raise RuntimeError(f"Error inesperado: {e}") + def update_data(self, query: str, data_tuple: tuple) -> bool: """ """ diff --git a/main.py b/main.py index 30e51be..ad5abbc 100644 --- a/main.py +++ b/main.py @@ -145,102 +145,67 @@ def solutions(): def methodology(): return render_template(v['methodology'], active_page='methodology') - @app.route('/blog') def blog(): + return render_template( v['blog']['all_posts'], active_page='blog' ) - # if any(x in search for x in ["'", '"', " OR ", "--", ";", "1=1"]): - # app.logger.warning(f"Intento de SQL injection detectado: {search}") - # 🛑 IMPORTANTE: Este bloque acepta input del usuario y necesita sanitización adecuada. - # TODO: Reemplazar concatenación de strings por parámetros SQL usando psycopg2.sql.SQL y placeholders (%s) - # Ejemplo de ataque detectado: ' OR '1'='1 - # Aunque no ejecuta el ataque, sí lanza error → posible vector de DoS - - - # Parámetros - page = request.args.get("page", 1, type=int) - search = request.args.get("q", "").strip() - per_page = 9 - offset = (page - 1) * per_page - - # Armado de condiciones SQL para búsqueda - search_filter = "" - if search: - like = f"'%{search}%'" - search_filter = f""" - WHERE - LOWER(p.title) LIKE LOWER({like}) OR - LOWER(p.body_no_img) LIKE LOWER({like}) OR - LOWER(u.nombre || ' ' || u.apellido) LIKE LOWER({like}) - """ - - # Conteo total - count_query = f""" - SELECT COUNT(*) - FROM posts p - INNER JOIN users u ON u.id = p.id_usr - {search_filter}; +@app.route('/blog/api/posts') +def api_posts(): + q = r""" + SELECT + p.id, + u.nombre || ' ' || u.apellido AS autor, + TO_CHAR(p.created_at AT TIME ZONE 'America/Mexico_City', 'YYYY-MM-DD HH24:MI') AS author_creation, + TO_CHAR(p.updated_at AT TIME ZONE 'America/Mexico_City', 'YYYY-MM-DD HH24:MI') AS author_updated, + p.title, + SUBSTRING(p.body_no_img FROM 1 FOR 180) AS preview, + (SELECT value FROM jsonb_array_elements_text(p.lista_imagenes) LIMIT 1) AS first_img, + ROUND(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375, 1) AS read_time, + GREATEST(1, CEIL(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375)) AS read_time_min + FROM posts p + JOIN users u ON p.id_usr = u.id + ORDER BY p.created_at DESC; """ - total_posts = dbUsers.get_all_data(count_query)[0][0] - total_pages = (total_posts + per_page - 1) // per_page + data = dbUsers.get_all_data_dict(q) + return jsonify(data) - # Consulta con paginación - q = fr""" - SELECT - p.id, - u.nombre, - u.apellido, - TO_CHAR(p.created_at, 'DD/MM/YYYY HH24:MI'), - TO_CHAR(p.updated_at, 'DD/MM/YYYY HH24:MI'), - p.title, - LEFT(p.body_no_img, 180) AS preview, - p.lista_imagenes->0 AS primera_imagen, - array_length(regexp_split_to_array(TRIM(body_no_img), '\s+'), 1) / 375 as n_words - FROM - posts p - INNER JOIN - users u ON u.id = p.id_usr - {search_filter} - ORDER BY - p.created_at DESC - LIMIT {per_page} OFFSET {offset}; - """ - data = dbUsers.get_all_data(q) - return render_template( - v['blog']['all_posts'], - active_page='blog', - data=data, - current_page=page, - total_pages=total_pages, - search=search # pasamos el término de búsqueda a la plantilla - ) @app.route('/blog/') +@app.route('/blog//src/') # @cache.cached(timeout=43200) -def blog_post(post_id): - # q_visited = "INSERT INTO posts_visited (id_post, viewed ) VALUES ( %s, %s);" - # t_visited = (post_id, cur_date()) - # dbUsers.update_data(q_visited, t_visited) - # Obtener el post +def blog_post(post_id, source_name=None): + # source_name = source_name or 'direct' + # source_name = source_name.lower() + # if source_name not in ['facebook', 'linkedin', 'x', 'li', 'fb', 'tw', 'direct']: + # source_name = 'None' + + if source_name != None: + q_vf = "INSERT INTO visited_from (post_id, source_name) VALUES (%s, %s);" + t_vf = (post_id, source_name) + dbUsers.update_data(q_vf, t_vf) + q = fr""" SELECT u.nombre, u.apellido, TO_CHAR(p.created_at, 'DD/MM/YYYY HH24:MI') AS fecha_creada, TO_CHAR(p.updated_at, 'DD/MM/YYYY HH24:MI') AS fecha_updated, - array_length(regexp_split_to_array(TRIM(p.body_no_img), '\s+'), 1) / 375 as read_time_min, + GREATEST(1, CEIL(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375)) AS read_time_min, p.title, - p.body + p.body, + COUNT(pv.viewed) AS total_views FROM posts p INNER JOIN - users u - ON - p.id_usr = u.id + users u ON p.id_usr = u.id + LEFT JOIN + posts_visited pv ON pv.id_post = p.id WHERE - p.id = %s; + p.id = %s + GROUP BY + u.nombre, u.apellido, p.created_at, p.updated_at, p.title, p.body, p.body_no_img; """ t = (post_id,) data = dbUsers.get_data(q, t) @@ -248,9 +213,6 @@ def blog_post(post_id): return render_template(v['blog']['post'], data=data) - - - @app.route("/contact", methods=['GET', 'POST']) def contact(): form = ContactForm() @@ -544,8 +506,6 @@ def save_post(): body = data['body'] time = cur_date() - print(time) - soup = BeautifulSoup(body, 'html.parser') body_no_img = soup.get_text(separator=' ', strip=True) etiquetas_img = re.findall(r']+src=["\'](.*?)["\']', body) diff --git a/static/e_blog/b_share_btn.css b/static/e_blog/b_share_btn.css new file mode 100644 index 0000000..de5d084 --- /dev/null +++ b/static/e_blog/b_share_btn.css @@ -0,0 +1,62 @@ +.share { + position: relative; + display: flex; + align-items: center; + background: #eee; + border-radius: 2rem; + width: 3rem; + height: 3rem; + overflow: hidden; + transition: width 0.3s ease; +} + +.share:hover { + width: 18rem; +} + +.share__wrapper { + display: flex; + align-items: center; + height: 100%; + position: relative; +} + +.share__toggle { + background: #549c67; + color: white; + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + margin: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.share__button { + background: #555; + color: white; + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + margin-left: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: scale(0); + transition: transform 0.3s ease, opacity 0.3s ease; + position: relative; +} + +/* Mostrar botones solo cuando se hace hover */ +.share:hover .share__button { + opacity: 1; + transform: scale(1); +} + +.fb { background: #1877f2; } +.tw { background: #000000; } +.in { background: #0077b5; } +.copy { background: #444; } \ No newline at end of file diff --git a/static/e_blog/copy_url.js b/static/e_blog/copy_url.js index 46acd7b..cca67fb 100644 --- a/static/e_blog/copy_url.js +++ b/static/e_blog/copy_url.js @@ -1,9 +1,16 @@ import { simpleNotification } from '../z_comps/notify.js'; -const url = window.location.href; -const url_encoded = encodeURIComponent(url); +// Limpia el /src/... si ya viene en la URL +let baseUrl = window.location.href.replace(/\/src\/\w+$/, ''); + +function encode_url(redoSocial = '') { + let finalUrl = baseUrl; + if (redoSocial !== '') { + finalUrl += `/src/${redoSocial}`; + } + return encodeURIComponent(finalUrl); +} -// Función reutilizable para abrir ventanas de compartir function openShareWindow(shareUrl) { window.open(shareUrl, '_blank', 'width=600,height=400,noopener,noreferrer'); } @@ -13,7 +20,7 @@ const btn_copy = document.querySelector("button.copy"); if (btn_copy) { btn_copy.addEventListener("click", async () => { try { - await navigator.clipboard.writeText(url); + await navigator.clipboard.writeText(baseUrl); simpleNotification("URL Copiada", "URL copiada", "success"); } catch (err) { console.error('Error al copiar: ', err); @@ -25,7 +32,7 @@ if (btn_copy) { const btn_in = document.querySelector("button.in"); if (btn_in) { btn_in.addEventListener("click", () => { - const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${url_encoded}`; + const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encode_url('li')}`; openShareWindow(linkedInUrl); }); } @@ -34,7 +41,7 @@ if (btn_in) { const btn_fb = document.querySelector("button.fb"); if (btn_fb) { btn_fb.addEventListener("click", () => { - const fbShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${url_encoded}`; + const fbShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encode_url('fb')}`; openShareWindow(fbShareUrl); }); } @@ -44,7 +51,7 @@ const btn_x = document.querySelector("button.tw"); if (btn_x) { btn_x.addEventListener("click", () => { const tweetText = encodeURIComponent("Mira este post interesante:"); - const xShareUrl = `https://twitter.com/intent/tweet?url=${url_encoded}&text=${tweetText}`; + const xShareUrl = `https://twitter.com/intent/tweet?url=${encode_url('x')}&text=${tweetText}`; openShareWindow(xShareUrl); }); } diff --git a/static/h_tmp_user/d_read_post/iframe_fullscreen.js b/static/h_tmp_user/d_read_post/iframe_fullscreen.js new file mode 100644 index 0000000..e69de29 diff --git a/static/h_tmp_user/d_read_post/read_post.css b/static/h_tmp_user/d_read_post/read_post.css index 2ab4b32..4005e77 100644 --- a/static/h_tmp_user/d_read_post/read_post.css +++ b/static/h_tmp_user/d_read_post/read_post.css @@ -99,5 +99,26 @@ clear: both; } +/* -------------------------- */ +ul, ol { + /* Asegura que el espacio no afecte la alineación de viñetas/números */ + padding-left: 1em; /* Ajusta según tu diseño */ +} + +li { + text-align: justify; /* Justifica el texto */ + margin-bottom: 0.5em; /* Espaciado entre elementos (opcional) */ +} + +li p, li span { + text-align: justify; + display: inline-block; /* Necesario para que funcione en elementos en línea */ + width: 100%; /* Ocupa todo el ancho disponible */ +} + +li { + text-align: justify; + hyphens: auto; /* Permite división de palabras con guiones */ +} diff --git a/templates/e_blog/a_all_posts copy.html b/templates/e_blog/a_all_posts copy.html new file mode 100644 index 0000000..7af9435 --- /dev/null +++ b/templates/e_blog/a_all_posts copy.html @@ -0,0 +1,132 @@ +{% extends 'template.html' %} + +{% block css %} + +{% endblock css %} + +{% block navbar %} +{% include 'z_comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + + + +
+
+ + +
+
+ +
+ + + + + +
+ + + {% for post in data %} +
+
+ + card image + +
+
+ + +
{{post[5]}}

+ + + {{ post[1] }} {{ post[2] }}
+ + {{ post[3] }}
+ + {% if post[4] is not none %} + {{ post[4] }}
+ {% endif %} + + {{ post[8] }} min.
+ +
+ + +

{{ post[6] }}...

+ +
+ +
+
+
+ {% endfor %} + + +
+ + + + +
+ + + +{% endblock body %} + +{% block js %} + + +{% include 'z_comps/aos_script.html' %} + +{% endblock js %} \ No newline at end of file diff --git a/templates/e_blog/a_all_posts.html b/templates/e_blog/a_all_posts.html index 7af9435..bb1b6a3 100644 --- a/templates/e_blog/a_all_posts.html +++ b/templates/e_blog/a_all_posts.html @@ -12,120 +12,344 @@ -
-
- - + + + +
+
+ + + + + + + + + 0  resultado(s) +
- - -
- - - - - -
- - - {% for post in data %} -
-
- - card image - -
-
- - -
{{post[5]}}

- - - {{ post[1] }} {{ post[2] }}
- - {{ post[3] }}
- - {% if post[4] is not none %} - {{ post[4] }}
- {% endif %} - - {{ post[8] }} min.
- -
- - -

{{ post[6] }}...

- -
- -
-
-
- {% endfor %} - - -
- - - -
+ + + +
+
+ Cargando... +
+
+ +
+ + + + + + + {% endblock body %} {% block js %} + + + + + + {% include 'z_comps/aos_script.html' %} diff --git a/templates/e_blog/b_post.html b/templates/e_blog/b_post.html index 0b951c8..66b9749 100644 --- a/templates/e_blog/b_post.html +++ b/templates/e_blog/b_post.html @@ -2,6 +2,7 @@ {% block css %} + {% endblock css %} {% block navbar %} @@ -10,87 +11,19 @@ {% block body %} - - +{{data}}

{{data[5]}}

- - Publicaciones
+ Publicaciones
{{data[0]}} {{data[1]}} | {{data[2]}} | {% if data[3] is not none %} {{data[3]}} | {% endif %} {{ data[4] }} Minutos | - + {{ data[7] }}
-
+
+ {{data[6] | safe}}
@@ -119,8 +53,13 @@ + + + + {% endblock body %} {% block js %} +{% include "z_comps/read_progress.html" %} diff --git a/templates/h_tmp_usr/d_read_post.html b/templates/h_tmp_usr/d_read_post.html index 6041627..95ed1cf 100644 --- a/templates/h_tmp_usr/d_read_post.html +++ b/templates/h_tmp_usr/d_read_post.html @@ -7,7 +7,26 @@ {% block body %} - + + +

{{data[2]}}

@@ -21,13 +40,30 @@ Editar.
-
+
{{data[3] | safe}}
+ + + + + + + {% endblock body %} @@ -36,4 +72,8 @@ {% include 'z_comps/arrow_to_up.html' %} + + + + {% endblock js %} \ No newline at end of file diff --git a/templates/z_comps/read_progress.html b/templates/z_comps/read_progress.html new file mode 100644 index 0000000..43c8f28 --- /dev/null +++ b/templates/z_comps/read_progress.html @@ -0,0 +1,72 @@ + +
+
+ 0% +
+
+ + + + + + +