diff --git a/clase1/Milton_Chiluisa b/clase1/Milton_Chiluisa.py similarity index 100% rename from clase1/Milton_Chiluisa rename to clase1/Milton_Chiluisa.py diff --git a/clase1/edisontanav.py b/clase1/edisontanav.py new file mode 100644 index 0000000..07617de --- /dev/null +++ b/clase1/edisontanav.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# tarea1.py +# +# Copyright 2025 leontv + +def agregarnombre(): + cadnombre = input("por favor, ingrese su nombre: ") + return cadnombre + +def agregaredad(): + iedad = input("por favor, ingrese su edad: ") + return iedad + +def agregartalla(): # en metros + ftalla = input("por favor, ingrese su talla (en metros): ") + return float(ftalla) + +def agregarpeso(): # en metros + fpeso = input("por favor, ingrese su peso (en kilogramos): ") + return float(fpeso) + +def calculaIMC( talla, peso): + fvalor = peso / talla ** 2 + return float( fvalor ) + +def resultadoIMC( valor ): + if valor >= 30: + print("obesidad") + elif valor >=25 or valor <30: + print("sobrepeso") + elif valor >18.5 or valor <25: + print("sobrepeso") + else: + print("obesidad") + +if __name__ == '__main__': + print("Progrma para halla el IMC de un ser humano") + nombre= agregarnombre() + edad = agregaredad() + peso = agregarpeso() + talla = agregartalla() + valorimc = calculaIMC( talla, peso) + print(f"Su IMC es {valorimc:.2f}") + resultadoIMC( valorimc ) + diff --git a/clase1/jose_campoverde.py b/clase1/jose_campoverde.py new file mode 100644 index 0000000..e69de29 diff --git a/clase2/edisontanav.py b/clase2/edisontanav.py new file mode 100644 index 0000000..c46a1d7 --- /dev/null +++ b/clase2/edisontanav.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# +# -*- coding: utf-8 -*- +# edisontanav.py +# +# Copyright 2025 Leo +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +""" +Crea un clase producto +""" + +class Producto: + + def __init__(self, cadcodigo, cadnombre, fprecio: float, istock: int): + """ + Crea un constructor para clase producto + """ + self.cadcodigo = cadcodigo + self.cadnombre = cadnombre + self.fprecio = fprecio + self.istock = istock + + def mostrarexistencia( self): + """ + Muestra la información del producto + """ + print(f"Codigo: {self.cadcodigo}, Nombre: {self.cadnombre}, Precio: {self.fprecio:.2f}, Stock: {self.istock}") + + def generarventa( self, icantidad: int ): + self.icantidad = icantidad + if self.icantidad <= self.istock: + ftotal = self.icantidad * self.fprecio + self.istock = self.istock - self.icantidad + resp = ftotal + else: + print("no se dispone del producto") + resp = 0 + return resp + + def actualizarstock(self, icantidad: int): + """ + Actualiza el stok de productos, la cantida puede ser positiva o negativa + """ + self.icantidad = icantidad + self.istock = self.istock + self.icantidad + return self.istock + +class Tienda: + def __init__(self, cadnombretienda): + """ + Crea un constructor para clase producto + """ + self.cadnombretienda = cadnombretienda + self.productos = [] + + def agregaproductos(self, productos: Producto): + self.productos.append(productos) + + def listar_productos(self): + """ + Lista los productos en la tienda + """ + print(f"Tienda: {self.cadnombretienda}") + print("codigo,producto,precio,stock") + for producto in self.productos: + print(f"{producto.cadcodigo},{producto.cadnombre},{producto.fprecio},{producto.istock}") + + def venderproducto(self, codigoproducto): + """ + Vende un producto de la tienda + """ + for producto in self.productos: + if producto.cadcodigo == codigoproducto: + self.productos.remove(producto) + print(f"Producto vendido: {producto.cadcodigo}") + return + print(f"Producto '{codigoproducto}' no encontrado en tienda.") + +if __name__ == '__main__': + print("Programa para simular productos de una tienda") + producto1 = Producto("A100", "Arroz", 12.5, 200) + producto2 = Producto("A101", "Aceite", 1.5, 100) + #print(producto1.cadnombre) + producto1.mostrarexistencia() + producto1.actualizarstock(100) + producto1.mostrarexistencia() + venta = 10 + print(f"Se vendió {venta} cantidades del producto: {producto1.cadnombre} \n \tSu Valor total de: $ {producto1.generarventa(venta):.2f}") # cantidad en numero enteros + producto1.mostrarexistencia() + + # Parte 2 + #Crea un objeto de tipo Tienda + tienda1 = Tienda("GeoQuito S.A.S") + tienda1.agregaproductos(producto1) + tienda1.agregaproductos(producto2) + print("-------------------") + print("REPORTE PRODUCTOS") + print("-------------------") + tienda1.listar_productos() + tienda1.venderproducto("A100") + print("-------------------") + print("REPORTE DE VENDIDOS") + print("-------------------") + tienda1.listar_productos() + print("Gracias por usar este programa") + +# SALIDA DEL PROGRAMA +# Programa para simular productos de una tienda +# Codigo: 100, Nombre: Arroz, Precio: 12.50, Stock: 200 +# Codigo: 100, Nombre: Arroz, Precio: 12.50, Stock: 300 +# Se vendió 10 cantidades del producto: Arroz + # Su Valor total de: $ 125.00 +# Codigo: 100, Nombre: Arroz, Precio: 12.50, Stock: 290 +# ------------------- +# REPORTE PRODUCTOS +# ------------------- +# Tienda: GeoQuito S.A.S +# codigo,producto,precio,stock +# 100,Arroz,12.5,290 +# 101,Aceite,1.5,100 +# Producto vendido: 100 +# ------------------- +# REPORTE DE VENDIDOS +# ------------------- +# Tienda: GeoQuito S.A.S +# codigo,producto,precio,stock +# 101,Aceite,1.5,100 diff --git a/clase2/gabriel_rivera.py b/clase2/gabriel_rivera.py new file mode 100644 index 0000000..b944bcb --- /dev/null +++ b/clase2/gabriel_rivera.py @@ -0,0 +1,180 @@ +# Sistema Básico de Tienda - Programación Orientada a Objetos +# Clase 2 +# Versión: 1.00 +# Autor: Gabriel Rivera +# Descripción: Sistema para gestionar productos y ventas en una tienda + +class Producto: + """ + Clase que representa un producto en la tienda. + + Attributes: + nombre (str): Nombre del producto + precio (float): Precio unitario del producto + stock (int): Cantidad disponible en inventario + """ + + def __init__(self, nombre, precio, stock): + """ + Inicializa un nuevo producto. + + Args: + nombre (str): Nombre del producto + precio (float): Precio unitario + stock (int): Cantidad inicial en stock + """ + self.nombre = nombre + self.precio = precio + self.stock = stock + + def mostrar_info(self): + """ + Muestra la información completa del producto. + """ + print(f"{self.nombre} | Precio: ${self.precio:.2f} | Stock: {self.stock}") + + def actualizar_stock(self, cantidad): + """ + Actualiza el stock del producto. + + Args: + cantidad (int): Cantidad a sumar (positiva) o restar (negativa) + """ + self.stock += cantidad + print(f"Stock de {self.nombre} actualizado a: {self.stock}") + + def vender(self, cantidad): + """ + Procesa la venta de una cantidad específica del producto. + + Args: + cantidad (int): Cantidad a vender + + Returns: + float or str: Total a pagar si hay stock suficiente, + mensaje de error si no hay stock + """ + if cantidad <= self.stock: + self.stock -= cantidad + total = cantidad * self.precio + return total + else: + return f"Error: No hay suficiente stock. Stock disponible: {self.stock}" + + +class Tienda: + """ + Clase que representa una tienda que gestiona múltiples productos. + + Attributes: + nombre (str): Nombre de la tienda + productos (list): Lista de objetos Producto + """ + + def __init__(self, nombre): + """ + Inicializa una nueva tienda. + + Args: + nombre (str): Nombre de la tienda + """ + self.nombre = nombre + self.productos = [] + + def agregar_producto(self, producto): + """ + Agrega un producto a la tienda. + + Args: + producto (Producto): Objeto producto a agregar + """ + self.productos.append(producto) + print(f"Producto '{producto.nombre}' agregado a {self.nombre}") + + def listar_productos(self): + """ + Lista todos los productos disponibles en la tienda. + """ + print(f"\nTienda: {self.nombre}") + print("\nProductos disponibles:") + print("-" * 40) + + if not self.productos: + print("No hay productos disponibles.") + else: + for producto in self.productos: + producto.mostrar_info() + + def vender_producto(self, nombre_producto, cantidad): + """ + Vende una cantidad específica de un producto. + + Args: + nombre_producto (str): Nombre del producto a vender + cantidad (int): Cantidad a vender + """ + # Buscar el producto por nombre + producto_encontrado = None + for producto in self.productos: + if producto.nombre.lower() == nombre_producto.lower(): + producto_encontrado = producto + break + + if producto_encontrado is None: + print(f"Error: Producto '{nombre_producto}' no encontrado en la tienda.") + return + + print(f"\nVendiendo {cantidad} {nombre_producto}...") + resultado = producto_encontrado.vender(cantidad) + + if isinstance(resultado, float): + print(f"Total a pagar: ${resultado:.2f}") + print(f"\nStock actualizado:") + print(f"{producto_encontrado.nombre} | Stock: {producto_encontrado.stock}") + else: + print(resultado) + + +# Ejemplo de uso del sistema +def main(): + """ + Función principal que demuestra el uso del sistema de tienda. + """ + # Crear tienda + tienda = Tienda("Super Market") + + # Crear productos + pan = Producto("Pan", 0.50, 20) + jugo = Producto("Jugo", 1.25, 10) + leche = Producto("Leche", 2.00, 5) + + # Agregar productos a la tienda + tienda.agregar_producto(pan) + tienda.agregar_producto(jugo) + tienda.agregar_producto(leche) + + # Listar productos disponibles + tienda.listar_productos() + + # Realizar ventas + tienda.vender_producto("Jugo", 3) + + # Intentar vender más cantidad de la disponible + print("\n" + "="*50) + tienda.vender_producto("Leche", 7) + + # Actualizar stock manualmente + print("\n" + "="*50) + leche.actualizar_stock(10) + + # Vender después de actualizar stock + tienda.vender_producto("Leche", 7) + + # Mostrar estado final + print("\n" + "="*50) + print("Estado final de la tienda:") + tienda.listar_productos() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/clase2/jose_campoverde.py b/clase2/jose_campoverde.py new file mode 100644 index 0000000..c330785 --- /dev/null +++ b/clase2/jose_campoverde.py @@ -0,0 +1,71 @@ +# /////////////////////////// Parte 1: Clase Producto /////////////////////////// +class Producto: + def __init__(self, nombre, precio, stock): + self.nombre = nombre + self.precio = precio + self.stock = stock + + def mostrar_info(self): + print(f"{self.nombre} | Precio: ${self.precio:.2f} | Stock: {self.stock}") + + def actualizar_stock(self, cantidad): + self.stock += cantidad + + def vender(self, cantidad): + if self.stock >= cantidad: + total = self.precio * cantidad + self.stock -= cantidad + return total + else: + return f"No hay suficiente stock de {self.nombre} (Stock disponible: {self.stock})" + + +# ///////////////////////////Parte 2: Clase Tienda /////////////////////////// +class Tienda: + def __init__(self, nombre): + self.nombre = nombre + self.productos = [] #Creo una lista vacia donde se van a almacenar los productos + + def agregar_producto(self, producto): + self.productos.append(producto) #añade un objeto Producto a la lista productos + + def listar_productos(self): + print(f"\nTienda: {self.nombre}") + print("\nProductos disponibles:\n") + for producto in self.productos: + producto.mostrar_info() # lista todos los productos + + def vender_producto(self, nombre_producto, cantidad): + for producto in self.productos: #busca un producto por nombre para la venta + if producto.nombre.lower() == nombre_producto.lower(): # lower compara sin importar mayusculas + resultado = producto.vender(cantidad) # llamo al metodo vender + if isinstance(resultado, float): # si la venta tubo exito + print(f"\nVendiendo {cantidad} {producto.nombre.lower()}(s)... Total a pagar: ${resultado:.2f}") + else: + print("\n" + resultado) + return + print(f"\nProducto '{nombre_producto}' no encontrado en la tienda.") + +# /////////////////////////// PRPRODUCTOS /////////////////////////// +if __name__ == "__main__": + # Crear productos + pan = Producto("Pan", 0.50, 20) + jugo = Producto("Jugo", 1.25, 10) + leche = Producto("Leche", 2.75, 9) + + # Crear tienda y agregar productos + tienda = Tienda("Tuti") + tienda.agregar_producto(pan) + tienda.agregar_producto(jugo) + tienda.agregar_producto(leche) + + # Listar productos + tienda.listar_productos() + + # Vender producto (3 unidades) + tienda.vender_producto("Jugo", 3) + tienda.vender_producto("leche", 2) + + # Mostrar stock actualizado despues de la venta + print("\nStock actualizado:\n") + jugo.mostrar_info() \ No newline at end of file diff --git a/clase2/walter_nunez.py b/clase2/walter_nunez.py new file mode 100644 index 0000000..72f78fd --- /dev/null +++ b/clase2/walter_nunez.py @@ -0,0 +1,64 @@ +class Producto: + def __init__(self, nombre, precio, stock): + self.nombre = nombre + self.precio = precio + self.stock = stock + + def mostrar_info(self): + print(f"{self.nombre} | Precio: ${self.precio:.2f} | Stock: {self.stock}") + + def actualizar_stock(self, cantidad): + self.stock += cantidad + + def vender(self, cantidad): + if cantidad <= self.stock: + self.stock -= cantidad + total = self.precio * cantidad + return total + else: + return f"No hay suficiente stock de {self.nombre}. Stock disponible: {self.stock}" + + +class Tienda: + def __init__(self, nombre): + self.nombre = nombre + self.productos = [] + + def agregar_producto(self, producto): + self.productos.append(producto) + + def listar_productos(self): + print("\nProductos disponibles:") + for producto in self.productos: + producto.mostrar_info() + + def vender_producto(self, nombre_producto, cantidad): + for producto in self.productos: + if producto.nombre.lower() == nombre_producto.lower(): + resultado = producto.vender(cantidad) + if isinstance(resultado, float): + print(f"\nVendiendo {cantidad} {producto.nombre}(s)...") + print(f"Total a pagar: ${resultado:.2f}") + else: + print(resultado) + return + print(f"Producto '{nombre_producto}' no encontrado.") + + +# Ejemplo de uso +if __name__ == "__main__": + tienda = Tienda("Super Market") + + pan = Producto("Pan", 0.50, 20) + jugo = Producto("Jugo", 1.25, 10) + + tienda.agregar_producto(pan) + tienda.agregar_producto(jugo) + + print(f"Tienda: {tienda.nombre}") + tienda.listar_productos() + + tienda.vender_producto("Jugo", 3) + + print("\nStock actualizado:") + jugo.mostrar_info() diff --git a/clase3/carlos_bodero/app.py b/clase3/carlos_bodero/app.py new file mode 100644 index 0000000..3b2f526 --- /dev/null +++ b/clase3/carlos_bodero/app.py @@ -0,0 +1,48 @@ +from flask import Flask, render_template, request, redirect, url_for + +app = Flask(__name__) + +# Lista en memoria, cada alumno tiene un id único +productos = [ + {'id': 1, 'nombre': 'martillo', 'precio': 20, 'categoria':'herramienta'}, + {'id': 2, 'nombre': 'Pinzas', 'precio': 21, 'categoria': 'herramienta'}, + {'id': 3, 'nombre': 'Alicate', 'precio': 22, 'categoria': 'herramienta'} +] +_next_id = 4 + +@app.route('/') +def lista_productos(): + return render_template('productos.html', productos=productos) + +@app.route('/agregar', methods=['GET', 'POST']) +def agregar_producto(): + global _next_id + if request.method == 'POST': + nombre = request.form['nombre'] + precio = int(request.form['precio']) + categoria = request.form['categoria'] + productos.append({'id': _next_id, 'nombre': nombre, 'precio': precio, 'categoria': categoria}) + _next_id += 1 + return redirect(url_for('lista_productos')) + return render_template('agregar.html') + +@app.route('/editar/', methods=['GET', 'POST']) +def editar_producto(id): + producto = next((a for a in productos if a['id'] == id), None) + if not producto: + return "Producto no encontrado", 404 + if request.method == 'POST': + producto['nombre'] = request.form['nombre'] + producto['precio'] = int(request.form['precio']) + producto['categoria'] = request.form['categoria'] + return redirect(url_for('lista_productos')) + return render_template('editar.html', producto=producto) + +@app.route('/eliminar/') +def eliminar_producto(id): + global productos + producto = [a for a in productos if a['id'] != id] + return redirect(url_for('lista_productos')) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/clase3/carlos_bodero/templates/agregar.html b/clase3/carlos_bodero/templates/agregar.html new file mode 100644 index 0000000..c7748c1 --- /dev/null +++ b/clase3/carlos_bodero/templates/agregar.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block title %}Agregar Producto{% endblock %} +{% block content %} +

Agregar Producto

+
+

+

+

+ +
+{% endblock %} \ No newline at end of file diff --git a/clase3/carlos_bodero/templates/base.html b/clase3/carlos_bodero/templates/base.html new file mode 100644 index 0000000..bb41284 --- /dev/null +++ b/clase3/carlos_bodero/templates/base.html @@ -0,0 +1,28 @@ + + + + + {% block title %}Productos{% endblock %} + + + + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/clase3/carlos_bodero/templates/editar.html b/clase3/carlos_bodero/templates/editar.html new file mode 100644 index 0000000..c8a7715 --- /dev/null +++ b/clase3/carlos_bodero/templates/editar.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block title %}Editar Producto{% endblock %} +{% block content %} +

Editar Producto

+
+

+

+

+ +
+{% endblock %} \ No newline at end of file diff --git a/clase3/carlos_bodero/templates/productos.html b/clase3/carlos_bodero/templates/productos.html new file mode 100644 index 0000000..d508d2e --- /dev/null +++ b/clase3/carlos_bodero/templates/productos.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %}Lista de Productos{% endblock %} +{% block content %} +

Lista de Productos

+ + + + + + + + + + + + {% for producto in productos %} + + + + + + + + {% endfor %} + +
IDNombrePrecioCategoriaAcciones
{{ producto.id }}{{ producto.nombre }}{{ producto.precio }}{{ producto.categoria }} + Editar + Eliminar +
+{% endblock %} diff --git a/clase3/flask_crud_usuarios/app.db b/clase3/flask_crud_usuarios/app.db new file mode 100644 index 0000000..beb559d Binary files /dev/null and b/clase3/flask_crud_usuarios/app.db differ diff --git a/clase3/flask_crud_usuarios/app/__init__.py b/clase3/flask_crud_usuarios/app/__init__.py new file mode 100644 index 0000000..f78781d --- /dev/null +++ b/clase3/flask_crud_usuarios/app/__init__.py @@ -0,0 +1,17 @@ +from flask import Flask +from config import Config +from .extensions import db, migrate + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) + + # Inicializar extensiones + db.init_app(app) + migrate.init_app(app, db) + + # Registrar blueprints + from app.routes import init_routes + init_routes(app) + + return app \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/extensions.py b/clase3/flask_crud_usuarios/app/extensions.py new file mode 100644 index 0000000..de1947d --- /dev/null +++ b/clase3/flask_crud_usuarios/app/extensions.py @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +db = SQLAlchemy() +migrate = Migrate() \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/models/__init__.py b/clase3/flask_crud_usuarios/app/models/__init__.py new file mode 100644 index 0000000..361987d --- /dev/null +++ b/clase3/flask_crud_usuarios/app/models/__init__.py @@ -0,0 +1,3 @@ +from .user import User + +__all__ = ['User'] \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/models/product.py b/clase3/flask_crud_usuarios/app/models/product.py new file mode 100644 index 0000000..3eaf6e9 --- /dev/null +++ b/clase3/flask_crud_usuarios/app/models/product.py @@ -0,0 +1,16 @@ +from datetime import datetime +from app.extensions import db + +class Product(db.Model): + __tablename__ = 'products' + + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String(100), nullable=False) + descripcion = db.Column(db.Text) + precio = db.Column(db.Float, nullable=False) + stock = db.Column(db.Integer, default=0) + categoria = db.Column(db.String(50)) + fecha_creacion = db.Column(db.DateTime, default=datetime.now) + imagen = db.Column(db.String(255)) + def __repr__(self): + return f'' \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/models/user.py b/clase3/flask_crud_usuarios/app/models/user.py new file mode 100644 index 0000000..e33d03e --- /dev/null +++ b/clase3/flask_crud_usuarios/app/models/user.py @@ -0,0 +1,15 @@ +from datetime import datetime +from app.extensions import db + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String(100), nullable=False) + correo = db.Column(db.String(100), unique=True, nullable=False) + telefono = db.Column(db.String(20)) + fecha_nacimiento = db.Column(db.Date) + fecha_registro = db.Column(db.DateTime, default=datetime.now) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/routes/__init__.py b/clase3/flask_crud_usuarios/app/routes/__init__.py new file mode 100644 index 0000000..ceea0ad --- /dev/null +++ b/clase3/flask_crud_usuarios/app/routes/__init__.py @@ -0,0 +1,8 @@ +def init_routes(app): + from .index import bp as index_bp + from .users import bp as users_bp + from .products import bp as products_bp + + app.register_blueprint(index_bp) + app.register_blueprint(users_bp) + app.register_blueprint(products_bp) \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/routes/index.py b/clase3/flask_crud_usuarios/app/routes/index.py new file mode 100644 index 0000000..6a9e5e7 --- /dev/null +++ b/clase3/flask_crud_usuarios/app/routes/index.py @@ -0,0 +1,7 @@ +from flask import Blueprint, render_template + +bp = Blueprint('index', __name__) + +@bp.route('/') +def home(): + return render_template('home.html') \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/routes/products.py b/clase3/flask_crud_usuarios/app/routes/products.py new file mode 100644 index 0000000..01174bb --- /dev/null +++ b/clase3/flask_crud_usuarios/app/routes/products.py @@ -0,0 +1,66 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from app.models.product import Product +from app.extensions import db +from datetime import datetime + +bp = Blueprint('products', __name__, url_prefix='/productos') + +@bp.route('/') +def listar(): + productos = Product.query.order_by(Product.fecha_creacion.desc()).all() + return render_template('products/listar.html', productos=productos) + +@bp.route('/crear', methods=['GET', 'POST']) +def crear(): + if request.method == 'POST': + try: + producto = Product( + nombre=request.form['nombre'], + descripcion=request.form['descripcion'], + precio=float(request.form['precio']), + stock=int(request.form['stock']), + categoria=request.form['categoria'] + ) + db.session.add(producto) + db.session.commit() + flash('Producto creado exitosamente', 'success') + return redirect(url_for('products.listar')) + except Exception as e: + db.session.rollback() + flash(f'Error al crear producto: {str(e)}', 'danger') + + return render_template('products/crear.html') + +@bp.route('/editar/', methods=['GET', 'POST']) +def editar(id): + producto = Product.query.get_or_404(id) + + if request.method == 'POST': + try: + producto.nombre = request.form['nombre'] + producto.descripcion = request.form['descripcion'] + producto.precio = float(request.form['precio']) + producto.stock = int(request.form['stock']) + producto.categoria = request.form['categoria'] + + db.session.commit() + flash('Producto actualizado exitosamente', 'success') + return redirect(url_for('products.listar')) + except Exception as e: + db.session.rollback() + flash(f'Error al actualizar producto: {str(e)}', 'danger') + + return render_template('products/editar.html', producto=producto) + +@bp.route('/eliminar/', methods=['POST']) +def eliminar(id): + producto = Product.query.get_or_404(id) + try: + db.session.delete(producto) + db.session.commit() + flash('Producto eliminado exitosamente', 'success') + except Exception as e: + db.session.rollback() + flash(f'Error al eliminar producto: {str(e)}', 'danger') + + return redirect(url_for('products.listar')) \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/routes/users.py b/clase3/flask_crud_usuarios/app/routes/users.py new file mode 100644 index 0000000..2c1e3b8 --- /dev/null +++ b/clase3/flask_crud_usuarios/app/routes/users.py @@ -0,0 +1,95 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from datetime import datetime +from app.extensions import db +from app.models.user import User + +bp = Blueprint('users', __name__, url_prefix='/usuarios') + +@bp.route('/usuarios') +def listar(): + usuarios = User.query.all() + return render_template('usuarios.html', usuarios=usuarios, datetime=datetime) + +@bp.route('/usuarios/crear', methods=['GET', 'POST']) +def crear_usuario(): + if request.method == 'POST': + nombre = request.form['nombre'].strip() + correo = request.form['correo'].strip() + telefono = request.form.get('telefono', '').strip() + fecha_nacimiento_str = request.form.get('fecha_nacimiento') + + try: + fecha_nacimiento = datetime.strptime(fecha_nacimiento_str, '%Y-%m-%d').date() if fecha_nacimiento_str else None + except ValueError: + flash('Formato de fecha inválido. Use YYYY-MM-DD', 'error') + return redirect(url_for('users.crear_usuario')) + + # Validaciones + if not nombre or not correo: + flash('Nombre y correo son campos obligatorios', 'error') + return redirect(url_for('users.crear_usuario')) + + if User.query.filter_by(correo=correo).first(): + flash('Este correo ya está registrado', 'error') + return redirect(url_for('users.crear_usuario')) + + nuevo_usuario = User( + nombre=nombre, + correo=correo, + telefono=telefono, + fecha_nacimiento=fecha_nacimiento + ) + + db.session.add(nuevo_usuario) + db.session.commit() # Importante al cambiar datos + + flash('User creado exitosamente', 'success') + return redirect(url_for('users.crear_usuario')) + + return render_template('form.html', accion='Crear', usuario=None) + +@bp.route('/usuarios/editar/', methods=['GET', 'POST']) +def editar_usuario(id): + usuario = User.query.get_or_404(id) + + if request.method == 'POST': + nombre = request.form['nombre'].strip() + correo = request.form['correo'].strip() + telefono = request.form.get('telefono', '').strip() + fecha_nacimiento_str = request.form.get('fecha_nacimiento') + + try: + fecha_nacimiento = datetime.strptime(fecha_nacimiento_str, '%Y-%m-%d').date() if fecha_nacimiento_str else None + except ValueError: + flash('Formato de fecha inválido. Use YYYY-MM-DD', 'error') + return redirect(url_for('users.editar_usuario', id=id)) + + # Validaciones + if not nombre or not correo: + flash('Nombre y correo son campos obligatorios', 'error') + return redirect(url_for('users.editar_usuario', id=id)) + + if correo != usuario.correo and User.query.filter_by(correo=correo).first(): + flash('Este correo ya está registrado', 'error') + return redirect(url_for('users.editar_usuario', id=id)) + + usuario.nombre = nombre + usuario.correo = correo + usuario.telefono = telefono + usuario.fecha_nacimiento = fecha_nacimiento + + db.session.commit() + + flash('User actualizado exitosamente', 'success') + return redirect(url_for('users.crear_usuario')) + + return render_template('form.html', accion='Editar', usuario=usuario) + +@bp.route('/usuarios/eliminar/', methods=['POST']) +def eliminar_usuario(id): + usuario = User.query.get_or_404(id) + db.session.delete(usuario) + db.session.commit() + + flash('User eliminado exitosamente', 'success') + return redirect(url_for('users.crear_usuario')) \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/templates/base.html b/clase3/flask_crud_usuarios/app/templates/base.html new file mode 100644 index 0000000..d02a8c3 --- /dev/null +++ b/clase3/flask_crud_usuarios/app/templates/base.html @@ -0,0 +1,51 @@ + + + + + + + CRUD Flask + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/templates/form.html b/clase3/flask_crud_usuarios/app/templates/form.html new file mode 100644 index 0000000..841ba9b --- /dev/null +++ b/clase3/flask_crud_usuarios/app/templates/form.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} +

{{ accion }} Usuario

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + Cancelar +
+{% endblock %} \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/templates/home.html b/clase3/flask_crud_usuarios/app/templates/home.html similarity index 100% rename from clase3/flask_crud_usuarios/templates/home.html rename to clase3/flask_crud_usuarios/app/templates/home.html diff --git a/clase3/flask_crud_usuarios/templates/productos.html b/clase3/flask_crud_usuarios/app/templates/productos.html similarity index 100% rename from clase3/flask_crud_usuarios/templates/productos.html rename to clase3/flask_crud_usuarios/app/templates/productos.html diff --git a/clase3/flask_crud_usuarios/app/templates/products/crear.html b/clase3/flask_crud_usuarios/app/templates/products/crear.html new file mode 100644 index 0000000..89af797 --- /dev/null +++ b/clase3/flask_crud_usuarios/app/templates/products/crear.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +

Crear Producto

+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Cancelar +
+{% endblock %} \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/templates/products/editar.html b/clase3/flask_crud_usuarios/app/templates/products/editar.html new file mode 100644 index 0000000..96e2ec8 --- /dev/null +++ b/clase3/flask_crud_usuarios/app/templates/products/editar.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} +

Editar Producto

+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Cancelar +
+{% endblock %} \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/templates/products/listar.html b/clase3/flask_crud_usuarios/app/templates/products/listar.html new file mode 100644 index 0000000..71f711b --- /dev/null +++ b/clase3/flask_crud_usuarios/app/templates/products/listar.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block content %} +

Lista de Productos

+ + + Crear Producto + + + {% if productos %} +
+ + + + + + + + + + + + + + {% for producto in productos %} + + + + + + + + + + {% endfor %} + +
IDNombreDescripciónPrecioStockCategoríaAcciones
{{ producto.id }}{{ producto.nombre }}{{ producto.descripcion|truncate(50) }}${{ "{:,.2f}".format(producto.precio) }}{{ producto.stock }}{{ producto.categoria }} +
+ + Editar + +
+ +
+
+
+
+ {% else %} +
No hay productos registrados.
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/app/templates/usuarios.html b/clase3/flask_crud_usuarios/app/templates/usuarios.html new file mode 100644 index 0000000..93b28c4 --- /dev/null +++ b/clase3/flask_crud_usuarios/app/templates/usuarios.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block content %} +

Lista de Usuarios

+ + + Crear Usuario + + + {% if usuarios %} +
+ + + + + + + + + + + + + + {% for usuario in usuarios %} + + + + + + + + + + {% endfor %} + +
IDNombreCorreoTeléfonoEdadRegistroAcciones
{{ usuario.id }}{{ usuario.nombre }}{{ usuario.correo }}{{ usuario.telefono if usuario.telefono else '-' }} + {% if usuario.fecha_nacimiento %} + {{ (datetime.now().date() - usuario.fecha_nacimiento).days // 365 }} años + {% else %} + - + {% endif %} + {{ usuario.fecha_registro.strftime('%d/%m/%Y') }} +
+ + Editar + +
+ +
+
+
+
+ {% else %} +
No hay usuarios registrados.
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/config.py b/clase3/flask_crud_usuarios/config.py new file mode 100644 index 0000000..d934c4e --- /dev/null +++ b/clase3/flask_crud_usuarios/config.py @@ -0,0 +1,11 @@ +import os +from dotenv import load_dotenv + +basedir = os.path.abspath(os.path.dirname(__file__)) +load_dotenv(os.path.join(basedir, '.env')) + +class Config: + SECRET_KEY = os.getenv('SECRET_KEY') or 'dev-key-segura' + SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'app.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/migrations/README b/clase3/flask_crud_usuarios/migrations/README new file mode 100644 index 0000000..e69de29 diff --git a/clase3/flask_crud_usuarios/migrations/alembic.ini b/clase3/flask_crud_usuarios/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/clase3/flask_crud_usuarios/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/clase3/flask_crud_usuarios/migrations/env.py b/clase3/flask_crud_usuarios/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/clase3/flask_crud_usuarios/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/clase3/flask_crud_usuarios/migrations/script.py.mako b/clase3/flask_crud_usuarios/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/clase3/flask_crud_usuarios/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/clase3/flask_crud_usuarios/migrations/versions/e42a8f8817ca_initial_migrate.py b/clase3/flask_crud_usuarios/migrations/versions/e42a8f8817ca_initial_migrate.py new file mode 100644 index 0000000..39f6fd2 --- /dev/null +++ b/clase3/flask_crud_usuarios/migrations/versions/e42a8f8817ca_initial_migrate.py @@ -0,0 +1,49 @@ +"""Initial migrate + +Revision ID: e42a8f8817ca +Revises: +Create Date: 2025-06-27 18:53:03.470737 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e42a8f8817ca' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('products', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('nombre', sa.String(length=100), nullable=False), + sa.Column('descripcion', sa.Text(), nullable=True), + sa.Column('precio', sa.Float(), nullable=False), + sa.Column('stock', sa.Integer(), nullable=True), + sa.Column('categoria', sa.String(length=50), nullable=True), + sa.Column('fecha_creacion', sa.DateTime(), nullable=True), + sa.Column('imagen', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('nombre', sa.String(length=100), nullable=False), + sa.Column('correo', sa.String(length=100), nullable=False), + sa.Column('telefono', sa.String(length=20), nullable=True), + sa.Column('fecha_nacimiento', sa.Date(), nullable=True), + sa.Column('fecha_registro', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('correo') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users') + op.drop_table('products') + # ### end Alembic commands ### diff --git a/clase3/flask_crud_usuarios/run.py b/clase3/flask_crud_usuarios/run.py new file mode 100644 index 0000000..f518ba8 --- /dev/null +++ b/clase3/flask_crud_usuarios/run.py @@ -0,0 +1,9 @@ +from app import create_app +from flask_migrate import Migrate +from app import db + +app = create_app() +migrate = Migrate(app, db) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/clase3/gabriel_rivera/app,py b/clase3/gabriel_rivera/app,py new file mode 100644 index 0000000..699df4f --- /dev/null +++ b/clase3/gabriel_rivera/app,py @@ -0,0 +1,213 @@ +# CRUD de Productos con Flask - Tarea Clase 3 +# Versión: 1.01 +# Autor: Gabriel Rivera +# Descripción: Aplicación Flask con templates separados + +from flask import Flask, render_template, request, redirect, url_for, flash + +# Inicializar la aplicación Flask +app = Flask(__name__) +app.secret_key = 'clave_secreta_para_sesiones' + +# Lista global para almacenar productos en memoria +productos = [ + {'id': 1, 'nombre': 'Laptop', 'precio': 999.99, 'cantidad': 5}, + {'id': 2, 'nombre': 'Mouse', 'precio': 25.50, 'cantidad': 15}, + {'id': 3, 'nombre': 'Teclado', 'precio': 45.00, 'cantidad': 8} +] + +# Variable global para generar IDs únicos +next_id = 4 + + +def encontrar_producto_por_id(producto_id): + """ + Encuentra un producto por su ID. + + Args: + producto_id (int): ID del producto a buscar + + Returns: + dict or None: Producto encontrado o None si no existe + """ + for producto in productos: + if producto['id'] == producto_id: + return producto + return None + + +def generar_nuevo_id(): + """ + Genera un nuevo ID único para productos. + + Returns: + int: Nuevo ID único + """ + global next_id + nuevo_id = next_id + next_id += 1 + return nuevo_id + + +@app.route('/') +def index(): + """Ruta principal que redirige a la lista de productos.""" + return redirect(url_for('listar_productos')) + + +@app.route('/productos') +def listar_productos(): + """Ruta para mostrar todos los productos.""" + return render_template('productos_lista.html', productos=productos) + + +@app.route('/productos/nuevo', methods=['GET', 'POST']) +def nuevo_producto(): + """Ruta para crear un nuevo producto.""" + if request.method == 'POST': + # Obtener datos del formulario + nombre = request.form.get('nombre', '').strip() + precio = request.form.get('precio', '') + cantidad = request.form.get('cantidad', '') + + # Validaciones + if not nombre: + flash('El nombre del producto es obligatorio.', 'error') + return render_template('productos_form.html', producto=None, accion='nuevo') + + try: + precio = float(precio) + cantidad = int(cantidad) + + if precio < 0: + flash('El precio no puede ser negativo.', 'error') + return render_template('productos_form.html', producto=None, accion='nuevo') + + if cantidad < 0: + flash('La cantidad no puede ser negativa.', 'error') + return render_template('productos_form.html', producto=None, accion='nuevo') + + except ValueError: + flash('Precio y cantidad deben ser números válidos.', 'error') + return render_template('productos_form.html', producto=None, accion='nuevo') + + # Verificar si ya existe un producto con el mismo nombre + for producto in productos: + if producto['nombre'].lower() == nombre.lower(): + flash('Ya existe un producto con ese nombre.', 'error') + return render_template('productos_form.html', producto=None, accion='nuevo') + + # Crear nuevo producto + nuevo_producto = { + 'id': generar_nuevo_id(), + 'nombre': nombre, + 'precio': precio, + 'cantidad': cantidad + } + + productos.append(nuevo_producto) + flash(f'Producto "{nombre}" creado exitosamente.', 'success') + return redirect(url_for('listar_productos')) + + # GET request - mostrar formulario + return render_template('productos_form.html', producto=None, accion='nuevo') + + +@app.route('/productos/editar/', methods=['GET', 'POST']) +def editar_producto(id): + """Ruta para editar un producto existente.""" + producto = encontrar_producto_por_id(id) + + if not producto: + flash('Producto no encontrado.', 'error') + return redirect(url_for('listar_productos')) + + if request.method == 'POST': + # Obtener datos del formulario + nombre = request.form.get('nombre', '').strip() + precio = request.form.get('precio', '') + cantidad = request.form.get('cantidad', '') + + # Validaciones + if not nombre: + flash('El nombre del producto es obligatorio.', 'error') + return render_template('productos_form.html', producto=producto, accion='editar') + + try: + precio = float(precio) + cantidad = int(cantidad) + + if precio < 0: + flash('El precio no puede ser negativo.', 'error') + return render_template('productos_form.html', producto=producto, accion='editar') + + if cantidad < 0: + flash('La cantidad no puede ser negativa.', 'error') + return render_template('productos_form.html', producto=producto, accion='editar') + + except ValueError: + flash('Precio y cantidad deben ser números válidos.', 'error') + return render_template('productos_form.html', producto=producto, accion='editar') + + # Verificar si ya existe otro producto con el mismo nombre + for p in productos: + if p['id'] != id and p['nombre'].lower() == nombre.lower(): + flash('Ya existe otro producto con ese nombre.', 'error') + return render_template('productos_form.html', producto=producto, accion='editar') + + # Actualizar producto + producto['nombre'] = nombre + producto['precio'] = precio + producto['cantidad'] = cantidad + + flash(f'Producto "{nombre}" actualizado exitosamente.', 'success') + return redirect(url_for('listar_productos')) + + # GET request - mostrar formulario con datos actuales + return render_template('productos_form.html', producto=producto, accion='editar') + + +@app.route('/productos/eliminar/') +def eliminar_producto(id): + """Ruta para eliminar un producto.""" + producto = encontrar_producto_por_id(id) + + if not producto: + flash('Producto no encontrado.', 'error') + else: + nombre_producto = producto['nombre'] + productos.remove(producto) + flash(f'Producto "{nombre_producto}" eliminado exitosamente.', 'success') + + return redirect(url_for('listar_productos')) + + +@app.errorhandler(404) +def page_not_found(e): + """Manejo de errores 404.""" + return render_template('404.html'), 404 + + +if __name__ == '__main__': + print("🚀 Iniciando aplicación Flask...") + print("📍 Accede a: http://127.0.0.1:5000") + print("📋 Rutas disponibles:") + print(" • /productos - Lista de productos") + print(" • /productos/nuevo - Crear producto") + print(" • /productos/editar/ - Editar producto") + print(" • /productos/eliminar/ - Eliminar producto") + print("-" * 50) + print("📁 Estructura de archivos necesaria:") + print(" mi_proyecto/") + print(" ├── app.py") + print(" ├── templates/") + print(" │ ├── base.html") + print(" │ ├── productos_lista.html") + print(" │ ├── productos_form.html") + print(" │ └── 404.html") + print(" └── static/") + print(" └── css/") + print(" └── style.css") + print("-" * 50) + + app.run(debug=True, host='127.0.0.1', port=5000) \ No newline at end of file diff --git a/clase3/gabriel_rivera/static/css/style.css b/clase3/gabriel_rivera/static/css/style.css new file mode 100644 index 0000000..52392ba --- /dev/null +++ b/clase3/gabriel_rivera/static/css/style.css @@ -0,0 +1,96 @@ +body { + font-family: Arial, sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: #f5f5f5; +} + +.container { + background-color: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +h1, h2 { + color: #333; + text-align: center; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; +} + +th, td { + border: 1px solid #ddd; + padding: 12px; + text-align: left; +} + +th { + background-color: #4CAF50; + color: white; +} + +tr:nth-child(even) { + background-color: #f2f2f2; +} + +.btn { + display: inline-block; + padding: 8px 16px; + margin: 5px; + text-decoration: none; + border-radius: 4px; + cursor: pointer; + border: none; + font-size: 14px; +} + +.btn-primary { background-color: #007bff; color: white; } +.btn-success { background-color: #28a745; color: white; } +.btn-warning { background-color: #ffc107; color: black; } +.btn-danger { background-color: #dc3545; color: white; } + +.btn:hover { opacity: 0.8; } + +.form-group { margin-bottom: 15px; } + +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="text"], input[type="number"] { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; +} + +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.text-center { text-align: center; } +.mt-3 { margin-top: 20px; } \ No newline at end of file diff --git a/clase3/gabriel_rivera/templates/404.html b/clase3/gabriel_rivera/templates/404.html new file mode 100644 index 0000000..9779807 --- /dev/null +++ b/clase3/gabriel_rivera/templates/404.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}Página No Encontrada{% endblock %} + +{% block content %} +
+

❌ Página No Encontrada

+

La página que buscas no existe.

+ 🏠 Volver al Inicio +
+{% endblock %} \ No newline at end of file diff --git a/clase3/gabriel_rivera/templates/base.html b/clase3/gabriel_rivera/templates/base.html new file mode 100644 index 0000000..56c1b96 --- /dev/null +++ b/clase3/gabriel_rivera/templates/base.html @@ -0,0 +1,26 @@ + + + + + + {% block title %}CRUD Productos{% endblock %} + + + +
+

🛒 Sistema de Gestión de Productos

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/clase3/gabriel_rivera/templates/productos_form.html b/clase3/gabriel_rivera/templates/productos_form.html new file mode 100644 index 0000000..c88210c --- /dev/null +++ b/clase3/gabriel_rivera/templates/productos_form.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %}{{ 'Editar' if accion == 'editar' else 'Nuevo' }} Producto{% endblock %} + +{% block content %} +

{{ '✏️ Editar' if accion == 'editar' else '➕ Nuevo' }} Producto

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 🔙 Cancelar +
+
+{% endblock %} \ No newline at end of file diff --git a/clase3/gabriel_rivera/templates/productos_lista.html b/clase3/gabriel_rivera/templates/productos_lista.html new file mode 100644 index 0000000..7d43a1f --- /dev/null +++ b/clase3/gabriel_rivera/templates/productos_lista.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block title %}Lista de Productos{% endblock %} + +{% block content %} +

📋 Lista de Productos

+ + + +{% if productos %} + + + + + + + + + + + + {% for producto in productos %} + + + + + + + + {% endfor %} + +
IDNombrePrecioCantidadAcciones
{{ producto.id }}{{ producto.nombre }}${{ "%.2f"|format(producto.precio) }}{{ producto.cantidad }} + ✏️ Editar + 🗑️ Eliminar +
+{% else %} +
+

No hay productos registrados.

+
+{% endif %} + +
+

Total de productos: {{ productos|length }}

+
+{% endblock %} \ No newline at end of file diff --git a/clase3/george_penafiel_alvarado/app.py b/clase3/george_penafiel_alvarado/app.py index 926ee17..610a8b6 100644 --- a/clase3/george_penafiel_alvarado/app.py +++ b/clase3/george_penafiel_alvarado/app.py @@ -1,13 +1,42 @@ from flask import Flask, render_template, request, redirect, url_for, flash +from flask_sqlalchemy import SQLAlchemy +import os app = Flask(__name__) app.secret_key = 'un_api_key' -base_de_datos_usuarios = [] +# Configuración de la base de datos SQLite +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(app.instance_path, 'mi_aplicacion.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -def imprimir_usuarios(): - print("\n--- LISTA ACTUAL DE USUARIOS ---") - print("-------------------------------\n") +db = SQLAlchemy(app) + +# Asegurarse de que el directorio de la instancia exista +with app.app_context(): + if not os.path.exists(app.instance_path): + os.makedirs(app.instance_path) + +# Definición de Modelos +class Usuario(db.Model): + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String(80), nullable=False) + correo = db.Column(db.String(120), unique=True, nullable=False) + + def __repr__(self): + return f'' + +class Producto(db.Model): + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String(80), nullable=False) + descripcion = db.Column(db.String(200), nullable=False) + precio = db.Column(db.Float, nullable=False) + + def __repr__(self): + return f'' + +# Crear las tablas en la base de datos (se ejecuta solo si no existen) +with app.app_context(): + db.create_all() @app.route('/') def home(): @@ -15,12 +44,73 @@ def home(): @app.route('/productos') def productos(): - return render_template('productos.html') + productos = Producto.query.all() + return render_template('productos.html', productos=productos) + +@app.route('/productos/crear', methods=['GET', 'POST']) +def crear_producto(): + if request.method == 'POST': + nombre = request.form['nombre'].strip() + descripcion = request.form['descripcion'].strip() + precio_str = request.form['precio'].strip() + + try: + precio = float(precio_str) + except ValueError: + flash('El precio debe ser un número válido.', 'error') + return redirect(url_for('crear_producto')) + + if nombre and descripcion and precio_str: + nuevo_producto = Producto(nombre=nombre, descripcion=descripcion, precio=precio) + db.session.add(nuevo_producto) + db.session.commit() + flash('Producto creado con éxito.', 'success') + return redirect(url_for('productos')) + else: + flash('Todos los campos son obligatorios.', 'error') + return redirect(url_for('crear_producto')) + return render_template('form_producto.html', accion='Crear', producto={}) + +@app.route('/productos/editar/', methods=['GET', 'POST']) +def editar_producto(id): + producto = Producto.query.get_or_404(id) + + if request.method == 'POST': + nombre = request.form['nombre'].strip() + descripcion = request.form['descripcion'].strip() + precio_str = request.form['precio'].strip() + + try: + precio = float(precio_str) + except ValueError: + flash('El precio debe ser un número válido.', 'error') + return redirect(url_for('editar_producto', id=producto.id)) + + if nombre and descripcion and precio_str: + producto.nombre = nombre + producto.descripcion = descripcion + producto.precio = precio + db.session.commit() + flash('Producto editado con éxito.', 'success') + return redirect(url_for('productos')) + else: + flash('Todos los campos son obligatorios.', 'error') + return redirect(url_for('editar_producto', id=producto.id)) + + return render_template('form_producto.html', accion='Editar', producto=producto, indice=producto.id) + +@app.route('/productos/eliminar/') +def eliminar_producto(id): + producto = Producto.query.get_or_404(id) + db.session.delete(producto) + db.session.commit() + flash('Producto eliminado con éxito.', 'success') + return redirect(url_for('productos')) @app.route('/usuarios') def usuarios(): - imprimir_usuarios() - return render_template('usuarios.html', usuarios=base_de_datos_usuarios) + usuarios = Usuario.query.all() + return render_template('usuarios.html', usuarios=usuarios) @app.route('/usuarios/crear', methods=['GET', 'POST']) def crear_usuario(): @@ -29,9 +119,10 @@ def crear_usuario(): correo = request.form['correo'].strip() if nombre and correo: - base_de_datos_usuarios.append({'nombre': nombre, 'correo': correo}) + nuevo_usuario = Usuario(nombre=nombre, correo=correo) + db.session.add(nuevo_usuario) + db.session.commit() flash('Usuario creado con éxito.', 'success') - imprimir_usuarios() return redirect(url_for('usuarios')) else: flash('Todos los campos son obligatorios.', 'error') @@ -39,47 +130,33 @@ def crear_usuario(): return render_template('form.html', accion='Crear', usuario={}) -@app.route('/usuarios/editar/', methods=['GET', 'POST']) -def editar_usuario(indice): - if indice >= len(base_de_datos_usuarios): - flash('Índice de usuario inválido.', 'error') - return redirect(url_for('usuarios')) - - usuario = base_de_datos_usuarios[indice] +@app.route('/usuarios/editar/', methods=['GET', 'POST']) +def editar_usuario(id): + usuario = Usuario.query.get_or_404(id) if request.method == 'POST': nombre = request.form['nombre'].strip() correo = request.form['correo'].strip() if nombre and correo: - print(f"\nEditando usuario #{indice + 1}:") - print(f"Antes: Nombre: {usuario['nombre']}, Correo: {usuario['correo']}") - usuario['nombre'] = nombre - usuario['correo'] = correo - print(f"Después: Nombre: {nombre}, Correo: {correo}") - imprimir_usuarios() + usuario.nombre = nombre + usuario.correo = correo + db.session.commit() flash('Usuario editado con éxito.', 'success') return redirect(url_for('usuarios')) else: flash('Todos los campos son obligatorios.', 'error') - return redirect(url_for('editar_usuario', indice=indice)) - - return render_template('form.html', accion='Editar', usuario=usuario, indice=indice) - -@app.route('/usuarios/eliminar/') -def eliminar_usuario(indice): - if 0 <= indice < len(base_de_datos_usuarios): - usuario_eliminado = base_de_datos_usuarios[indice] - print(f"\nEliminando usuario #{indice + 1}:") - print(f"Usuario eliminado: Nombre: {usuario_eliminado['nombre']}, Correo: {usuario_eliminado['correo']}") - base_de_datos_usuarios.pop(indice) - imprimir_usuarios() - flash('Usuario eliminado con éxito.', 'success') - else: - flash('Índice inválido.', 'error') + return redirect(url_for('editar_usuario', id=usuario.id)) + + return render_template('form.html', accion='Editar', usuario=usuario, indice=usuario.id) + +@app.route('/usuarios/eliminar/') +def eliminar_usuario(id): + usuario = Usuario.query.get_or_404(id) + db.session.delete(usuario) + db.session.commit() + flash('Usuario eliminado con éxito.', 'success') return redirect(url_for('usuarios')) if __name__ == '__main__': - import warnings - warnings.filterwarnings("ignore", message="This is a development server.") app.run(debug=True) \ No newline at end of file diff --git a/clase3/george_penafiel_alvarado/templates/form_producto.html b/clase3/george_penafiel_alvarado/templates/form_producto.html new file mode 100644 index 0000000..a7547e3 --- /dev/null +++ b/clase3/george_penafiel_alvarado/templates/form_producto.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

{{ accion }} Producto

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + Cancelar +
+{% endblock %} \ No newline at end of file diff --git a/clase3/george_penafiel_alvarado/templates/productos.html b/clase3/george_penafiel_alvarado/templates/productos.html index 774d516..0364fbe 100644 --- a/clase3/george_penafiel_alvarado/templates/productos.html +++ b/clase3/george_penafiel_alvarado/templates/productos.html @@ -1,46 +1,37 @@ {% extends "base.html" %} {% block content %} -
-

Nuestro Catálogo de Productos

-

Explora nuestra selección de productos. Puedes añadir más información aquí.

-
+

Nuestro Catálogo de Productos

-
-
-
-
-
Producto A
-

Descripción breve del Producto A. Ideal para X propósito.

- $19.99 - Ver detalles -
-
-
-
-
-
-
Producto B
-

Descripción breve del Producto B. Perfecto para Y uso.

- $25.50 - Ver detalles -
-
-
-
-
-
-
Producto C
-

Descripción breve del Producto C. La mejor opción para Z.

- $9.75 - Ver detalles -
-
-
-
+ Crear Producto -
-

¿Quieres añadir un producto? ¡Esta es tu oportunidad para expandir la aplicación!

- Volver al inicio -
+ {% if productos %} + + + + + + + + + + + + {% for producto in productos %} + + + + + + + + {% endfor %} + +
#NombreDescripciónPrecioAcciones
{{ loop.index }}{{ producto.nombre }}{{ producto.descripcion }}${{ "%.2f" | format(producto.precio) }} + Editar + Eliminar +
+ {% else %} +
No hay productos registrados.
+ {% endif %} {% endblock %} \ No newline at end of file diff --git a/clase3/george_penafiel_alvarado/templates/usuarios.html b/clase3/george_penafiel_alvarado/templates/usuarios.html index 6901ecc..56df0c0 100644 --- a/clase3/george_penafiel_alvarado/templates/usuarios.html +++ b/clase3/george_penafiel_alvarado/templates/usuarios.html @@ -22,8 +22,8 @@

Lista de Usuarios

{{ usuario.nombre }} {{ usuario.correo }} - Editar - Eliminar + Editar + Eliminar {% endfor %} diff --git a/clase3/index.py b/clase3/index.py index 79e83a1..863dd32 100644 --- a/clase3/index.py +++ b/clase3/index.py @@ -13,6 +13,9 @@ # 5. mkdir flask_crud_usuarios​ # 6. cd flask_crud_usuarios​ # 5. Ejecutar app: flask run +# pip freeze > requirements.txt +# pip3 install -r requirements.txt +# deactivate from flask import Flask diff --git a/clase3/jose_campoverde/app.py b/clase3/jose_campoverde/app.py new file mode 100644 index 0000000..4a29e85 --- /dev/null +++ b/clase3/jose_campoverde/app.py @@ -0,0 +1,90 @@ +from flask import Flask, render_template, request, redirect, url_for, flash + +app = Flask(__name__) +app.secret_key = 'clave_secreta_segura_123' # Cambia esta clave por una secreta + +productos = [] +contador_id = 1 + + +@app.route('/') +def home(): + return redirect(url_for('mostrar_productos')) + + +@app.route('/productos') +def mostrar_productos(): + return render_template('mostrar_productos.html', productos=productos) + + +@app.route('/productos/crear', methods=['GET', 'POST']) +def crear_producto(): + global contador_id + if request.method == 'POST': + nombre = request.form['nombre'] + precio = request.form['precio'] + cantidad = request.form['cantidad'] + + try: + precio = float(precio) + cantidad = int(cantidad) + except ValueError: + flash('Precio o cantidad inválidos.') + return redirect(url_for('crear_producto')) + + productos.append({ + 'id': contador_id, + 'nombre': nombre, + 'precio': precio, + 'cantidad': cantidad + }) + contador_id += 1 + flash(f'Producto "{nombre}" creado correctamente.') + return redirect(url_for('mostrar_productos')) + + return render_template('crear_producto.html') + + +@app.route('/productos/actualizar/', methods=['GET', 'POST']) +def actualizar_producto(id): + producto = next((p for p in productos if p['id'] == id), None) + if not producto: + flash('Producto no encontrado.') + return redirect(url_for('mostrar_productos')) + + if request.method == 'POST': + nombre = request.form['nombre'] + precio = request.form['precio'] + cantidad = request.form['cantidad'] + + try: + precio = float(precio) + cantidad = int(cantidad) + except ValueError: + flash('Precio o cantidad inválidos.') + return redirect(url_for('actualizar_producto', id=id)) + + producto['nombre'] = nombre + producto['precio'] = precio + producto['cantidad'] = cantidad + + flash(f'Producto "{nombre}" actualizado correctamente.') + return redirect(url_for('mostrar_productos')) + + return render_template('actualizar_producto.html', producto=producto) + + +@app.route('/productos/borrar/', methods=['POST']) +def borrar_producto(id): + global productos + producto = next((p for p in productos if p['id'] == id), None) + if producto: + productos = [p for p in productos if p['id'] != id] + flash(f'Producto "{producto["nombre"]}" eliminado correctamente.') + else: + flash('Producto no encontrado.') + return redirect(url_for('mostrar_productos')) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/clase3/jose_campoverde/templates/actualizar_producto.html b/clase3/jose_campoverde/templates/actualizar_producto.html new file mode 100644 index 0000000..7f6b702 --- /dev/null +++ b/clase3/jose_campoverde/templates/actualizar_producto.html @@ -0,0 +1,29 @@ + +
+ + {% block content %} +

Actualizar producto

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + Cancelar +
+ {% endblock %} + +
+ diff --git a/clase3/jose_campoverde/templates/crear_producto.html b/clase3/jose_campoverde/templates/crear_producto.html new file mode 100644 index 0000000..5360bf1 --- /dev/null +++ b/clase3/jose_campoverde/templates/crear_producto.html @@ -0,0 +1,45 @@ +{% extends 'index.html' %} +{% block content %} +

Crear producto

+
+
+ + +
+ Por favor ingresa un nombre. +
+
+
+ + +
+ Por favor ingresa un precio válido. +
+
+
+ + +
+ Por favor ingresa una cantidad válida. +
+
+ +
+ + +{% endblock %} diff --git a/clase3/jose_campoverde/templates/index.html b/clase3/jose_campoverde/templates/index.html new file mode 100644 index 0000000..8a435e3 --- /dev/null +++ b/clase3/jose_campoverde/templates/index.html @@ -0,0 +1,35 @@ + + + + + CRUD Productos + + + + +
+

CRUD de Productos

+ + + + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + + \ No newline at end of file diff --git a/clase3/jose_campoverde/templates/mostrar_productos.html b/clase3/jose_campoverde/templates/mostrar_productos.html new file mode 100644 index 0000000..de993e3 --- /dev/null +++ b/clase3/jose_campoverde/templates/mostrar_productos.html @@ -0,0 +1,31 @@ +{% extends 'index.html' %} +{% block content %} +

Lista de productos

+ + + + + + + + + + + + {% for p in productos %} + + + + + + + + {% endfor %} + +
IDNombrePrecioCantidadAcciones
{{ p.id }}{{ p.nombre }}${{ "%.2f"|format(p.precio) }}{{ p.cantidad }} + Editar +
+ +
+
+{% endblock %} diff --git a/clase3/readme.md b/clase3/readme.md index 4571ab4..5feb933 100644 --- a/clase3/readme.md +++ b/clase3/readme.md @@ -1,34 +1,81 @@ -# 🛒 Tarea Clase 3 – CRUD de Productos con Flask (sin Base de Datos) +# 🚀 CRUD de Productos y Usuarios con Flask y SQLite -## 🎯 Objetivo +## 📌 Tabla de Contenidos +- [Requisitos](#-requisitos) +- [Configuración](#-configuración) +- [Estructura del Proyecto](#-estructura-del-proyecto) +- [Funcionalidades](#-funcionalidades) +- [Modelos de Datos](#-modelos-de-datos) +- [Ejecución](#-ejecución) +- [Migraciones](#-migraciones) -Crear una aplicación web utilizando **Flask** que permita realizar operaciones básicas de un CRUD (**Crear, Leer, Actualizar, Eliminar**) sobre una lista de productos **sin usar base de datos**. La información se almacenará temporalmente en memoria usando una lista de Python. +## 📋 Requisitos ---- +### Prerrequisitos +- Python 3.8+ +- Pip (Gestor de paquetes de Python) -## 📋 Requisitos +### Dependencias + +```bash +pip install flask flask-sqlalchemy flask-migrate +``` -- Python 3.x -- Flask (instalar con `pip install flask`) -- Uso de listas para simular una base de datos +# Linux/Mac +export FLASK_APP=run.py +export FLASK_ENV=development -Cada producto debe tener: -- `nombre` (str) -- `precio` (float) -- `cantidad` (int) +# Windows +set FLASK_APP=run.py +set FLASK_ENV=development ---- +## 📁 Estructura del Proyecto +```bash +flask_crud_usuarios/ +├── app/ +│ ├── __init__.py +│ ├── models/ +│ │ ├── user.py +│ │ └── product.py +│ ├── routes/ +│ │ ├── users.py +│ │ └── products.py +│ └── templates/ +│ ├── products/ +│ │ ├── listar.html +│ │ ├── crear.html +│ │ └── editar.html +│ └── base.html +├── migrations/ +├── config.py +└── run.py + +``` ## 🛠️ Funcionalidades -| Ruta | Descripción | -|------|-------------| -| `/productos` | Mostrar todos los productos | -| `/productos/nuevo` | Formulario para crear un nuevo producto | -| `/productos/editar/` | Formulario para editar un producto existente | -| `/productos/eliminar/` | Eliminar un producto existente | +Productos +Endpoint Método Descripción +/productos GET Listar productos +/productos/crear GET/POST Crear producto +/productos/editar/ GET/POST Editar producto +/productos/eliminar/ POST Eliminar producto +Usuarios +Endpoint Método Descripción +/usuarios GET Listar usuarios +/usuarios/crear GET/POST Crear usuario +/usuarios/editar/ GET/POST Editar usuario + +## 🚀 Ejecución ---- +```bash +flask run +``` -## 📁 Estructura sugerida del proyecto +## 🔄 Migraciones +```bash +flask db init +flask db migrate -m "Descripción de cambios" +flask db upgrade +``` diff --git a/clase3/santiago_calvopina_crud_deber/app.py b/clase3/santiago_calvopina_crud_deber/app.py new file mode 100644 index 0000000..4a29e85 --- /dev/null +++ b/clase3/santiago_calvopina_crud_deber/app.py @@ -0,0 +1,90 @@ +from flask import Flask, render_template, request, redirect, url_for, flash + +app = Flask(__name__) +app.secret_key = 'clave_secreta_segura_123' # Cambia esta clave por una secreta + +productos = [] +contador_id = 1 + + +@app.route('/') +def home(): + return redirect(url_for('mostrar_productos')) + + +@app.route('/productos') +def mostrar_productos(): + return render_template('mostrar_productos.html', productos=productos) + + +@app.route('/productos/crear', methods=['GET', 'POST']) +def crear_producto(): + global contador_id + if request.method == 'POST': + nombre = request.form['nombre'] + precio = request.form['precio'] + cantidad = request.form['cantidad'] + + try: + precio = float(precio) + cantidad = int(cantidad) + except ValueError: + flash('Precio o cantidad inválidos.') + return redirect(url_for('crear_producto')) + + productos.append({ + 'id': contador_id, + 'nombre': nombre, + 'precio': precio, + 'cantidad': cantidad + }) + contador_id += 1 + flash(f'Producto "{nombre}" creado correctamente.') + return redirect(url_for('mostrar_productos')) + + return render_template('crear_producto.html') + + +@app.route('/productos/actualizar/', methods=['GET', 'POST']) +def actualizar_producto(id): + producto = next((p for p in productos if p['id'] == id), None) + if not producto: + flash('Producto no encontrado.') + return redirect(url_for('mostrar_productos')) + + if request.method == 'POST': + nombre = request.form['nombre'] + precio = request.form['precio'] + cantidad = request.form['cantidad'] + + try: + precio = float(precio) + cantidad = int(cantidad) + except ValueError: + flash('Precio o cantidad inválidos.') + return redirect(url_for('actualizar_producto', id=id)) + + producto['nombre'] = nombre + producto['precio'] = precio + producto['cantidad'] = cantidad + + flash(f'Producto "{nombre}" actualizado correctamente.') + return redirect(url_for('mostrar_productos')) + + return render_template('actualizar_producto.html', producto=producto) + + +@app.route('/productos/borrar/', methods=['POST']) +def borrar_producto(id): + global productos + producto = next((p for p in productos if p['id'] == id), None) + if producto: + productos = [p for p in productos if p['id'] != id] + flash(f'Producto "{producto["nombre"]}" eliminado correctamente.') + else: + flash('Producto no encontrado.') + return redirect(url_for('mostrar_productos')) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/clase3/santiago_calvopina_crud_deber/templates/actualizar_producto.html b/clase3/santiago_calvopina_crud_deber/templates/actualizar_producto.html new file mode 100644 index 0000000..7f6b702 --- /dev/null +++ b/clase3/santiago_calvopina_crud_deber/templates/actualizar_producto.html @@ -0,0 +1,29 @@ + +
+ + {% block content %} +

Actualizar producto

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + Cancelar +
+ {% endblock %} + +
+ diff --git a/clase3/santiago_calvopina_crud_deber/templates/crear_producto.html b/clase3/santiago_calvopina_crud_deber/templates/crear_producto.html new file mode 100644 index 0000000..5360bf1 --- /dev/null +++ b/clase3/santiago_calvopina_crud_deber/templates/crear_producto.html @@ -0,0 +1,45 @@ +{% extends 'index.html' %} +{% block content %} +

Crear producto

+
+
+ + +
+ Por favor ingresa un nombre. +
+
+
+ + +
+ Por favor ingresa un precio válido. +
+
+
+ + +
+ Por favor ingresa una cantidad válida. +
+
+ +
+ + +{% endblock %} diff --git a/clase3/santiago_calvopina_crud_deber/templates/index.html b/clase3/santiago_calvopina_crud_deber/templates/index.html new file mode 100644 index 0000000..8a435e3 --- /dev/null +++ b/clase3/santiago_calvopina_crud_deber/templates/index.html @@ -0,0 +1,35 @@ + + + + + CRUD Productos + + + + +
+

CRUD de Productos

+ + + + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + + \ No newline at end of file diff --git a/clase3/santiago_calvopina_crud_deber/templates/mostrar_productos.html b/clase3/santiago_calvopina_crud_deber/templates/mostrar_productos.html new file mode 100644 index 0000000..de993e3 --- /dev/null +++ b/clase3/santiago_calvopina_crud_deber/templates/mostrar_productos.html @@ -0,0 +1,31 @@ +{% extends 'index.html' %} +{% block content %} +

Lista de productos

+ + + + + + + + + + + + {% for p in productos %} + + + + + + + + {% endfor %} + +
IDNombrePrecioCantidadAcciones
{{ p.id }}{{ p.nombre }}${{ "%.2f"|format(p.precio) }}{{ p.cantidad }} + Editar +
+ +
+
+{% endblock %} diff --git a/proyecto/carlos_bodero/app.db b/proyecto/carlos_bodero/app.db new file mode 100644 index 0000000..9b6492f Binary files /dev/null and b/proyecto/carlos_bodero/app.db differ diff --git a/proyecto/carlos_bodero/app/__init__.py b/proyecto/carlos_bodero/app/__init__.py new file mode 100644 index 0000000..5e31fb3 --- /dev/null +++ b/proyecto/carlos_bodero/app/__init__.py @@ -0,0 +1,37 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +import os + +db = SQLAlchemy() +migrate = Migrate() + + +def create_app(): + templates_dir = os.path.join(os.path.dirname(__file__), '..', 'templates') + app = Flask(__name__, template_folder=templates_dir) + + basedir = os.path.abspath(os.path.dirname(__file__)) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, '..', 'app.db') + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + db.init_app(app) + migrate.init_app(app, db) + + from .routes_productos import productos_bp + from .routes_clientes import clientes_bp + from .routes_ventas import ventas_bp + + app.register_blueprint(productos_bp) + app.register_blueprint(clientes_bp) + app.register_blueprint(ventas_bp) + + @app.route("/") + def index(): + from .models import Producto + productos = Producto.query.all() + return render_template('index.html', productos=productos) + + from flask import render_template # <- necesario para index + + return app diff --git a/proyecto/carlos_bodero/app/models.py b/proyecto/carlos_bodero/app/models.py new file mode 100644 index 0000000..b841a50 --- /dev/null +++ b/proyecto/carlos_bodero/app/models.py @@ -0,0 +1,20 @@ +from . import db + +class Producto(db.Model): + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String(100), nullable=False) + precio = db.Column(db.Float, nullable=False) + cantidad = db.Column(db.Integer, nullable=False) + +class Cliente(db.Model): + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String(100), nullable=False) + telefono = db.Column(db.String(20), nullable=False) + +class Venta(db.Model): + id = db.Column(db.Integer, primary_key=True) + cliente_id = db.Column(db.Integer, db.ForeignKey('cliente.id')) + producto_id = db.Column(db.Integer, db.ForeignKey('producto.id')) + + cliente = db.relationship("Cliente", backref="ventas") + producto = db.relationship("Producto", backref="ventas") diff --git a/proyecto/carlos_bodero/app/routes_clientes.py b/proyecto/carlos_bodero/app/routes_clientes.py new file mode 100644 index 0000000..762c4a7 --- /dev/null +++ b/proyecto/carlos_bodero/app/routes_clientes.py @@ -0,0 +1,37 @@ +# app/routes_clientes.py +from flask import Blueprint, render_template, request, redirect, url_for +from .models import Cliente +from . import db + +clientes_bp = Blueprint('clientes', __name__, url_prefix='/clientes') + +@clientes_bp.route('/') +def lista(): + clientes = Cliente.query.all() + return render_template('clientes.html', clientes=clientes) + +@clientes_bp.route('/agregar', methods=['GET', 'POST']) +def agregar(): + if request.method == 'POST': + c = Cliente(nombre=request.form['nombre'], telefono=request.form['telefono']) + db.session.add(c) + db.session.commit() + return redirect(url_for('clientes.lista')) + return render_template('cliente_form.html', action="Agregar") + +@clientes_bp.route('/editar/', methods=['GET', 'POST']) +def editar(id): + c = Cliente.query.get_or_404(id) + if request.method == 'POST': + c.nombre = request.form['nombre'] + c.telefono = request.form['telefono'] + db.session.commit() + return redirect(url_for('clientes.lista')) + return render_template('cliente_form.html', action="Editar", cliente=c) + +@clientes_bp.route('/eliminar/') +def eliminar(id): + c = Cliente.query.get_or_404(id) + db.session.delete(c) + db.session.commit() + return redirect(url_for('clientes.lista')) \ No newline at end of file diff --git a/proyecto/carlos_bodero/app/routes_productos.py b/proyecto/carlos_bodero/app/routes_productos.py new file mode 100644 index 0000000..cd5a479 --- /dev/null +++ b/proyecto/carlos_bodero/app/routes_productos.py @@ -0,0 +1,37 @@ +from flask import Blueprint, render_template, request, redirect, url_for +from .models import Producto +from . import db + +productos_bp = Blueprint('productos', __name__, url_prefix='/productos') + +@productos_bp.route('/') +def lista(): + productos = Producto.query.all() + return render_template('productos.html', productos=productos) + +@productos_bp.route('/agregar', methods=['GET', 'POST']) +def agregar(): + if request.method == 'POST': + p = Producto(nombre=request.form['nombre'], precio=request.form['precio'], cantidad=request.form['cantidad']) + db.session.add(p) + db.session.commit() + return redirect(url_for('productos.lista')) + return render_template('producto_form.html', action="Agregar") + +@productos_bp.route('/editar/', methods=['GET', 'POST']) +def editar(id): + p = Producto.query.get_or_404(id) + if request.method == 'POST': + p.nombre = request.form['nombre'] + p.precio = request.form['precio'] + p.cantidad = request.form['cantidad'] + db.session.commit() + return redirect(url_for('productos.lista')) + return render_template('producto_form.html', action="Editar", producto=p) + +@productos_bp.route('/eliminar/') +def eliminar(id): + p = Producto.query.get_or_404(id) + db.session.delete(p) + db.session.commit() + return redirect(url_for('productos.lista')) \ No newline at end of file diff --git a/proyecto/carlos_bodero/app/routes_ventas.py b/proyecto/carlos_bodero/app/routes_ventas.py new file mode 100644 index 0000000..2197116 --- /dev/null +++ b/proyecto/carlos_bodero/app/routes_ventas.py @@ -0,0 +1,29 @@ +# app/routes_ventas.py +from flask import Blueprint, render_template, request, redirect, url_for +from .models import Venta, Cliente, Producto +from . import db + +ventas_bp = Blueprint('ventas', __name__, url_prefix='/ventas') + +@ventas_bp.route('/') +def lista(): + ventas = Venta.query.all() + return render_template('ventas.html', ventas=ventas) + +@ventas_bp.route('/agregar', methods=['GET', 'POST']) +def agregar(): + clientes = Cliente.query.all() + productos = Producto.query.all() + if request.method == 'POST': + v = Venta(cliente_id=request.form['cliente_id'], producto_id=request.form['producto_id']) + db.session.add(v) + db.session.commit() + return redirect(url_for('ventas.lista')) + return render_template('venta_form.html', clientes=clientes, productos=productos) + +@ventas_bp.route('/eliminar/') +def eliminar(id): + v = Venta.query.get_or_404(id) + db.session.delete(v) + db.session.commit() + return redirect(url_for('ventas.lista')) diff --git a/proyecto/carlos_bodero/main.py b/proyecto/carlos_bodero/main.py new file mode 100644 index 0000000..8e3c6fe --- /dev/null +++ b/proyecto/carlos_bodero/main.py @@ -0,0 +1,5 @@ +from app import create_app +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) diff --git a/proyecto/carlos_bodero/migrations/README b/proyecto/carlos_bodero/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/proyecto/carlos_bodero/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/proyecto/carlos_bodero/migrations/alembic.ini b/proyecto/carlos_bodero/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/proyecto/carlos_bodero/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/proyecto/carlos_bodero/migrations/env.py b/proyecto/carlos_bodero/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/proyecto/carlos_bodero/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/proyecto/carlos_bodero/migrations/script.py.mako b/proyecto/carlos_bodero/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/proyecto/carlos_bodero/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/proyecto/carlos_bodero/migrations/versions/ee86101cb0d8_crear_tablas_iniciales.py b/proyecto/carlos_bodero/migrations/versions/ee86101cb0d8_crear_tablas_iniciales.py new file mode 100644 index 0000000..1915a30 --- /dev/null +++ b/proyecto/carlos_bodero/migrations/versions/ee86101cb0d8_crear_tablas_iniciales.py @@ -0,0 +1,50 @@ +"""crear tablas iniciales + +Revision ID: ee86101cb0d8 +Revises: +Create Date: 2025-07-02 09:33:40.195581 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ee86101cb0d8' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('cliente', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('nombre', sa.String(length=100), nullable=False), + sa.Column('telefono', sa.String(length=20), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('producto', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('nombre', sa.String(length=100), nullable=False), + sa.Column('precio', sa.Float(), nullable=False), + sa.Column('cantidad', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('venta', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('cliente_id', sa.Integer(), nullable=True), + sa.Column('producto_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['cliente_id'], ['cliente.id'], ), + sa.ForeignKeyConstraint(['producto_id'], ['producto.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('venta') + op.drop_table('producto') + op.drop_table('cliente') + # ### end Alembic commands ### diff --git a/proyecto/carlos_bodero/requirements.txt b/proyecto/carlos_bodero/requirements.txt new file mode 100644 index 0000000..1234bc7 Binary files /dev/null and b/proyecto/carlos_bodero/requirements.txt differ diff --git a/proyecto/carlos_bodero/templates/base.html b/proyecto/carlos_bodero/templates/base.html new file mode 100644 index 0000000..70944f2 --- /dev/null +++ b/proyecto/carlos_bodero/templates/base.html @@ -0,0 +1,17 @@ + + + + + Gestión de Ventas + + + +
+ {% block content %}{% endblock %} + + diff --git a/proyecto/carlos_bodero/templates/cliente_form.html b/proyecto/carlos_bodero/templates/cliente_form.html new file mode 100644 index 0000000..298cec2 --- /dev/null +++ b/proyecto/carlos_bodero/templates/cliente_form.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +

{{ action }} Cliente

+
+ Nombre:
+ Teléfono:
+ +
+{% endblock %} diff --git a/proyecto/carlos_bodero/templates/clientes.html b/proyecto/carlos_bodero/templates/clientes.html new file mode 100644 index 0000000..f0c6221 --- /dev/null +++ b/proyecto/carlos_bodero/templates/clientes.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} +

Clientes

+Agregar Cliente + + + {% for c in clientes %} + + + + + + + {% endfor %} +
IDNombreTeléfonoAcciones
{{ c.id }}{{ c.nombre }}{{ c.telefono }} + Editar | + Eliminar +
+{% endblock %} diff --git a/proyecto/carlos_bodero/templates/index.html b/proyecto/carlos_bodero/templates/index.html new file mode 100644 index 0000000..5e6aa3e --- /dev/null +++ b/proyecto/carlos_bodero/templates/index.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block content %} +

Listado de Productos

+ + + {% for p in productos %} + + + + + + + {% endfor %} +
IDNombrePrecioCantidad
{{ p.id }}{{ p.nombre }}{{ p.precio }}{{ p.cantidad }}
+{% endblock %} diff --git a/proyecto/carlos_bodero/templates/producto_form.html b/proyecto/carlos_bodero/templates/producto_form.html new file mode 100644 index 0000000..6b2f107 --- /dev/null +++ b/proyecto/carlos_bodero/templates/producto_form.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block content %} +

{{ action }} Producto

+
+ Nombre:
+ Precio:
+ Cantidad:
+ +
+{% endblock %} diff --git a/proyecto/carlos_bodero/templates/productos.html b/proyecto/carlos_bodero/templates/productos.html new file mode 100644 index 0000000..ebc88bd --- /dev/null +++ b/proyecto/carlos_bodero/templates/productos.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block content %} +

Productos

+Agregar Producto + + + {% for p in productos %} + + + + + + + + {% endfor %} +
IDNombrePrecioCantidadAcciones
{{ p.id }}{{ p.nombre }}{{ p.precio }}{{ p.cantidad }} + Editar | + Eliminar +
+{% endblock %} diff --git a/proyecto/carlos_bodero/templates/venta_form.html b/proyecto/carlos_bodero/templates/venta_form.html new file mode 100644 index 0000000..4b52ae5 --- /dev/null +++ b/proyecto/carlos_bodero/templates/venta_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} +

Registrar Venta

+
+ Cliente: +
+ Producto: +
+ +
+{% endblock %} diff --git a/proyecto/carlos_bodero/templates/ventas.html b/proyecto/carlos_bodero/templates/ventas.html new file mode 100644 index 0000000..1a71034 --- /dev/null +++ b/proyecto/carlos_bodero/templates/ventas.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block content %} +

Ventas

+Registrar Venta + + + {% for v in ventas %} + + + + + + + {% endfor %} +
IDClienteProductoAcciones
{{ v.id }}{{ v.cliente.nombre }}{{ v.producto.nombre }} + Eliminar +
+{% endblock %} diff --git a/proyecto/george_penafielp/.web/.gitignore b/proyecto/george_penafielp/.web/.gitignore new file mode 100644 index 0000000..534bc86 --- /dev/null +++ b/proyecto/george_penafielp/.web/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +/_static + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +# DS_Store +.DS_Store \ No newline at end of file diff --git a/proyecto/george_penafielp/.web/.npmrc b/proyecto/george_penafielp/.web/.npmrc new file mode 100644 index 0000000..8feef66 --- /dev/null +++ b/proyecto/george_penafielp/.web/.npmrc @@ -0,0 +1,3 @@ + +registry=https://registry.npmjs.org +fetch-retries=0 diff --git a/proyecto/george_penafielp/.web/bunfig.toml b/proyecto/george_penafielp/.web/bunfig.toml new file mode 100644 index 0000000..123823b --- /dev/null +++ b/proyecto/george_penafielp/.web/bunfig.toml @@ -0,0 +1,3 @@ + +[install] +registry = "https://registry.npmjs.org" diff --git a/proyecto/george_penafielp/.web/components/reflex/radix_themes_color_mode_provider.js b/proyecto/george_penafielp/.web/components/reflex/radix_themes_color_mode_provider.js new file mode 100644 index 0000000..25a5a5b --- /dev/null +++ b/proyecto/george_penafielp/.web/components/reflex/radix_themes_color_mode_provider.js @@ -0,0 +1,60 @@ +import { useTheme } from "next-themes"; +import { useRef, useEffect, useState, createElement } from "react"; +import { + ColorModeContext, + defaultColorMode, + isDevMode, + lastCompiledTimeStamp, +} from "$/utils/context.js"; + +export default function RadixThemesColorModeProvider({ children }) { + const { theme, resolvedTheme, setTheme } = useTheme(); + const [rawColorMode, setRawColorMode] = useState(defaultColorMode); + const [resolvedColorMode, setResolvedColorMode] = useState( + defaultColorMode === "dark" ? "dark" : "light", + ); + const firstUpdate = useRef(true); + useEffect(() => { + if (firstUpdate.current) { + firstUpdate.current = false; + setRawColorMode(theme); + setResolvedColorMode(resolvedTheme); + } + }); + + useEffect(() => { + if (isDevMode) { + const lastCompiledTimeInLocalStorage = + localStorage.getItem("last_compiled_time"); + if (lastCompiledTimeInLocalStorage !== lastCompiledTimeStamp) { + // on app startup, make sure the application color mode is persisted correctly. + setTheme(defaultColorMode); + localStorage.setItem("last_compiled_time", lastCompiledTimeStamp); + return; + } + } + setRawColorMode(theme); + setResolvedColorMode(resolvedTheme); + }, [theme, resolvedTheme]); + + const toggleColorMode = () => { + setTheme(resolvedTheme === "light" ? "dark" : "light"); + }; + const setColorMode = (mode) => { + const allowedModes = ["light", "dark", "system"]; + if (!allowedModes.includes(mode)) { + console.error( + `Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`, + ); + mode = defaultColorMode; + } + setTheme(mode); + }; + return createElement( + ColorModeContext, + { + value: { rawColorMode, resolvedColorMode, toggleColorMode, setColorMode }, + }, + children, + ); +} diff --git a/proyecto/george_penafielp/.web/components/shiki/code.js b/proyecto/george_penafielp/.web/components/shiki/code.js new file mode 100644 index 0000000..8d721dd --- /dev/null +++ b/proyecto/george_penafielp/.web/components/shiki/code.js @@ -0,0 +1,40 @@ +import { useEffect, useState, createElement } from "react"; +import { codeToHtml } from "shiki"; + +/** + * Code component that uses Shiki to convert code to HTML and render it. + * + * @param code - The code to be highlighted. + * @param theme - The theme to be used for highlighting. + * @param language - The language of the code. + * @param transformers - The transformers to be applied to the code. + * @param decorations - The decorations to be applied to the code. + * @param divProps - Additional properties to be passed to the div element. + * @returns The rendered code block. + */ +export function Code({ + code, + theme, + language, + transformers, + decorations, + ...divProps +}) { + const [codeResult, setCodeResult] = useState(""); + useEffect(() => { + async function fetchCode() { + const result = await codeToHtml(code, { + lang: language, + theme, + transformers, + decorations, + }); + setCodeResult(result); + } + fetchCode(); + }, [code, language, theme, transformers, decorations]); + return createElement("div", { + dangerouslySetInnerHTML: { __html: codeResult }, + ...divProps, + }); +} diff --git a/proyecto/george_penafielp/.web/jsconfig.json b/proyecto/george_penafielp/.web/jsconfig.json new file mode 100644 index 0000000..3fcb35b --- /dev/null +++ b/proyecto/george_penafielp/.web/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "$/*": ["*"], + "@/*": ["public/*"] + } + } +} diff --git a/proyecto/george_penafielp/.web/next.config.js b/proyecto/george_penafielp/.web/next.config.js new file mode 100644 index 0000000..41b1924 --- /dev/null +++ b/proyecto/george_penafielp/.web/next.config.js @@ -0,0 +1 @@ +module.exports = {basePath: "", compress: true, trailingSlash: true, staticPageGenerationTimeout: 60, devIndicators: false}; \ No newline at end of file diff --git a/proyecto/george_penafielp/.web/package.json b/proyecto/george_penafielp/.web/package.json new file mode 100644 index 0000000..e52e994 --- /dev/null +++ b/proyecto/george_penafielp/.web/package.json @@ -0,0 +1,30 @@ +{ + "name": "reflex", + "scripts": { + "dev": "next dev ", + "export": "next build ", + "export-sitemap": "next build && next-sitemap", + "prod": "next start" + }, + "dependencies": { + "@emotion/react": "11.14.0", + "axios": "1.9.0", + "json5": "2.2.3", + "next": "15.3.2", + "next-sitemap": "4.2.3", + "next-themes": "0.4.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-focus-lock": "2.13.6", + "socket.io-client": "4.8.1", + "universal-cookie": "7.2.2" + }, + "devDependencies": { + "autoprefixer": "10.4.21", + "postcss": "8.5.4", + "postcss-import": "16.1.0" + }, + "overrides": { + "react-is": "19.1.0" + } +} \ No newline at end of file diff --git a/proyecto/george_penafielp/.web/postcss.config.js b/proyecto/george_penafielp/.web/postcss.config.js new file mode 100644 index 0000000..616a362 --- /dev/null +++ b/proyecto/george_penafielp/.web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + "postcss-import": {}, + autoprefixer: {}, + }, +}; diff --git a/proyecto/george_penafielp/.web/reflex.json b/proyecto/george_penafielp/.web/reflex.json new file mode 100644 index 0000000..b014569 --- /dev/null +++ b/proyecto/george_penafielp/.web/reflex.json @@ -0,0 +1 @@ +{"version": "0.7.14", "project_hash": 186056847066177739372624309409402140151} \ No newline at end of file diff --git a/proyecto/george_penafielp/.web/utils/client_side_routing.js b/proyecto/george_penafielp/.web/utils/client_side_routing.js new file mode 100644 index 0000000..3589b75 --- /dev/null +++ b/proyecto/george_penafielp/.web/utils/client_side_routing.js @@ -0,0 +1,43 @@ +import { useEffect, useRef, useState } from "react"; +import { useRouter } from "next/router"; + +/** + * React hook for use in /404 page to enable client-side routing. + * + * Uses the next/router to redirect to the provided URL when loading + * the 404 page (for example as a fallback in static hosting situations). + * + * @returns {boolean} routeNotFound - true if the current route is an actual 404 + */ +export const useClientSideRouting = () => { + const [routeNotFound, setRouteNotFound] = useState(false); + const didRedirect = useRef(false); + const router = useRouter(); + useEffect(() => { + if ( + router.isReady && + !didRedirect.current // have not tried redirecting yet + ) { + didRedirect.current = true; // never redirect twice to avoid "Hard Navigate" error + // attempt to redirect to the route in the browser address bar once + router + .replace({ + pathname: window.location.pathname, + query: window.location.search.slice(1), + }) + .then(() => { + // Check if the current route is /404 + if (router.pathname === "/404") { + setRouteNotFound(true); // Mark as an actual 404 + } + }) + .catch((e) => { + setRouteNotFound(true); // navigation failed, so this is a real 404 + }); + } + }, [router.isReady]); + + // Return the reactive bool, to avoid flashing 404 page until we know for sure + // the route is not found. + return routeNotFound; +}; diff --git a/proyecto/george_penafielp/.web/utils/helpers/dataeditor.js b/proyecto/george_penafielp/.web/utils/helpers/dataeditor.js new file mode 100644 index 0000000..7c8da26 --- /dev/null +++ b/proyecto/george_penafielp/.web/utils/helpers/dataeditor.js @@ -0,0 +1,69 @@ +import { GridCellKind } from "@glideapps/glide-data-grid"; + +export function getDEColumn(columns, col) { + let c = columns[col]; + c.pos = col; + return c; +} + +export function getDERow(data, row) { + return data[row]; +} + +export function locateCell(row, column) { + if (Array.isArray(row)) { + return row[column.pos]; + } else { + return row[column.id]; + } +} + +export function formatCell(value, column) { + const editable = column.editable ?? true; + switch (column.type) { + case "int": + case "float": + return { + kind: GridCellKind.Number, + data: value, + displayData: value + "", + readonly: !editable, + allowOverlay: editable, + }; + case "datetime": + // value = moment format? + case "str": + return { + kind: GridCellKind.Text, + data: value, + displayData: value, + readonly: !editable, + allowOverlay: editable, + }; + case "bool": + return { + kind: GridCellKind.Boolean, + data: value, + readonly: !editable, + }; + default: + console.log( + "Warning: column.type is undefined for column.title=" + column.title, + ); + return { + kind: GridCellKind.Text, + data: value, + displayData: column.type, + }; + } +} + +export function formatDataEditorCells(col, row, columns, data) { + if (row < data.length && col < columns.length) { + const column = getDEColumn(columns, col); + const rowData = getDERow(data, row); + const cellData = locateCell(rowData, column); + return formatCell(cellData, column); + } + return { kind: GridCellKind.Loading }; +} diff --git a/proyecto/george_penafielp/.web/utils/helpers/debounce.js b/proyecto/george_penafielp/.web/utils/helpers/debounce.js new file mode 100644 index 0000000..465baae --- /dev/null +++ b/proyecto/george_penafielp/.web/utils/helpers/debounce.js @@ -0,0 +1,17 @@ +const debounce_timeout_id = {}; + +/** + * Generic debounce helper + * + * @param {string} name - the name of the event to debounce + * @param {function} func - the function to call after debouncing + * @param {number} delay - the time in milliseconds to wait before calling the function + */ +export default function debounce(name, func, delay) { + const key = `${name}__${delay}`; + clearTimeout(debounce_timeout_id[key]); + debounce_timeout_id[key] = setTimeout(() => { + func(); + delete debounce_timeout_id[key]; + }, delay); +} diff --git a/proyecto/george_penafielp/.web/utils/helpers/paste.js b/proyecto/george_penafielp/.web/utils/helpers/paste.js new file mode 100644 index 0000000..f30fe94 --- /dev/null +++ b/proyecto/george_penafielp/.web/utils/helpers/paste.js @@ -0,0 +1,59 @@ +import { useEffect } from "react"; + +const handle_paste_data = (clipboardData) => + new Promise((resolve, reject) => { + const pasted_data = []; + const n_items = clipboardData.items.length; + const extract_data = (item) => { + const type = item.type; + if (item.kind === "string") { + item.getAsString((data) => { + pasted_data.push([type, data]); + if (pasted_data.length === n_items) { + resolve(pasted_data); + } + }); + } else if (item.kind === "file") { + const file = item.getAsFile(); + const reader = new FileReader(); + reader.onload = (e) => { + pasted_data.push([type, e.target.result]); + if (pasted_data.length === n_items) { + resolve(pasted_data); + } + }; + if (type.indexOf("text/") === 0) { + reader.readAsText(file); + } else { + reader.readAsDataURL(file); + } + } + }; + for (const item of clipboardData.items) { + extract_data(item); + } + }); + +export default function usePasteHandler(target_ids, event_actions, on_paste) { + return useEffect(() => { + const handle_paste = (_ev) => { + event_actions.preventDefault && _ev.preventDefault(); + event_actions.stopPropagation && _ev.stopPropagation(); + handle_paste_data(_ev.clipboardData).then(on_paste); + }; + const targets = target_ids + .map((id) => document.getElementById(id)) + .filter((element) => !!element); + if (target_ids.length === 0) { + targets.push(document); + } + targets.forEach((target) => + target.addEventListener("paste", handle_paste, false), + ); + return () => { + targets.forEach((target) => + target.removeEventListener("paste", handle_paste, false), + ); + }; + }); +} diff --git a/proyecto/george_penafielp/.web/utils/helpers/range.js b/proyecto/george_penafielp/.web/utils/helpers/range.js new file mode 100644 index 0000000..b649c16 --- /dev/null +++ b/proyecto/george_penafielp/.web/utils/helpers/range.js @@ -0,0 +1,43 @@ +/** + * Simulate the python range() builtin function. + * inspired by https://dev.to/guyariely/using-python-range-in-javascript-337p + * + * If needed outside of an iterator context, use `Array.from(range(10))` or + * spread syntax `[...range(10)]` to get an array. + * + * @param {number} start: the start or end of the range. + * @param {number} stop: the end of the range. + * @param {number} step: the step of the range. + * @returns {object} an object with a Symbol.iterator method over the range + */ +export default function range(start, stop, step) { + return { + [Symbol.iterator]() { + if (stop === undefined) { + stop = start; + start = 0; + } + if (step === undefined) { + step = 1; + } + + let i = start - step; + + return { + next() { + i += step; + if ((step > 0 && i < stop) || (step < 0 && i > stop)) { + return { + value: i, + done: false, + }; + } + return { + value: undefined, + done: true, + }; + }, + }; + }, + }; +} diff --git a/proyecto/george_penafielp/.web/utils/helpers/throttle.js b/proyecto/george_penafielp/.web/utils/helpers/throttle.js new file mode 100644 index 0000000..771937b --- /dev/null +++ b/proyecto/george_penafielp/.web/utils/helpers/throttle.js @@ -0,0 +1,22 @@ +const in_throttle = {}; + +/** + * Generic throttle helper + * + * @param {string} name - the name of the event to throttle + * @param {number} limit - time in milliseconds between events + * @returns true if the event is allowed to execute, false if it is throttled + */ +export default function throttle(name, limit) { + const key = `${name}__${limit}`; + if (!in_throttle[key]) { + in_throttle[key] = true; + + setTimeout(() => { + delete in_throttle[key]; + }, limit); + // function was not throttled, so allow execution + return true; + } + return false; +} diff --git a/proyecto/george_penafielp/.web/utils/state.js b/proyecto/george_penafielp/.web/utils/state.js new file mode 100644 index 0000000..58eb3f1 --- /dev/null +++ b/proyecto/george_penafielp/.web/utils/state.js @@ -0,0 +1,1041 @@ +// State management for Reflex web apps. +import axios from "axios"; +import io from "socket.io-client"; +import JSON5 from "json5"; +import env from "$/env.json"; +import reflexEnvironment from "$/reflex.json"; +import Cookies from "universal-cookie"; +import { useEffect, useRef, useState } from "react"; +import Router, { useRouter } from "next/router"; +import { + initialEvents, + initialState, + onLoadInternalEvent, + state_name, + exception_state_name, +} from "$/utils/context.js"; +import debounce from "$/utils/helpers/debounce"; +import throttle from "$/utils/helpers/throttle"; + +// Endpoint URLs. +const EVENTURL = env.EVENT; +const UPLOADURL = env.UPLOAD; + +// These hostnames indicate that the backend and frontend are reachable via the same domain. +const SAME_DOMAIN_HOSTNAMES = ["localhost", "0.0.0.0", "::", "0:0:0:0:0:0:0:0"]; + +// Global variable to hold the token. +let token; + +// Key for the token in the session storage. +const TOKEN_KEY = "token"; + +// create cookie instance +const cookies = new Cookies(); + +// Dictionary holding component references. +export const refs = {}; + +// Flag ensures that only one event is processing on the backend concurrently. +let event_processing = false; +// Array holding pending events to be processed. +const event_queue = []; + +/** + * Generate a UUID (Used for session tokens). + * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid + * @returns A UUID. + */ +export const generateUUID = () => { + let d = new Date().getTime(), + d2 = (performance && performance.now && performance.now() * 1000) || 0; + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + let r = Math.random() * 16; + if (d > 0) { + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } else { + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c == "x" ? r : (r & 0x7) | 0x8).toString(16); + }); +}; + +/** + * Get the token for the current session. + * @returns The token. + */ +export const getToken = () => { + if (token) { + return token; + } + if (typeof window !== "undefined") { + if (!window.sessionStorage.getItem(TOKEN_KEY)) { + window.sessionStorage.setItem(TOKEN_KEY, generateUUID()); + } + token = window.sessionStorage.getItem(TOKEN_KEY); + } + return token; +}; + +/** + * Get the URL for the backend server + * @param url_str The URL string to parse. + * @returns The given URL modified to point to the actual backend server. + */ +export const getBackendURL = (url_str) => { + // Get backend URL object from the endpoint. + const endpoint = new URL(url_str); + if ( + typeof window !== "undefined" && + SAME_DOMAIN_HOSTNAMES.includes(endpoint.hostname) + ) { + // Use the frontend domain to access the backend + const frontend_hostname = window.location.hostname; + endpoint.hostname = frontend_hostname; + if (window.location.protocol === "https:") { + if (endpoint.protocol === "ws:") { + endpoint.protocol = "wss:"; + } else if (endpoint.protocol === "http:") { + endpoint.protocol = "https:"; + } + endpoint.port = ""; // Assume websocket is on https port via load balancer. + } + } + return endpoint; +}; + +/** + * Check if the backend is disabled. + * + * @returns True if the backend is disabled, false otherwise. + */ +export const isBackendDisabled = () => { + const cookie = document.cookie + .split("; ") + .find((row) => row.startsWith("backend-enabled=")); + return cookie !== undefined && cookie.split("=")[1] == "false"; +}; + +/** + * Determine if any event in the event queue is stateful. + * + * @returns True if there's any event that requires state and False if none of them do. + */ +export const isStateful = () => { + if (event_queue.length === 0) { + return false; + } + return event_queue.some((event) => event.name.startsWith("reflex___state")); +}; + +/** + * Apply a delta to the state. + * @param state The state to apply the delta to. + * @param delta The delta to apply. + */ +export const applyDelta = (state, delta) => { + return { ...state, ...delta }; +}; + +/** + * Evaluate a dynamic component. + * @param component The component to evaluate. + * @returns The evaluated component. + */ +export const evalReactComponent = async (component) => { + if (!window.React && window.__reflex) { + window.React = window.__reflex.react; + } + const encodedJs = encodeURIComponent(component); + const dataUri = "data:text/javascript;charset=utf-8," + encodedJs; + const module = await eval(`import(dataUri)`); + return module.default; +}; + +/** + * Only Queue and process events when websocket connection exists. + * @param event The event to queue. + * @param socket The socket object to send the event on. + * + * @returns Adds event to queue and processes it if websocket exits, does nothing otherwise. + */ +export const queueEventIfSocketExists = async (events, socket) => { + if (!socket) { + return; + } + await queueEvents(events, socket); +}; + +/** + * Handle frontend event or send the event to the backend via Websocket. + * @param event The event to send. + * @param socket The socket object to send the event on. + * + * @returns True if the event was sent, false if it was handled locally. + */ +export const applyEvent = async (event, socket) => { + // Handle special events + if (event.name == "_redirect") { + if ((event.payload.path ?? undefined) === undefined) { + return false; + } + if (event.payload.external) { + window.open(event.payload.path, "_blank", "noopener"); + } else if (event.payload.replace) { + Router.replace(event.payload.path); + } else { + Router.push(event.payload.path); + } + return false; + } + + if (event.name == "_remove_cookie") { + cookies.remove(event.payload.key, { ...event.payload.options }); + queueEventIfSocketExists(initialEvents(), socket); + return false; + } + + if (event.name == "_clear_local_storage") { + localStorage.clear(); + queueEventIfSocketExists(initialEvents(), socket); + return false; + } + + if (event.name == "_remove_local_storage") { + localStorage.removeItem(event.payload.key); + queueEventIfSocketExists(initialEvents(), socket); + return false; + } + + if (event.name == "_clear_session_storage") { + sessionStorage.clear(); + queueEvents(initialEvents(), socket); + return false; + } + + if (event.name == "_remove_session_storage") { + sessionStorage.removeItem(event.payload.key); + queueEvents(initialEvents(), socket); + return false; + } + + if (event.name == "_download") { + const a = document.createElement("a"); + a.hidden = true; + a.href = event.payload.url; + // Special case when linking to uploaded files + if (a.href.includes("getBackendURL(env.UPLOAD)")) { + a.href = eval?.( + event.payload.url.replace( + "getBackendURL(env.UPLOAD)", + `"${getBackendURL(env.UPLOAD)}"`, + ), + ); + } + a.download = event.payload.filename; + a.click(); + a.remove(); + return false; + } + + if (event.name == "_set_focus") { + const ref = + event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref; + const current = ref?.current; + if (current === undefined || current?.focus === undefined) { + console.error( + `No element found for ref ${event.payload.ref} in _set_focus`, + ); + } else { + current.focus(); + } + return false; + } + + if (event.name == "_set_value") { + const ref = + event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref; + if (ref.current) { + ref.current.value = event.payload.value; + } + return false; + } + + if ( + event.name == "_call_function" && + typeof event.payload.function !== "string" + ) { + try { + const eval_result = event.payload.function(); + if (event.payload.callback) { + const final_result = + !!eval_result && typeof eval_result.then === "function" + ? await eval_result + : eval_result; + const callback = + typeof event.payload.callback === "string" + ? eval(event.payload.callback) + : event.payload.callback; + callback(final_result); + } + } catch (e) { + console.log("_call_function", e); + if (window && window?.onerror) { + window.onerror(e.message, null, null, null, e); + } + } + return false; + } + + if (event.name == "_call_script" || event.name == "_call_function") { + try { + const eval_result = + event.name == "_call_script" + ? eval(event.payload.javascript_code) + : eval(event.payload.function)(); + + if (event.payload.callback) { + const final_result = + !!eval_result && typeof eval_result.then === "function" + ? await eval_result + : eval_result; + const callback = + typeof event.payload.callback === "string" + ? eval(event.payload.callback) + : event.payload.callback; + callback(final_result); + } + } catch (e) { + console.log("_call_script", e); + if (window && window?.onerror) { + window.onerror(e.message, null, null, null, e); + } + } + return false; + } + + // Update token and router data (if missing). + event.token = getToken(); + if ( + event.router_data === undefined || + Object.keys(event.router_data).length === 0 + ) { + event.router_data = (({ pathname, query, asPath }) => ({ + pathname, + query, + asPath, + }))(Router); + } + + // Send the event to the server. + if (socket) { + socket.emit("event", event); + return true; + } + + return false; +}; + +/** + * Send an event to the server via REST. + * @param event The current event. + * @param socket The socket object to send the response event(s) on. + * + * @returns Whether the event was sent. + */ +export const applyRestEvent = async (event, socket) => { + let eventSent = false; + if (event.handler === "uploadFiles") { + if (event.payload.files === undefined || event.payload.files.length === 0) { + // Submit the event over the websocket to trigger the event handler. + return await applyEvent(Event(event.name, { files: [] }), socket); + } + + // Start upload, but do not wait for it, which would block other events. + uploadFiles( + event.name, + event.payload.files, + event.payload.upload_id, + event.payload.on_upload_progress, + socket, + ); + return false; + } + return eventSent; +}; + +/** + * Queue events to be processed and trigger processing of queue. + * @param events Array of events to queue. + * @param socket The socket object to send the event on. + * @param prepend Whether to place the events at the beginning of the queue. + */ +export const queueEvents = async (events, socket, prepend) => { + if (prepend) { + // Drain the existing queue and place it after the given events. + events = [ + ...events, + ...Array.from({ length: event_queue.length }).map(() => + event_queue.shift(), + ), + ]; + } + event_queue.push(...events.filter((e) => e !== undefined && e !== null)); + await processEvent(socket.current); +}; + +/** + * Process an event off the event queue. + * @param socket The socket object to send the event on. + */ +export const processEvent = async (socket) => { + // Only proceed if the socket is up and no event in the queue uses state, otherwise we throw the event into the void + if (!socket && isStateful()) { + return; + } + + // Only proceed if we're not already processing an event. + if (event_queue.length === 0 || event_processing) { + return; + } + + // Set processing to true to block other events from being processed. + event_processing = true; + + // Apply the next event in the queue. + const event = event_queue.shift(); + + let eventSent = false; + // Process events with handlers via REST and all others via websockets. + if (event.handler) { + eventSent = await applyRestEvent(event, socket); + } else { + eventSent = await applyEvent(event, socket); + } + // If no event was sent, set processing to false. + if (!eventSent) { + event_processing = false; + // recursively call processEvent to drain the queue, since there is + // no state update to trigger the useEffect event loop. + await processEvent(socket); + } +}; + +/** + * Connect to a websocket and set the handlers. + * @param socket The socket object to connect. + * @param dispatch The function to queue state update + * @param transports The transports to use. + * @param setConnectErrors The function to update connection error value. + * @param client_storage The client storage object from context.js + */ +export const connect = async ( + socket, + dispatch, + transports, + setConnectErrors, + client_storage = {}, +) => { + // Get backend URL object from the endpoint. + const endpoint = getBackendURL(EVENTURL); + + // Create the socket. + socket.current = io(endpoint.href, { + path: endpoint["pathname"], + transports: transports, + protocols: [reflexEnvironment.version], + autoUnref: false, + }); + // Ensure undefined fields in events are sent as null instead of removed + socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v); + socket.current.io.decoder.tryParse = (str) => { + try { + return JSON5.parse(str); + } catch (e) { + return false; + } + }; + + function checkVisibility() { + if (document.visibilityState === "visible") { + if (!socket.current.connected) { + console.log("Socket is disconnected, attempting to reconnect "); + socket.current.connect(); + } else { + console.log("Socket is reconnected "); + } + } + } + + const disconnectTrigger = (event) => { + if (socket.current?.connected) { + console.log("Disconnect websocket on unload"); + socket.current.disconnect(); + } + }; + + const pagehideHandler = (event) => { + if (event.persisted && socket.current?.connected) { + console.log("Disconnect backend before bfcache on navigation"); + socket.current.disconnect(); + } + }; + + // Once the socket is open, hydrate the page. + socket.current.on("connect", () => { + setConnectErrors([]); + window.addEventListener("pagehide", pagehideHandler); + window.addEventListener("beforeunload", disconnectTrigger); + window.addEventListener("unload", disconnectTrigger); + }); + + socket.current.on("connect_error", (error) => { + setConnectErrors((connectErrors) => [connectErrors.slice(-9), error]); + }); + + // When the socket disconnects reset the event_processing flag + socket.current.on("disconnect", () => { + event_processing = false; + window.removeEventListener("unload", disconnectTrigger); + window.removeEventListener("beforeunload", disconnectTrigger); + window.removeEventListener("pagehide", pagehideHandler); + }); + + // On each received message, queue the updates and events. + socket.current.on("event", async (update) => { + for (const substate in update.delta) { + dispatch[substate](update.delta[substate]); + } + applyClientStorageDelta(client_storage, update.delta); + event_processing = !update.final; + if (update.events) { + queueEvents(update.events, socket); + } + }); + socket.current.on("reload", async (event) => { + event_processing = false; + queueEvents([...initialEvents(), event], socket, true); + }); + + document.addEventListener("visibilitychange", checkVisibility); +}; + +/** + * Upload files to the server. + * + * @param state The state to apply the delta to. + * @param handler The handler to use. + * @param upload_id The upload id to use. + * @param on_upload_progress The function to call on upload progress. + * @param socket the websocket connection + * + * @returns The response from posting to the UPLOADURL endpoint. + */ +export const uploadFiles = async ( + handler, + files, + upload_id, + on_upload_progress, + socket, +) => { + // return if there's no file to upload + if (files === undefined || files.length === 0) { + return false; + } + + const upload_ref_name = `__upload_controllers_${upload_id}`; + + if (refs[upload_ref_name]) { + console.log("Upload already in progress for ", upload_id); + return false; + } + + // Track how many partial updates have been processed for this upload. + let resp_idx = 0; + const eventHandler = (progressEvent) => { + const event_callbacks = socket._callbacks.$event; + // Whenever called, responseText will contain the entire response so far. + const chunks = progressEvent.event.target.responseText.trim().split("\n"); + // So only process _new_ chunks beyond resp_idx. + chunks.slice(resp_idx).map((chunk_json) => { + try { + const chunk = JSON5.parse(chunk_json); + event_callbacks.map((f, ix) => { + f(chunk) + .then(() => { + if (ix === event_callbacks.length - 1) { + // Mark this chunk as processed. + resp_idx += 1; + } + }) + .catch((e) => { + if (progressEvent.progress === 1) { + // Chunk may be incomplete, so only report errors when full response is available. + console.log("Error processing chunk", chunk, e); + } + return; + }); + }); + } catch (e) { + if (progressEvent.progress === 1) { + console.log("Error parsing chunk", chunk_json, e); + } + return; + } + }); + }; + + const controller = new AbortController(); + const config = { + headers: { + "Reflex-Client-Token": getToken(), + "Reflex-Event-Handler": handler, + }, + signal: controller.signal, + onDownloadProgress: eventHandler, + }; + if (on_upload_progress) { + config["onUploadProgress"] = on_upload_progress; + } + const formdata = new FormData(); + + // Add the token and handler to the file name. + files.forEach((file) => { + formdata.append("files", file, file.path || file.name); + }); + + // Send the file to the server. + refs[upload_ref_name] = controller; + + try { + return await axios.post(getBackendURL(UPLOADURL), formdata, config); + } catch (error) { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.log(error.response.data); + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log(error.message); + } + return false; + } finally { + delete refs[upload_ref_name]; + } +}; + +/** + * Create an event object. + * @param {string} name The name of the event. + * @param {Object.} payload The payload of the event. + * @param {Object.} event_actions The actions to take on the event. + * @param {string} handler The client handler to process event. + * @returns The event object. + */ +export const Event = ( + name, + payload = {}, + event_actions = {}, + handler = null, +) => { + return { name, payload, handler, event_actions }; +}; + +/** + * Package client-side storage values as payload to send to the + * backend with the hydrate event + * @param client_storage The client storage object from context.js + * @returns payload dict of client storage values + */ +export const hydrateClientStorage = (client_storage) => { + const client_storage_values = {}; + if (client_storage.cookies) { + for (const state_key in client_storage.cookies) { + const cookie_options = client_storage.cookies[state_key]; + const cookie_name = cookie_options.name || state_key; + const cookie_value = cookies.get(cookie_name); + if (cookie_value !== undefined) { + client_storage_values[state_key] = cookies.get(cookie_name); + } + } + } + if (client_storage.local_storage && typeof window !== "undefined") { + for (const state_key in client_storage.local_storage) { + const options = client_storage.local_storage[state_key]; + const local_storage_value = localStorage.getItem( + options.name || state_key, + ); + if (local_storage_value !== null) { + client_storage_values[state_key] = local_storage_value; + } + } + } + if (client_storage.session_storage && typeof window != "undefined") { + for (const state_key in client_storage.session_storage) { + const session_options = client_storage.session_storage[state_key]; + const session_storage_value = sessionStorage.getItem( + session_options.name || state_key, + ); + if (session_storage_value != null) { + client_storage_values[state_key] = session_storage_value; + } + } + } + if ( + client_storage.cookies || + client_storage.local_storage || + client_storage.session_storage + ) { + return client_storage_values; + } + return {}; +}; + +/** + * Update client storage values based on backend state delta. + * @param client_storage The client storage object from context.js + * @param delta The state update from the backend + */ +const applyClientStorageDelta = (client_storage, delta) => { + // find the main state and check for is_hydrated + const unqualified_states = Object.keys(delta).filter( + (key) => key.split(".").length === 1, + ); + if (unqualified_states.length === 1) { + const main_state = delta[unqualified_states[0]]; + if (main_state.is_hydrated !== undefined && !main_state.is_hydrated) { + // skip if the state is not hydrated yet, since all client storage + // values are sent in the hydrate event + return; + } + } + // Save known client storage values to cookies and localStorage. + for (const substate in delta) { + for (const key in delta[substate]) { + const state_key = `${substate}.${key}`; + if (client_storage.cookies && state_key in client_storage.cookies) { + const cookie_options = { ...client_storage.cookies[state_key] }; + const cookie_name = cookie_options.name || state_key; + delete cookie_options.name; // name is not a valid cookie option + cookies.set(cookie_name, delta[substate][key], cookie_options); + } else if ( + client_storage.local_storage && + state_key in client_storage.local_storage && + typeof window !== "undefined" + ) { + const options = client_storage.local_storage[state_key]; + localStorage.setItem(options.name || state_key, delta[substate][key]); + } else if ( + client_storage.session_storage && + state_key in client_storage.session_storage && + typeof window !== "undefined" + ) { + const session_options = client_storage.session_storage[state_key]; + sessionStorage.setItem( + session_options.name || state_key, + delta[substate][key], + ); + } + } + } +}; + +/** + * Establish websocket event loop for a NextJS page. + * @param dispatch The reducer dispatch function to update state. + * @param initial_events The initial app events. + * @param client_storage The client storage object from context.js + * + * @returns [addEvents, connectErrors] - + * addEvents is used to queue an event, and + * connectErrors is an array of reactive js error from the websocket connection (or null if connected). + */ +export const useEventLoop = ( + dispatch, + initial_events = () => [], + client_storage = {}, +) => { + const socket = useRef(null); + const router = useRouter(); + const [connectErrors, setConnectErrors] = useState([]); + + // Function to add new events to the event queue. + const addEvents = (events, args, event_actions) => { + const _events = events.filter((e) => e !== undefined && e !== null); + + if (!(args instanceof Array)) { + args = [args]; + } + + event_actions = _events.reduce( + (acc, e) => ({ ...acc, ...e.event_actions }), + event_actions ?? {}, + ); + + const _e = args.filter((o) => o?.preventDefault !== undefined)[0]; + + if (event_actions?.preventDefault && _e?.preventDefault) { + _e.preventDefault(); + } + if (event_actions?.stopPropagation && _e?.stopPropagation) { + _e.stopPropagation(); + } + const combined_name = _events.map((e) => e.name).join("+++"); + if (event_actions?.temporal) { + if (!socket.current || !socket.current.connected) { + return; // don't queue when the backend is not connected + } + } + if (event_actions?.throttle) { + // If throttle returns false, the events are not added to the queue. + if (!throttle(combined_name, event_actions.throttle)) { + return; + } + } + if (event_actions?.debounce) { + // If debounce is used, queue the events after some delay + debounce( + combined_name, + () => queueEvents(_events, socket), + event_actions.debounce, + ); + } else { + queueEvents(_events, socket); + } + }; + + const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode + useEffect(() => { + if (router.isReady && !sentHydrate.current) { + queueEvents( + initial_events().map((e) => ({ + ...e, + router_data: (({ pathname, query, asPath }) => ({ + pathname, + query, + asPath, + }))(router), + })), + socket, + true, + ); + sentHydrate.current = true; + } + }, [router.isReady]); + + // Handle frontend errors and send them to the backend via websocket. + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + window.onerror = function (msg, url, lineNo, columnNo, error) { + addEvents([ + Event(`${exception_state_name}.handle_frontend_exception`, { + stack: error.stack, + component_stack: "", + }), + ]); + return false; + }; + + //NOTE: Only works in Chrome v49+ + //https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events + window.onunhandledrejection = function (event) { + addEvents([ + Event(`${exception_state_name}.handle_frontend_exception`, { + stack: event.reason?.stack, + component_stack: "", + }), + ]); + return false; + }; + }, []); + + // Handle socket connect/disconnect. + useEffect(() => { + // only use websockets if state is present and backend is not disabled (reflex cloud). + if (Object.keys(initialState).length > 1 && !isBackendDisabled()) { + // Initialize the websocket connection. + if (!socket.current) { + connect( + socket, + dispatch, + ["websocket"], + setConnectErrors, + client_storage, + ); + } + } + + // Cleanup function. + return () => { + if (socket.current) { + socket.current.disconnect(); + } + }; + }, []); + + // Main event loop. + useEffect(() => { + // Skip if the router is not ready. + if (!router.isReady || isBackendDisabled()) { + return; + } + (async () => { + // Process all outstanding events. + while (event_queue.length > 0 && !event_processing) { + await processEvent(socket.current); + } + })(); + }); + + // localStorage event handling + useEffect(() => { + const storage_to_state_map = {}; + + if (client_storage.local_storage && typeof window !== "undefined") { + for (const state_key in client_storage.local_storage) { + const options = client_storage.local_storage[state_key]; + if (options.sync) { + const local_storage_value_key = options.name || state_key; + storage_to_state_map[local_storage_value_key] = state_key; + } + } + } + + // e is StorageEvent + const handleStorage = (e) => { + if (storage_to_state_map[e.key]) { + const vars = {}; + vars[storage_to_state_map[e.key]] = e.newValue; + const event = Event( + `${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`, + { vars: vars }, + ); + addEvents([event], e); + } + }; + + window.addEventListener("storage", handleStorage); + return () => window.removeEventListener("storage", handleStorage); + }); + + // Route after the initial page hydration. + useEffect(() => { + const change_start = () => { + const main_state_dispatch = dispatch["reflex___state____state"]; + if (main_state_dispatch !== undefined) { + main_state_dispatch({ is_hydrated: false }); + } + }; + const change_complete = () => addEvents(onLoadInternalEvent()); + const change_error = () => { + // Remove cached error state from router for this page, otherwise the + // page will never send on_load events again. + if (router.components[router.pathname].error) { + delete router.components[router.pathname].error; + } + }; + router.events.on("routeChangeStart", change_start); + router.events.on("routeChangeComplete", change_complete); + router.events.on("routeChangeError", change_error); + return () => { + router.events.off("routeChangeStart", change_start); + router.events.off("routeChangeComplete", change_complete); + router.events.off("routeChangeError", change_error); + }; + }, [router]); + + return [addEvents, connectErrors]; +}; + +/*** + * Check if a value is truthy in python. + * @param val The value to check. + * @returns True if the value is truthy, false otherwise. + */ +export const isTrue = (val) => { + if (Array.isArray(val)) return val.length > 0; + if (val === Object(val)) return Object.keys(val).length > 0; + return Boolean(val); +}; + +/*** + * Check if a value is not null or undefined. + * @param val The value to check. + * @returns True if the value is not null or undefined, false otherwise. + */ +export const isNotNullOrUndefined = (val) => { + return (val ?? undefined) !== undefined; +}; + +/** + * Get the value from a ref. + * @param ref The ref to get the value from. + * @returns The value. + */ +export const getRefValue = (ref) => { + if (!ref || !ref.current) { + return; + } + if (ref.current.type == "checkbox") { + return ref.current.checked; // chakra + } else if ( + ref.current.className?.includes("rt-CheckboxRoot") || + ref.current.className?.includes("rt-SwitchRoot") + ) { + return ref.current.ariaChecked == "true"; // radix + } else if (ref.current.className?.includes("rt-SliderRoot")) { + // find the actual slider + return ref.current.querySelector(".rt-SliderThumb")?.ariaValueNow; + } else { + //querySelector(":checked") is needed to get value from radio_group + return ( + ref.current.value || + (ref.current.querySelector && + ref.current.querySelector(":checked") && + ref.current.querySelector(":checked")?.value) + ); + } +}; + +/** + * Get the values from a ref array. + * @param refs The refs to get the values from. + * @returns The values array. + */ +export const getRefValues = (refs) => { + if (!refs) { + return; + } + // getAttribute is used by RangeSlider because it doesn't assign value + return refs.map((ref) => + ref.current + ? ref.current.value || ref.current.getAttribute("aria-valuenow") + : null, + ); +}; + +/** + * Spread two arrays or two objects. + * @param first The first array or object. + * @param second The second array or object. + * @returns The final merged array or object. + */ +export const spreadArraysOrObjects = (first, second) => { + if (Array.isArray(first) && Array.isArray(second)) { + return [...first, ...second]; + } else if (typeof first === "object" && typeof second === "object") { + return { ...first, ...second }; + } else { + throw new Error("Both parameters must be either arrays or objects."); + } +}; diff --git a/proyectoedisontanav/README.md b/proyectoedisontanav/README.md new file mode 100644 index 0000000..c8c1e03 --- /dev/null +++ b/proyectoedisontanav/README.md @@ -0,0 +1,21 @@ +# Mi ejemplo + +Desarrollado por Edi TANA + +Mi video aquí + +[![Ver el video](https://github.com/gnuleospython/python-ws/raw/refs/heads/main/proyectoedisontanav/portada.jpg)](https://github.com/user-attachments/assets/74304462-c9fc-4e32-a5aa-cdf6a467b49d) + + + +## Licencia + +[![CC BY-SA 4.0][cc-by-sa-shield]][cc-by-sa] + +Este repositorio digital está licenciado bajo la licencia Creative Commons 4.0, CC-BY-NC-SA-4.0 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.][cc-by-sa]. + +[![CC BY-SA 4.0][cc-by-sa-image]][cc-by-sa] + +[cc-by-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png +[cc-by-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/ +[cc-by-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg diff --git a/proyectoedisontanav/assets/chakra_color_mode_provider.js b/proyectoedisontanav/assets/chakra_color_mode_provider.js new file mode 100644 index 0000000..f7ac446 --- /dev/null +++ b/proyectoedisontanav/assets/chakra_color_mode_provider.js @@ -0,0 +1,44 @@ +import { useTheme } from "$/utils/react-theme"; +import { useColorMode as chakraUseColorMode } from "@chakra-ui/react"; +import { createElement, useEffect } from "react"; +import { ColorModeContext, defaultColorMode } from "$/utils/context"; + +export default function ChakraColorModeProvider({ children }) { + const { theme, resolvedTheme, setTheme } = useTheme(); + const { colorMode: chakraColorMode, toggleColorMode: toggleChakraColorMode } = + chakraUseColorMode(); + + useEffect(() => { + if (chakraColorMode != resolvedTheme) { + toggleChakraColorMode(); + } + }, [theme, resolvedTheme]); + + const toggleColorMode = () => { + setTheme(resolvedTheme === "light" ? "dark" : "light"); + }; + + const setColorMode = (mode) => { + const allowedModes = ["light", "dark", "system"]; + if (!allowedModes.includes(mode)) { + console.error( + `Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".` + ); + mode = defaultColorMode; + } + setTheme(mode); + }; + + return createElement( + ColorModeContext.Provider, + { + value: { + rawColorMode: theme, + resolvedColorMode: resolvedTheme, + toggleColorMode, + setColorMode, + }, + }, + children + ); +} diff --git a/proyectoedisontanav/assets/favicon.ico b/proyectoedisontanav/assets/favicon.ico new file mode 100644 index 0000000..166ae99 Binary files /dev/null and b/proyectoedisontanav/assets/favicon.ico differ diff --git a/proyectoedisontanav/ejemploQuizEnReflexEdisonTANA.mp4 b/proyectoedisontanav/ejemploQuizEnReflexEdisonTANA.mp4 new file mode 100644 index 0000000..51200e0 Binary files /dev/null and b/proyectoedisontanav/ejemploQuizEnReflexEdisonTANA.mp4 differ diff --git a/proyectoedisontanav/markDown (copia).md b/proyectoedisontanav/markDown (copia).md new file mode 100644 index 0000000..db0add8 --- /dev/null +++ b/proyectoedisontanav/markDown (copia).md @@ -0,0 +1,92 @@ +% Titulo Presentacion +% Leonardo TANA +% + +# 🎥Titulo1 + +MI parrafo de ejemplo + +a. Uno + +## 📹Subtitulo 2 + +- Uno + +# Titulo2 + + + +![alt roca](/home/leos/24ago2021_21.png){: width="100%" style="text-align:center;"} + + +![alt bd](https://notabug.org/ltanav/termo/raw/master/basedatos.png){: width="200px" style="text-align:center;"} + + +# My Table of content +- [Section 1](#id-section1) +- [Section 2](#id-section2) + +Este sitio web se visualizará correctamente al usar un navegador compatible con los Estándares Web, por ejemplo: + +- Amaya +- Cunaguaro +- Chromium Browser +- Galeon +- Icecat +- Iceweasel +- Midori > 0.4.8 +- Mozilla Firefox > 4.0 +- Navegador web Abrowser > 4.0, +- Navegador web Epiphany +- Opera +- Navegador Naver Whale +- Navegador web Palemoon +- Navegador Web Vivaldi +- Safari +- SRWare Iron +- Uzbeth +- Webian Shell + +
+ +## Section 1 + +
+ +## Section 2 + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +## Licencia + +[![CC BY-SA 4.0][cc-by-sa-shield]][cc-by-sa] + +Este documento digital está licenciado bajo CC-BY-NC-SA-4.0 +[Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.][cc-by-sa]. + +[![CC BY-SA 4.0][cc-by-sa-image]][cc-by-sa] + +[cc-by-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/ +[cc-by-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png +[cc-by-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg diff --git a/proyectoedisontanav/portada.jpg b/proyectoedisontanav/portada.jpg new file mode 100644 index 0000000..8d599de Binary files /dev/null and b/proyectoedisontanav/portada.jpg differ diff --git a/proyectoedisontanav/quiz/__init__.py b/proyectoedisontanav/quiz/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/proyectoedisontanav/quiz/quiz.py b/proyectoedisontanav/quiz/quiz.py new file mode 100644 index 0000000..36d0865 --- /dev/null +++ b/proyectoedisontanav/quiz/quiz.py @@ -0,0 +1,152 @@ +"""Welcome to Reflex! This file outlines the steps to create a basic app.""" + +import copy +from typing import Any, List + +import reflex as rx + +from .results import results +from .styles import question_style, page_background + + +class State(rx.State): + """The app state.""" + + default_answers = [None, None, [False, False, False, False, False]] + answers: List[Any] + answer_key = ["False", "[10, 20, 30, 40]", [False, False, True, True, True]] + score: int + + def onload(self): + self.answers = copy.deepcopy(self.default_answers) + + def set_answers(self, answer, index, sub_index=None): + if sub_index is None: + self.answers[index] = answer + else: + self.answers[index][sub_index] = answer + + def submit(self): + total, correct = 0, 0 + for i in range(len(self.answers)): + if self.answers[i] == self.answer_key[i]: + correct += 1 + total += 1 + self.score = int(correct / total * 100) + return rx.redirect("/result") + + @rx.var + def percent_score(self) -> str: + return f"{self.score}%" + + +def header(): + return rx.vstack( + rx.heading("Python Quiz"), + rx.divider(), + rx.text("Aqui un ejemplo de quiz hecho en Reflex."), + rx.text("Una vez enviados, los resultados se mostrarán en la página de resultados."), + style=question_style, + ) + + +def question1(): + """The main view.""" + return rx.vstack( + rx.heading("Pregunta #1"), + rx.text( + "En Python 3, el valor máximo de un entero es 26", + rx.el.sup("3"), + " - 1", + ), + rx.divider(), + rx.radio( + items=["True", "False"], + default_value=State.default_answers[0], + default_checked=True, + on_change=lambda answer: State.set_answers(answer, 0), + ), + ) + + +def question2(): + return rx.vstack( + rx.heading("Pregunta #2"), + rx.text("¿Cuál es el resultado del siguiente operador de suma (+)?"), + rx.code_block( + """a = [10, 20] +b = a +b += [30, 40] +print(a)""", + language="python", + ), + rx.radio( + items=["[10, 20, 30, 40]", "[10, 20]"], + default_value=State.default_answers[1], + default_check=True, + on_change=lambda answer: State.set_answers(answer, 1), + ), + ) + + +def question3(): + def answer_checkbox(answer, index): + return rx.checkbox( + text=rx.code(answer), + on_change=lambda answer: State.set_answers(answer, 2, index), + ) + + return rx.vstack( + rx.heading("Pregunta #3"), + rx.text( + "¿Cuál de las siguientes formas es válida para especificar la cadena literal ", + rx.code("foo'bar"), + " in Python:", + ), + rx.vstack( + answer_checkbox("foo'bar", 0), + answer_checkbox("'foo''bar'", 1), + answer_checkbox("'foo\\\\'bar'", 2), + answer_checkbox('"""foo\'bar"""', 3), + answer_checkbox('"foo\'bar"', 4), + ), + ) + + +def index(): + """The main view.""" + return rx.color_mode.button(position="top-right"), rx.center( + rx.vstack( + header(), + rx.vstack( + question1(), + rx.divider(), + question2(), + rx.divider(), + question3(), + rx.center( + rx.button("Enviar", width="6em", on_click=State.submit), + width="100%", + ), + style=question_style, + spacing="5", + ), + align="center", + ), + bg=page_background, + padding_y="2em", + min_height="100vh", + ) + + +def result(): + return rx.color_mode.button(position="top-right"), results(State) + + +app = rx.App( + theme=rx.theme( + has_background=True, radius="none", accent_color="orange", appearance="dark" + ), +) +app.add_page(index, title="Quiz - Reflex", on_load=State.onload) +app.add_page(result, title="Quiz Resultados") diff --git a/proyectoedisontanav/quiz/results.py b/proyectoedisontanav/quiz/results.py new file mode 100644 index 0000000..fff2fc3 --- /dev/null +++ b/proyectoedisontanav/quiz/results.py @@ -0,0 +1,59 @@ +import reflex as rx +import reflex_chakra as rc + +from .styles import base_style as answer_style +from .styles import page_background + + +def render_answer(State, index): + return rx.table.row( + rx.table.cell(index + 1), + rx.table.cell( + rx.cond( + State.answers[index].to_string() == State.answer_key[index].to_string(), + rx.icon(tag="check", color="green"), + rx.icon(tag="x", color="red"), + ) + ), + rx.table.cell(State.answers[index].to_string()), + rx.table.cell(State.answer_key[index].to_string()), + ) + + +def results(State): + """The results view.""" + + def centered_item(item): + return rx.center(item, width="100%") + + return rx.center( + rx.vstack( + rx.heading("Resultados"), + rx.text("Abajo muestran los resultados del quiz."), + rx.divider(), + centered_item( + rc.circular_progress( + label=State.percent_score, value=State.score, size="3em" + ) + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("#"), + rx.table.column_header_cell("Resultado"), + rx.table.column_header_cell("Su respuesta"), + rx.table.column_header_cell("Respuesta correcta"), + ), + ), + rx.table.body( + rx.foreach(State.answers, lambda _, i: render_answer(State, i)), + ), + ), + centered_item( + rx.link(rx.button("Haga de nuevo el quiz"), href="/"), + ), + style=answer_style, + ), + bg=page_background, + min_height="100vh", + ) diff --git a/proyectoedisontanav/quiz/styles.py b/proyectoedisontanav/quiz/styles.py new file mode 100644 index 0000000..7ddb5af --- /dev/null +++ b/proyectoedisontanav/quiz/styles.py @@ -0,0 +1,13 @@ +import reflex as rx + +base_style = { + "padding": "2em", + "border_radius": "25px", + "border": f"1px solid {rx.color('accent', 12)}", + "box_shadow": f"0px 0px 10px 0px {rx.color('gray', 11)}", + "bg": rx.color("gray", 1), +} + +question_style = base_style | {"width": "100%"} + +page_background = rx.color("gray", 3) diff --git a/proyectoedisontanav/requirements.txt b/proyectoedisontanav/requirements.txt new file mode 100644 index 0000000..a29a6db --- /dev/null +++ b/proyectoedisontanav/requirements.txt @@ -0,0 +1,2 @@ +reflex>=0.7.8 +reflex-chakra>=0.6.0a7 \ No newline at end of file diff --git a/proyectoedisontanav/rxconfig.py b/proyectoedisontanav/rxconfig.py new file mode 100644 index 0000000..b89b518 --- /dev/null +++ b/proyectoedisontanav/rxconfig.py @@ -0,0 +1,6 @@ +import reflex as rx + +config = rx.Config( + app_name="quiz", + tailwind=None, +) diff --git a/clase3/flask_crud_usuarios/app.py b/walter_nunez/app.py similarity index 100% rename from clase3/flask_crud_usuarios/app.py rename to walter_nunez/app.py diff --git a/clase3/flask_crud_usuarios/templates/base.html b/walter_nunez/templates/base.html similarity index 100% rename from clase3/flask_crud_usuarios/templates/base.html rename to walter_nunez/templates/base.html diff --git a/clase3/flask_crud_usuarios/templates/form.html b/walter_nunez/templates/form.html similarity index 100% rename from clase3/flask_crud_usuarios/templates/form.html rename to walter_nunez/templates/form.html diff --git a/walter_nunez/templates/home.html b/walter_nunez/templates/home.html new file mode 100644 index 0000000..8a4fb57 --- /dev/null +++ b/walter_nunez/templates/home.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +

Página de Inicio

+

Bienvenido a la aplicación

+{% endblock %} \ No newline at end of file diff --git a/walter_nunez/templates/productos.html b/walter_nunez/templates/productos.html new file mode 100644 index 0000000..8ec5d6f --- /dev/null +++ b/walter_nunez/templates/productos.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +

Página de productos

+

Aqui desarrollar los productos

+{% endblock %} \ No newline at end of file diff --git a/clase3/flask_crud_usuarios/templates/usuarios.html b/walter_nunez/templates/usuarios.html similarity index 100% rename from clase3/flask_crud_usuarios/templates/usuarios.html rename to walter_nunez/templates/usuarios.html