diff --git a/Documents_ref/conf.sql b/Documents_ref/conf.sql index a9470bf..e26f73b 100644 --- a/Documents_ref/conf.sql +++ b/Documents_ref/conf.sql @@ -74,5 +74,29 @@ CREATE TABLE visited_from ( ); +-- cargar a la base de datos de producción +CREATE TABLE carousel ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + img_name VARCHAR(150) NOT NULL, + bg_color VARCHAR(40) NOT NULL, + txt_color VARCHAR(40) NOT NULL, + txt VARCHAR(400) NOT NULL +); -ALTER DATABASE formha SET timezone TO 'America/Mexico_City'; \ No newline at end of file +ALTER DATABASE formha SET timezone TO 'America/Mexico_City'; + + +SELECT + CASE source_name + WHEN 'li' THEN 'LinkedIn' + WHEN 'wa' THEN 'WhatsApp' + WHEN 'fb' THEN 'Facebook' + WHEN 'x' THEN 'X' + ELSE 'Otro' + END AS nombre_legible, + COUNT(source_name) AS total +FROM visited_from +GROUP BY source_name; + +-- select * from visited_from; \ No newline at end of file diff --git a/forms_py/__pycache__/cls_form_carousel.cpython-312.pyc b/forms_py/__pycache__/cls_form_carousel.cpython-312.pyc new file mode 100644 index 0000000..f00ea98 Binary files /dev/null and b/forms_py/__pycache__/cls_form_carousel.cpython-312.pyc differ diff --git a/forms_py/__pycache__/functions.cpython-312.pyc b/forms_py/__pycache__/functions.cpython-312.pyc index a4d19ad..717c6fc 100644 Binary files a/forms_py/__pycache__/functions.cpython-312.pyc and b/forms_py/__pycache__/functions.cpython-312.pyc differ diff --git a/forms_py/cls_form_carousel.py b/forms_py/cls_form_carousel.py new file mode 100644 index 0000000..07e699b --- /dev/null +++ b/forms_py/cls_form_carousel.py @@ -0,0 +1,33 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, FileField +from wtforms.fields import HiddenField +from wtforms.validators import DataRequired +from wtforms.widgets import ColorInput +from flask_wtf.file import FileAllowed + +import re +from wtforms import ValidationError + +# def hex_color_only(form, field): +# if not re.match(r'^#(?:[0-9a-fA-F]{3}){1,2}$', field.data): +# raise ValidationError('Debe ser un color hexadecimal válido (#RRGGBB o #RGB).') + +class Carousel(FlaskForm): + # id = HiddenField() # Descomenta si lo necesitas + + img = FileField( + 'Imagen de fondo: ', + validators=[ + DataRequired(), + FileAllowed(['jpg', 'jpeg', 'png', 'mp4'], 'Solo se permiten archivos .jpg, .png o .mp4') + ] + ) + + + bg_color = StringField('Color de fondo del texto: ', widget=ColorInput(), validators=[DataRequired()]) + + txt_color = StringField('Color del texto: ', widget=ColorInput(), validators=[DataRequired()]) + + txt = StringField('Texto: ', validators=[DataRequired()]) + + diff --git a/forms_py/functions.py b/forms_py/functions.py index 1361284..ecba75d 100644 --- a/forms_py/functions.py +++ b/forms_py/functions.py @@ -32,7 +32,8 @@ v = { 'edit_post': 'h_tmp_usr/e_edit_post.html', 'change_pswd': 'h_tmp_usr/f_change_pswd.html', 'metrics': 'h_tmp_usr/g_metrics.html', - 'manage_profiles': 'h_tmp_usr/h_manage_profiles.html' + 'manage_profiles': 'h_tmp_usr/h_manage_profiles.html', + 'carousel': 'h_tmp_usr/i_carousel_form.html' } } @@ -137,3 +138,20 @@ def get_date_n_time(timestamp: datetime) -> dict: } return obj +def hex_to_rgb(hex_color): + # Elimina el símbolo '#' si está presente + hex_color = hex_color.lstrip('#') + + # Convierte los pares de caracteres hexadecimales a enteros + if len(hex_color) == 6: + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + return (r, g, b) + else: + raise ValueError("El color HEX debe tener 6 dígitos.") + + +def rgba_to_string(rgba): + r, g, b, a = rgba + return f"rgba({r}, {g}, {b}, {a})" diff --git a/main.py b/main.py index ad5abbc..4bea124 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,15 @@ from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, get_jwt, decode_token, verify_jwt_in_request, set_access_cookies, get_csrf_token from flask_mail import Message, Mail from threading import Thread -from flask import Flask, render_template, redirect, url_for, flash, current_app, request, jsonify, make_response +from flask import Flask, render_template, redirect, url_for, flash, current_app, request, jsonify, make_response, send_from_directory, abort from forms_py.cls_form_contact import ContactForm from forms_py.cls_form_add_usr import AddUser from forms_py.cls_form_login import LogIn from forms_py.cls_db import DBContact from forms_py.cls_db_usr import DBForma from forms_py.cls_change_pswd import ChangePwsd -from forms_py.functions import db_conf_obj, generar_contrasena, hash_password, v, l_flash_msj, saludo_hr, cur_date, min_read_pst, get_date_n_time, getRandomId +from forms_py.cls_form_carousel import Carousel +from forms_py.functions import db_conf_obj, generar_contrasena, hash_password, v, l_flash_msj, saludo_hr, cur_date, min_read_pst, get_date_n_time, getRandomId, hex_to_rgb, rgba_to_string from forms_py.cls_recover_pswd import RecoverPswd import os from datetime import datetime, timedelta, timezone @@ -21,6 +22,7 @@ from bs4 import BeautifulSoup from functools import wraps from flask_caching import Cache + app = Flask(__name__) cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': '/tmp/flask-formha'}) @@ -126,9 +128,18 @@ def validate_user_exists(f): # ################################################################## @app.route('/') -@cache.cached(timeout=3600) +# @cache.cached(timeout=3600) def home(): - return render_template(v['home'], active_page='home') + q = """ + SELECT + id, img_name, bg_color, txt_color, txt, TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada + FROM + carousel + ORDER BY + id DESC; + """ + data = dbUsers.get_all_data(q) + return render_template(v['home'], active_page='home', data=data) @app.route('/about-us') @cache.cached(timeout=43200) @@ -892,12 +903,70 @@ def update_user(): t = (f_nombre, f_apellido, f_genero, f_email, f_isAdmin, id_usr) dbUsers.update_data(q, t) - res = {'ok': True, 'message': 'El elemento se actualizó correctamente', "id": "id_usr"} return jsonify(res), 200 + +@app.route('/user/carousel', methods=['GET', 'POST']) +@jwt_required() +@validate_user_exists +@admin_required +def carousel(): + form = Carousel() + mnsj_flash = None + + q_all_slides = """ + SELECT + id, img_name, bg_color, txt_color, txt, TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada + FROM + carousel + ORDER BY + id DESC; + """ + + data = dbUsers.get_all_data(q_all_slides) + + if request.method == 'POST' and form.validate_on_submit(): + # img + image_file = form.img.data + filename = secure_filename(image_file.filename) + + prev_img = dbUsers.get_all_data("SELECT img_name FROM carousel;") + lst_img = [ele[0] for ele in prev_img] + + # validar que el archivo no se haya cargado previamente + if filename in lst_img: + mnsj_flash = l_flash_msj('Error Archivo', 'La imagen ya ha sido cargada previamente.', 'error') + else: + image_file.save(f'./static/uploads/{filename}') + bg_color = rgba_to_string((*hex_to_rgb(form.bg_color.data), 0.5)) + txt_color = rgba_to_string((*hex_to_rgb(form.txt_color.data), 1)) + txt = form.txt.data + + q = "INSERT INTO carousel (img_name, bg_color, txt_color, txt) VALUES (%s, %s, %s, %s);" + t = (filename, bg_color, txt_color, txt) + dbUsers.update_data(q, t) + mnsj_flash = l_flash_msj('Datos Slide', f'Datos agregados para imagen: {filename}', 'success') + + flash(mnsj_flash) + return redirect(url_for('carousel')) + + return render_template(v['tmp_user']['carousel'], form=form, data=data, active_page='metrics') + + +@app.route('/user/carousel/download/') +@jwt_required() +@validate_user_exists +@admin_required +def download_file(filename): + uploads_dir = os.path.join(app.root_path, 'static', 'uploads') + try: + return send_from_directory(uploads_dir, filename, as_attachment=True) + except FileNotFoundError: + abort(404) + # ------------------------------------------------------------- # MANEJO DE ERRORES DE JWT diff --git a/static/a_home/home.css b/static/a_home/home.css index b12ca25..53a71e3 100644 --- a/static/a_home/home.css +++ b/static/a_home/home.css @@ -1,99 +1,5 @@ /* Variables para consistencia */ -:root { - --primary-color: #00acc1; - --shadow-light: 0 3px 4px rgba(0, 0, 0, 0.4); - --shadow-hover: 0 15px 26px rgba(0, 0, 0, 0.5); - --card-radius: 5px; - --image-height: 250px; - --banner-text-color: #ffff; - --banner-overlay: rgba(116, 118, 119, 0.8); -} -/* Estructura principal */ -/* main { - width: 95vw; - min-height: 100vh; -} */ - -/* ---------------------------- */ -/* Fila 1 - Banner con efecto de zoom */ -.r-i { - width: 100%; -} - -.r-i-ii { - background-image: url('/static/y_img/home/reunion.avif'); - background-size: 100%; - background-repeat: no-repeat; - background-position: center; - border-radius: 10px; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - transition: background-size 2s ease-in-out; - height: 35vh; /* Valor por defecto para móviles */ -} - -.r-i-ii:hover { - background-size: 110%; /* Efecto de zoom mantenido */ -} - -.r-i-ii spam { - background-color: var(--banner-overlay); - padding: 20px; - border-radius: 10px; - text-align: center; - font-size: clamp(1rem, 3vw, 25px); /* Texto responsivo */ - color: var(--banner-text-color); - max-width: 80%; -} - -/* ---------------------------- */ -/* Fila 2 - Tarjetas con efectos */ -.r-ii { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 30px; - margin-top: 1em; -} - -.card-flyer { - min-height: 100%; - background: #fff; - border-radius: var(--card-radius); - box-shadow: var(--shadow-light); - transition: transform 0.3s, box-shadow 0.3s; - overflow: hidden; -} - -.image-box img { - width: 100%; - height: var(--image-height); - object-fit: cover; - transition: transform 0.9s; -} - -.text-container { - padding: 20px; - text-align: center; -} - -.text-container h6 { - margin: 0; - font-size: 18px; - color: var(--primary-color); -} - -/* Efectos hover (completos) */ -.card-flyer:hover { - transform: translateY(-10px); - box-shadow: var(--shadow-hover); -} - -.card-flyer:hover img { - transform: scale(1.15); /* Efecto de escala mantenido */ -} /* ---------------------------- */ /* Media Queries optimizadas */ @@ -101,9 +7,7 @@ main { width: 90vw; } - .r-i-ii { - height: 35vh; - } + } @media (min-width: 1024px) { @@ -111,9 +15,7 @@ width: 85vw; margin: 20px auto; } - .r-i-ii { - height: 45vh; - } + } @media (min-width: 1440px) { @@ -121,20 +23,14 @@ width: 80vw; margin: 2em auto; } - .r-i-ii { - height: 50vh; - } + } @media (min-width: 1920px) { - .r-i-ii { - height: 60vh; - } + } /* Ajuste específico para texto en móviles */ @media (max-width: 767px) { - .r-i-ii spam { - font-size: 1em; - } + } \ No newline at end of file diff --git a/static/h_tmp_user/i_carousel_form/carousel_form.js b/static/h_tmp_user/i_carousel_form/carousel_form.js new file mode 100644 index 0000000..8fe7a91 --- /dev/null +++ b/static/h_tmp_user/i_carousel_form/carousel_form.js @@ -0,0 +1,18 @@ +import { simpleNotification } from '../../z_comps/notify.js'; + + +let form = document.querySelector("form"); +let img = document.getElementById("img"); + +form.addEventListener("submit", function (e) { + let allowedExtensions = ['jpg', 'jpeg', 'png', 'mp4']; + let filePath = img.value; + let extension = filePath.split('.').pop().toLowerCase(); + + if (!allowedExtensions.includes(extension)) { + e.preventDefault(); + simpleNotification("Error Extensión", "Solo se permiten archivos con extensión: .jpg, .jpeg, .png, .mp4", "error") + return; + } + +}); \ No newline at end of file diff --git a/templates/a_home/home.html b/templates/a_home/home.html index f1084f0..d0ddb9e 100644 --- a/templates/a_home/home.html +++ b/templates/a_home/home.html @@ -10,96 +10,69 @@ {% block body %} -
-
- - Potenciamos el talento con soluciones personalizadas que alinean personas, información y propósito. Diseñamos - estrategias basadas en información, ordenada, confiable, disponible y segura, dando FORMHä a tus decisiones. - -
-
- -
- -
-
- Software -
-
-
- Actualización del proceso de reclutamiento y selección. -
-

- Actualmente desde la AI hasta las técnicas de entrevista, han abierto un nuevo horizonte en el marco del - reclutamiento y selección de personal… -

-
-
- - -
-
- Productividad -
-
-
- Beneficios e impacto en la productividad. -
-

- En la medida que adoptemos la NOM 035, factores de riesgos psicosocial en el trabajo, identificación y - prevención; como una guía para optimizar el clima y… -

-
-
- - -
-
- Ley Silla -
-
-
- Alcances y objetivos de la Ley Silla. -
-

- El pasado 19 de diciembre de 2024, se publico en el Diario Oficial de la Federación el decreto por el cual - se reforman y adicionan diversas… -

-
-
-
+