├── .gitignore ├── logo_Codo.jpg ├── Proyecto CRUD 1C 2024.pdf ├── static ├── imagenes │ ├── altashtml.png │ ├── indexhtml.png │ ├── logo_Codo.jpg │ ├── listadohtml.png │ ├── listadoeliminar.png │ ├── modificarhtml.png │ ├── dbpythonanywhere.png │ └── apikeypythonanywhere.png └── css │ └── estilos.css ├── datosconexion-template.py ├── reload_pythonanywhere.py ├── index.html ├── listado.html ├── altas.html ├── listadoEliminar.html ├── app.py ├── app_closingconnection.py ├── modificaciones.html └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | datosconexion.py 2 | __pycache__/ -------------------------------------------------------------------------------- /logo_Codo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/logo_Codo.jpg -------------------------------------------------------------------------------- /Proyecto CRUD 1C 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/Proyecto CRUD 1C 2024.pdf -------------------------------------------------------------------------------- /static/imagenes/altashtml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/altashtml.png -------------------------------------------------------------------------------- /static/imagenes/indexhtml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/indexhtml.png -------------------------------------------------------------------------------- /static/imagenes/logo_Codo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/logo_Codo.jpg -------------------------------------------------------------------------------- /static/imagenes/listadohtml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/listadohtml.png -------------------------------------------------------------------------------- /static/imagenes/listadoeliminar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/listadoeliminar.png -------------------------------------------------------------------------------- /static/imagenes/modificarhtml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/modificarhtml.png -------------------------------------------------------------------------------- /static/imagenes/dbpythonanywhere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/dbpythonanywhere.png -------------------------------------------------------------------------------- /static/imagenes/apikeypythonanywhere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre/HEAD/static/imagenes/apikeypythonanywhere.png -------------------------------------------------------------------------------- /datosconexion-template.py: -------------------------------------------------------------------------------- 1 | # Datos de acceso a la Base de Datos 2 | host='host' 3 | user='user' 4 | password='pass' 5 | database='database' 6 | 7 | # Credenciales API PythonAnywhere 8 | username = "xxxxxxx" 9 | api_token = "api_token" 10 | domain_name = "xxxxxxx.pythonanywhere.com" -------------------------------------------------------------------------------- /reload_pythonanywhere.py: -------------------------------------------------------------------------------- 1 | # Using PythonAnywhere API to reload the web app without using the web interface 2 | 3 | import requests 4 | import datosconexion as dt 5 | 6 | response = requests.post(f'https://www.pythonanywhere.com/api/v0/user/{dt.username}/webapps/{dt.domain_name}/reload/', headers={'Authorization': f'Token {dt.api_token}'}) 7 | 8 | if response.status_code == 200: 9 | print('reloaded OK') 10 | else: 11 | print('Got unexpected status code {}: {!r}'.format(response.status_code, response.content)) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mini Frontend Codo a Codo 8 | 9 | 10 | 11 |
12 |
13 | logo 14 |
15 |

Mini Frontend Codo a Codo


16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Listar todos los productos
Dar de altas productos
Modificar datos de algun productos
Eliminar productos
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /static/css/estilos.css: -------------------------------------------------------------------------------- 1 | /* Estilos para todo el proyecto */ 2 | *{ 3 | font-family: 'Roboto', sans-serif; 4 | margin: 0; 5 | } 6 | 7 | body { 8 | background-color: lightskyblue; 9 | color:white; 10 | } 11 | 12 | .contenedor-centrado { 13 | display:flex; 14 | width: 100%; 15 | justify-content: center; 16 | } 17 | 18 | .logo-centrado{ 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | margin-bottom: 10px; 23 | margin-top: 10px; 24 | 25 | } 26 | 27 | .logo-centrado img { 28 | border-radius: 10px; 29 | } 30 | 31 | .navbar-index{ 32 | margin: 0; 33 | padding: 0; 34 | 35 | top: 0; 36 | width: 100vw; 37 | height: 100px; 38 | background-color: #390E8C; 39 | display: flex; 40 | justify-content: flex-start; 41 | align-items: center; 42 | 43 | overflow: hidden; 44 | margin-bottom: 20px; 45 | position: fixed !important; 46 | box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.5); 47 | } 48 | 49 | .logo-nav{ 50 | height: 80%; 51 | overflow: hidden; 52 | margin-left: 1em; 53 | } 54 | 55 | p { 56 | font-family: 'Times New Roman', Times, serif; 57 | background-color: #f9f9f9; 58 | max-width: 400px; 59 | margin: 0 auto; 60 | padding: 20px; 61 | } 62 | 63 | h1, h2 { 64 | text-align: center; 65 | color: #1d1a39; 66 | } 67 | 68 | form { 69 | max-width: 400px; 70 | margin: 0 auto; 71 | padding: 20px; 72 | background-color: #1d1a39; 73 | border: 1px solid lightslategray; 74 | border-radius: 5px; 75 | box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.5); 76 | border-radius: 10px; 77 | } 78 | 79 | table { 80 | max-width: 90%; 81 | margin: 0 auto; 82 | padding: 20px; 83 | background-color: #1d1a39; 84 | border: 1px solid lightslategray; 85 | border-radius: 5px; 86 | box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.5); 87 | border-radius: 10px; 88 | } 89 | 90 | label { 91 | display: block; 92 | margin-bottom: 5px; 93 | } 94 | 95 | input[type="text"], 96 | input[type="number"], 97 | textarea { 98 | width: 90%; 99 | padding: 10px; 100 | margin-bottom: 10px; 101 | border: 1px solid #cccccc; 102 | border-radius: 5px; 103 | } 104 | 105 | input[type="submit"] { 106 | padding: 10px; 107 | background-color: #007bff; 108 | color: #ffffff; 109 | border: none; 110 | border-radius: 5px; 111 | cursor: pointer; 112 | } 113 | 114 | input[type="submit"]:hover { 115 | background-color: #0056b3; 116 | } 117 | 118 | button { 119 | padding: 8px; 120 | margin:4px; 121 | background-color: #007bff; 122 | color: #ffffff; 123 | border: none; 124 | border-radius: 5px; 125 | cursor: pointer; 126 | } 127 | 128 | button:hover { 129 | background-color: #0056b3; 130 | } 131 | 132 | a { 133 | padding: 8px; 134 | margin:4px; 135 | background-color: rgb(248, 202, 53); 136 | color: #1d1a39; 137 | border: 2px #1d1a39 dashed; 138 | border-radius: 10px; 139 | cursor: pointer; 140 | text-decoration: none; 141 | font-size: 100%; 142 | font-weight: 600; 143 | } 144 | 145 | a:hover { 146 | background-color: #1d1a39; 147 | border: 1px white solid; 148 | color: white; 149 | } -------------------------------------------------------------------------------- /listado.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Listado de Productos 9 | 10 | 11 | 12 | 13 |
14 | logo 15 |
16 |

Listado de Productos del Inventario


17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
CódigoDescripciónCantidadPrecioImagenProveedor

32 | 33 |
34 | Menu principal 35 |
36 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /altas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Agregar producto 9 | 10 | 11 | 12 | 13 |
14 | logo 15 |
16 |

Agregar Productos al Inventario


17 | 18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 |

33 | 34 | 35 | 36 | 37 | Menu principal 38 |
39 | 40 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /listadoEliminar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Eliminar Productos 9 | 10 | 11 | 12 | 13 |
14 | logo 15 |
16 |

Eliminar Productos del Inventario


17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
CódigoDescripciónCantidadPrecioAcciones

31 | 32 |
33 | Menu principal 34 |
35 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------------------- 2 | # Instalar con pip install Flask 3 | from flask import Flask, request, jsonify 4 | 5 | # Instalar con pip install flask-cors 6 | from flask_cors import CORS 7 | 8 | # Instalar con pip install mysql-connector-python 9 | import mysql.connector 10 | 11 | # Si es necesario, pip install Werkzeug 12 | from werkzeug.utils import secure_filename 13 | 14 | # No es necesario instalar, es parte del sistema standard de Python 15 | import os 16 | import time 17 | import datosconexion as bd 18 | #-------------------------------------------------------------------- 19 | 20 | app = Flask(__name__) 21 | CORS(app) # Esto habilitará CORS para todas las rutas 22 | 23 | class Catalogo: 24 | #---------------------------------------------------------------- 25 | # Constructor de la clase 26 | def __init__(self, host, user, password, database): 27 | self.conn = mysql.connector.connect( 28 | host=host, 29 | user=user, 30 | password=password 31 | ) 32 | self.cursor = self.conn.cursor() 33 | # Intentamos seleccionar la base de datos 34 | try: 35 | self.cursor.execute(f"USE {database}") 36 | except mysql.connector.Error as err: 37 | # Si la base de datos no existe, la creamos 38 | if err.errno == mysql.connector.errorcode.ER_BAD_DB_ERROR: 39 | self.cursor.execute(f"CREATE DATABASE {database}") 40 | self.conn.database = database 41 | else: 42 | raise err 43 | 44 | self.cursor.execute('''CREATE TABLE IF NOT EXISTS productos ( 45 | codigo INT AUTO_INCREMENT PRIMARY KEY, 46 | descripcion VARCHAR(255) NOT NULL, 47 | cantidad INT NOT NULL, 48 | precio DECIMAL(10, 2) NOT NULL, 49 | imagen_url VARCHAR(255), 50 | proveedor INT(4))''') 51 | self.conn.commit() 52 | 53 | # Cerrar el cursor inicial y abrir uno nuevo con el parámetro dictionary=True 54 | self.cursor.close() 55 | self.cursor = self.conn.cursor(dictionary=True) 56 | 57 | def listar_productos(self): 58 | self.cursor.execute("SELECT * FROM productos") 59 | productos = self.cursor.fetchall() 60 | return productos 61 | 62 | def consultar_producto(self, codigo): 63 | # Consultamos un producto a partir de su código 64 | self.cursor.execute(f"SELECT * FROM productos WHERE codigo = {codigo}") 65 | return self.cursor.fetchone() 66 | 67 | def agregar_producto(self, descripcion, cantidad, precio, imagen, proveedor): 68 | sql = "INSERT INTO productos (descripcion, cantidad, precio, imagen_url, proveedor) VALUES (%s, %s, %s, %s, %s)" 69 | valores = (descripcion, cantidad, precio, imagen, proveedor) 70 | 71 | self.cursor.execute(sql,valores) 72 | self.conn.commit() 73 | return self.cursor.lastrowid 74 | 75 | def modificar_producto(self, codigo, nueva_descripcion, nueva_cantidad, nuevo_precio, nueva_imagen, nuevo_proveedor): 76 | sql = "UPDATE productos SET descripcion = %s, cantidad = %s, precio = %s, imagen_url = %s, proveedor = %s WHERE codigo = %s" 77 | valores = (nueva_descripcion, nueva_cantidad, nuevo_precio, nueva_imagen, nuevo_proveedor, codigo) 78 | 79 | self.cursor.execute(sql, valores) 80 | self.conn.commit() 81 | return self.cursor.rowcount > 0 82 | 83 | def eliminar_producto(self, codigo): 84 | # Eliminamos un producto de la tabla a partir de su código 85 | self.cursor.execute(f"DELETE FROM productos WHERE codigo = {codigo}") 86 | self.conn.commit() 87 | return self.cursor.rowcount > 0 88 | 89 | #-------------------------------------------------------------------- 90 | # Cuerpo del programa 91 | #-------------------------------------------------------------------- 92 | # Crear una instancia de la clase Catalogo 93 | 94 | catalogo = Catalogo(host=bd.host, user=bd.user, password=bd.password, database=bd.database) 95 | # las variables con los datos de la conexion estan guardadas en el archivo datosconexion.py 96 | 97 | 98 | # Carpeta para guardar las imagenes 99 | ruta_destino = './static/imagenes/' # Reemplazar por los datos de Pythonanywhere 100 | 101 | #ruta_destino = '/home/maxisimonazzi/mysite/static/imagenes/' 102 | 103 | @app.route("/productos", methods=["GET"]) 104 | def listar_productos(): 105 | productos = catalogo.listar_productos() 106 | return jsonify(productos) 107 | 108 | @app.route("/productos/", methods=["GET"]) 109 | def mostrar_producto(codigo): 110 | producto = catalogo.consultar_producto(codigo) 111 | if producto: 112 | return jsonify(producto) 113 | else: 114 | return "Producto no encontrado", 404 115 | 116 | @app.route("/productos", methods=["POST"]) 117 | def agregar_producto(): 118 | #Recojo los datos del form 119 | descripcion = request.form['descripcion'] 120 | cantidad = request.form['cantidad'] 121 | precio = request.form['precio'] 122 | imagen = request.files['imagen'] 123 | proveedor = request.form['proveedor'] 124 | nombre_imagen = "" 125 | 126 | # Genero el nombre de la imagen 127 | nombre_imagen = secure_filename(imagen.filename) 128 | nombre_base, extension = os.path.splitext(nombre_imagen) 129 | nombre_imagen = f"{nombre_base}_{int(time.time())}{extension}" 130 | 131 | nuevo_codigo = catalogo.agregar_producto(descripcion, cantidad, precio, nombre_imagen, proveedor) 132 | if nuevo_codigo: 133 | imagen.save(os.path.join(ruta_destino, nombre_imagen)) 134 | return jsonify({"mensaje": "Producto agregado correctamente.", "codigo": nuevo_codigo, "imagen": nombre_imagen}), 201 135 | else: 136 | return jsonify({"mensaje": "Error al agregar el producto."}), 500 137 | 138 | @app.route("/productos/", methods=["PUT"]) 139 | def modificar_producto(codigo): 140 | #Se recuperan los nuevos datos del formulario 141 | nueva_descripcion = request.form.get("descripcion") 142 | nueva_cantidad = request.form.get("cantidad") 143 | nuevo_precio = request.form.get("precio") 144 | nuevo_proveedor = request.form.get("proveedor") 145 | 146 | # Verifica si se proporcionó una nueva imagen 147 | if 'imagen' in request.files: 148 | imagen = request.files['imagen'] 149 | # Procesamiento de la imagen 150 | nombre_imagen = secure_filename(imagen.filename) 151 | nombre_base, extension = os.path.splitext(nombre_imagen) 152 | nombre_imagen = f"{nombre_base}_{int(time.time())}{extension}" 153 | 154 | # Guardar la imagen en el servidor 155 | imagen.save(os.path.join(ruta_destino, nombre_imagen)) 156 | 157 | # Busco el producto guardado 158 | producto = catalogo.consultar_producto(codigo) 159 | if producto: # Si existe el producto... 160 | imagen_vieja = producto["imagen_url"] 161 | # Armo la ruta a la imagen 162 | ruta_imagen = os.path.join(ruta_destino, imagen_vieja) 163 | 164 | # Y si existe la borro. 165 | if os.path.exists(ruta_imagen): 166 | os.remove(ruta_imagen) 167 | else: 168 | producto = catalogo.consultar_producto(codigo) 169 | if producto: 170 | nombre_imagen = producto["imagen_url"] 171 | 172 | # Se llama al método modificar_producto pasando el codigo del producto y los nuevos datos. 173 | if catalogo.modificar_producto(codigo, nueva_descripcion, nueva_cantidad, nuevo_precio, nombre_imagen, nuevo_proveedor): 174 | return jsonify({"mensaje": "Producto modificado"}), 200 175 | else: 176 | return jsonify({"mensaje": "Producto no encontrado"}), 404 177 | 178 | @app.route("/productos/", methods=["DELETE"]) 179 | def eliminar_producto(codigo): 180 | # Primero, obtiene la información del producto para encontrar la imagen 181 | producto = catalogo.consultar_producto(codigo) 182 | if producto: 183 | # Eliminar la imagen asociada si existe 184 | ruta_imagen = os.path.join(ruta_destino, producto['imagen_url']) 185 | if os.path.exists(ruta_imagen): 186 | os.remove(ruta_imagen) 187 | 188 | # Luego, elimina el producto del catálogo 189 | if catalogo.eliminar_producto(codigo): 190 | return jsonify({"mensaje": "Producto eliminado"}), 200 191 | else: 192 | return jsonify({"mensaje": "Error al eliminar el producto"}), 500 193 | else: 194 | return jsonify({"mensaje": "Producto no encontrado"}), 404 195 | 196 | 197 | if __name__ == "__main__": 198 | app.run(debug=True) -------------------------------------------------------------------------------- /app_closingconnection.py: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------------------- 2 | # Instalar con pip install Flask 3 | from flask import Flask, request, jsonify 4 | 5 | # Instalar con pip install flask-cors 6 | from flask_cors import CORS 7 | 8 | # Instalar con pip install mysql-connector-python 9 | import mysql.connector 10 | 11 | # Si es necesario, pip install Werkzeug 12 | from werkzeug.utils import secure_filename 13 | 14 | # No es necesario instalar, es parte del sistema standard de Python 15 | import os 16 | import time 17 | import datosconexion as bd 18 | #-------------------------------------------------------------------- 19 | 20 | app = Flask(__name__) 21 | CORS(app) # Esto habilitará CORS para todas las rutas 22 | 23 | class Catalogo: 24 | #---------------------------------------------------------------- 25 | # Constructor de la clase 26 | 27 | def __init__(self): 28 | self.conn = mysql.connector.connect( 29 | host=bd.host, 30 | user=bd.user, 31 | password=bd.password 32 | ) 33 | self.cursor = self.conn.cursor() 34 | # Intentamos seleccionar la base de datos 35 | try: 36 | self.cursor.execute(f"USE {bd.database}") 37 | except mysql.connector.Error as err: 38 | # Si la base de datos no existe, la creamos 39 | if err.errno == mysql.connector.errorcode.ER_BAD_DB_ERROR: 40 | self.cursor.execute(f"CREATE DATABASE {bd.database}") 41 | self.conn.database = bd.database 42 | else: 43 | raise err 44 | 45 | self.cursor.execute('''CREATE TABLE IF NOT EXISTS productos ( 46 | codigo INT AUTO_INCREMENT PRIMARY KEY, 47 | descripcion VARCHAR(255) NOT NULL, 48 | cantidad INT NOT NULL, 49 | precio DECIMAL(10, 2) NOT NULL, 50 | imagen_url VARCHAR(255), 51 | proveedor INT(4))''') 52 | self.conn.commit() 53 | 54 | # Cerrar el cursor inicial y abrir uno nuevo con el parámetro dictionary=True 55 | self.cursor.close() 56 | self.conn.close() 57 | 58 | def conectar(self): 59 | try: 60 | self.conn = mysql.connector.connect( 61 | host=bd.host, 62 | user=bd.user, 63 | password=bd.password, 64 | database=bd.database 65 | ) 66 | self.cursor = self.conn.cursor(dictionary=True) 67 | return "OK" 68 | except: 69 | return "Error al conectar con la base de datos." 70 | 71 | def desconectar(self): 72 | try: 73 | self.cursor.close() 74 | self.conn.close() 75 | return "OK" 76 | except: 77 | return "Error al cerrar la conexión." 78 | 79 | def listar_productos(self): 80 | self.conectar() 81 | self.cursor.execute("SELECT * FROM productos") 82 | productos = self.cursor.fetchall() 83 | self.desconectar() 84 | return productos 85 | 86 | def consultar_producto(self, codigo): 87 | # Consultamos un producto a partir de su código 88 | self.conectar() 89 | self.cursor.execute(f"SELECT * FROM productos WHERE codigo = {codigo}") 90 | self.desconectar() 91 | return self.cursor.fetchone() 92 | 93 | def mostrar_producto(self, codigo): 94 | # Mostramos los datos de un producto a partir de su código 95 | producto = self.consultar_producto(codigo) 96 | if producto: 97 | print("-" * 40) 98 | print(f"Código.....: {producto['codigo']}") 99 | print(f"Descripción: {producto['descripcion']}") 100 | print(f"Cantidad...: {producto['cantidad']}") 101 | print(f"Precio.....: {producto['precio']}") 102 | print(f"Imagen.....: {producto['imagen_url']}") 103 | print(f"Proveedor..: {producto['proveedor']}") 104 | print("-" * 40) 105 | else: 106 | print("Producto no encontrado.") 107 | 108 | def agregar_producto(self, descripcion, cantidad, precio, imagen, proveedor): 109 | self.conectar() 110 | sql = "INSERT INTO productos (descripcion, cantidad, precio, imagen_url, proveedor) VALUES (%s, %s, %s, %s, %s)" 111 | valores = (descripcion, cantidad, precio, imagen, proveedor) 112 | 113 | self.cursor.execute(sql,valores) 114 | self.conn.commit() 115 | self.desconectar() 116 | return self.cursor.lastrowid 117 | 118 | def modificar_producto(self, codigo, nueva_descripcion, nueva_cantidad, nuevo_precio, nueva_imagen, nuevo_proveedor): 119 | self.conectar() 120 | sql = "UPDATE productos SET descripcion = %s, cantidad = %s, precio = %s, imagen_url = %s, proveedor = %s WHERE codigo = %s" 121 | valores = (nueva_descripcion, nueva_cantidad, nuevo_precio, nueva_imagen, nuevo_proveedor, codigo) 122 | 123 | self.cursor.execute(sql, valores) 124 | self.conn.commit() 125 | self.desconectar() 126 | return self.cursor.rowcount > 0 127 | 128 | def eliminar_producto(self, codigo): 129 | self.conectar() 130 | # Eliminamos un producto de la tabla a partir de su código 131 | self.cursor.execute(f"DELETE FROM productos WHERE codigo = {codigo}") 132 | self.conn.commit() 133 | self.desconectar() 134 | return self.cursor.rowcount > 0 135 | 136 | #-------------------------------------------------------------------- 137 | # Cuerpo del programa 138 | #-------------------------------------------------------------------- 139 | # Crear una instancia de la clase Catalogo 140 | 141 | catalogo = Catalogo() 142 | # las variables con los datos de la conexion estan guardadas en el archivo datosconexion.py 143 | 144 | 145 | # Carpeta para guardar las imagenes 146 | ruta_destino = './static/imagenes/' # Reemplazar por los datos de Pythonanywhere 147 | 148 | #ruta_destino = '/home/maxisimonazzi/mysite/static/imagenes/' 149 | 150 | @app.route("/productos", methods=["GET"]) 151 | def listar_productos(): 152 | productos = catalogo.listar_productos() 153 | return jsonify(productos) 154 | 155 | @app.route("/productos/", methods=["GET"]) 156 | def mostrar_producto(codigo): 157 | producto = catalogo.consultar_producto(codigo) 158 | if producto: 159 | return jsonify(producto) 160 | else: 161 | return "Producto no encontrado", 404 162 | 163 | @app.route("/productos", methods=["POST"]) 164 | def agregar_producto(): 165 | #Recojo los datos del form 166 | descripcion = request.form['descripcion'] 167 | cantidad = request.form['cantidad'] 168 | precio = request.form['precio'] 169 | imagen = request.files['imagen'] 170 | proveedor = request.form['proveedor'] 171 | nombre_imagen = "" 172 | 173 | # Genero el nombre de la imagen 174 | nombre_imagen = secure_filename(imagen.filename) 175 | nombre_base, extension = os.path.splitext(nombre_imagen) 176 | nombre_imagen = f"{nombre_base}_{int(time.time())}{extension}" 177 | 178 | nuevo_codigo = catalogo.agregar_producto(descripcion, cantidad, precio, nombre_imagen, proveedor) 179 | if nuevo_codigo: 180 | imagen.save(os.path.join(ruta_destino, nombre_imagen)) 181 | return jsonify({"mensaje": "Producto agregado correctamente.", "codigo": nuevo_codigo, "imagen": nombre_imagen}), 201 182 | else: 183 | return jsonify({"mensaje": "Error al agregar el producto."}), 500 184 | 185 | @app.route("/productos/", methods=["PUT"]) 186 | def modificar_producto(codigo): 187 | #Se recuperan los nuevos datos del formulario 188 | nueva_descripcion = request.form.get("descripcion") 189 | nueva_cantidad = request.form.get("cantidad") 190 | nuevo_precio = request.form.get("precio") 191 | nuevo_proveedor = request.form.get("proveedor") 192 | 193 | # Verifica si se proporcionó una nueva imagen 194 | if 'imagen' in request.files: 195 | imagen = request.files['imagen'] 196 | # Procesamiento de la imagen 197 | nombre_imagen = secure_filename(imagen.filename) 198 | nombre_base, extension = os.path.splitext(nombre_imagen) 199 | nombre_imagen = f"{nombre_base}_{int(time.time())}{extension}" 200 | 201 | # Guardar la imagen en el servidor 202 | imagen.save(os.path.join(ruta_destino, nombre_imagen)) 203 | 204 | # Busco el producto guardado 205 | producto = catalogo.consultar_producto(codigo) 206 | if producto: # Si existe el producto... 207 | imagen_vieja = producto["imagen_url"] 208 | # Armo la ruta a la imagen 209 | ruta_imagen = os.path.join(ruta_destino, imagen_vieja) 210 | 211 | # Y si existe la borro. 212 | if os.path.exists(ruta_imagen): 213 | os.remove(ruta_imagen) 214 | else: 215 | producto = catalogo.consultar_producto(codigo) 216 | if producto: 217 | nombre_imagen = producto["imagen_url"] 218 | 219 | # Se llama al método modificar_producto pasando el codigo del producto y los nuevos datos. 220 | if catalogo.modificar_producto(codigo, nueva_descripcion, nueva_cantidad, nuevo_precio, nombre_imagen, nuevo_proveedor): 221 | return jsonify({"mensaje": "Producto modificado"}), 200 222 | else: 223 | return jsonify({"mensaje": "Producto no encontrado"}), 403 224 | 225 | @app.route("/productos/", methods=["DELETE"]) 226 | def eliminar_producto(codigo): 227 | # Primero, obtiene la información del producto para encontrar la imagen 228 | producto = catalogo.consultar_producto(codigo) 229 | if producto: 230 | # Eliminar la imagen asociada si existe 231 | ruta_imagen = os.path.join(ruta_destino, producto['imagen_url']) 232 | if os.path.exists(ruta_imagen): 233 | os.remove(ruta_imagen) 234 | 235 | # Luego, elimina el producto del catálogo 236 | if catalogo.eliminar_producto(codigo): 237 | return jsonify({"mensaje": "Producto eliminado"}), 200 238 | else: 239 | return jsonify({"mensaje": "Error al eliminar el producto"}), 500 240 | else: 241 | return jsonify({"mensaje": "Producto no encontrado"}), 404 242 | 243 | if __name__ == "__main__": 244 | app.run(debug=True) -------------------------------------------------------------------------------- /modificaciones.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Modificar Producto 9 | 10 | 11 | 12 | 13 |
14 | logo 15 |
16 |

Modificar Productos del Inventario


17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | Menu principal 25 |
26 | 27 | 28 | 57 |
58 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Proyecto de BackEnd para el programa Codo a Codo 2 | 3 | ![Codo a Codo](./static/imagenes/logo_Codo.jpg) 4 | 5 | --- 6 | 7 | **Tabla de contenido** 8 | 9 | - [Proyecto de BackEnd para el programa Codo a Codo](#proyecto-de-backend-para-el-programa-codo-a-codo) 10 | - [Descripción general](#descripción-general) 11 | - [Clonando el repositorio](#clonando-el-repositorio) 12 | - [Configuración](#configuración) 13 | - [Funcionamiento](#funcionamiento) 14 | - [BackEnd](#backend) 15 | - [API](#api) 16 | - [Base URL](#base-url) 17 | - [Endpoints](#endpoints) 18 | - [`GET` `/productos` `(Lista todos los productos cargados y sus propiedades)`](#get-productos-lista-todos-los-productos-cargados-y-sus-propiedades) 19 | - [`GET` `/productos/` `(Muestra las propiedades del producto seleccionado)`](#get-productosintcodigo-muestra-las-propiedades-del-producto-seleccionado) 20 | - [`POST` `/productos` `(Agrega un producto a la lista)`](#post-productos-agrega-un-producto-a-la-lista) 21 | - [`PUT` `/productos/` `(Modifica el producto seleccionado)`](#put-productosintcodigo-modifica-el-producto-seleccionado) 22 | - [`DELETE` `/productos/` `(Elimina el producto seleccionado)`](#delete-productosintcodigo-elimina-el-producto-seleccionado) 23 | - [Cerrando conexiones a la Base de datos](#cerrando-conexiones-a-la-base-de-datos) 24 | - [Usando API Key de PythonAnywhere para recargar el sitio](#usando-api-key-de-pythonanywhere-para-recargar-el-sitio) 25 | - [Endpoint](#endpoint) 26 | - [`POST` `/api/v0/user/{tu_usuario}/webapps/{tu_dominio}/reload/` `(Recarga la pagina web.)`](#post-apiv0usertu_usuariowebappstu_dominioreload-recarga-la-pagina-web) 27 | - [FrontEnd](#frontend) 28 | - [index.html](#indexhtml) 29 | - [listado.html](#listadohtml) 30 | - [altas.html](#altashtml) 31 | - [modificaciones.html](#modificacioneshtml) 32 | - [listadoEliminar.html](#listadoeliminarhtml) 33 | 34 | 35 | ## Descripción general 36 | 37 | El proyecto se trata de un sistema que permite administrar una base de datos de productos, implementado una API en Python utilizando el framework Flask. Las operaciones que se realizarán en este proyecto es dar de alta, modificar, eliminar y listar los productos, operaciones que podrán hacer los usuarios a través de una página Web. 38 | 39 | Como ejemplo trabajaremos con una empresa de venta de artículos de computación que ofrece sus productos a través de una Web. 40 | 41 | Aquí hay un resumen de las principales características y funcionalidades del proyecto: 42 | 43 | **Gestión de productos:** 44 | - Agregar un nuevo producto al catálogo. 45 | - Mostrar un listado de los productos en el catálogo. 46 | - Modificar la información de un producto existente en el catálogo. 47 | - Eliminar un producto del catálogo. 48 | 49 | **Persistencia de datos:** 50 | - Los datos de los productos se almacenan en una base de datos SQL. 51 | - Se utiliza una conexión a la base de datos para realizar operaciones CRUD en los productos. 52 | - El código proporcionado incluye las clases Catálogo, que representa la estructura y funcionalidad relacionada con el catálogo de productos. Además, se define una serie de rutas en Flask para manejar las solicitudes HTTP relacionadas con la gestión de productos. 53 | 54 | Se implementan desde cero el backend y el frontend. En el caso del backend, el proyecto va "evolucionando", comenzando en el desarrollo de las funciones que se necesitan para manipular los productos utilizando arreglos en memoria, luego se modifica para utilizar objetos, más tarde se gestiona la persistencia de los datos utilizando una base de datos SQL, se aloja el script Python en un servidor, y por último se crea un frontend básico para interactuar con los datos desde el navegador, a través de la API creada. Para este primer proyecto de BackEnd no vamos a utilizar entornos virtuales de ningun tipo ya que el proposito es solo entender los conceptos. 55 | 56 | 57 | El proyecto se divide en seis etapas: 58 | 59 | 1) Desarrollo de arreglos y funciones: Implementar un CRUD de productos utilizando arreglos y funciones. 60 | 2) Conversión a clases y objetos: Convertir las funciones vistas en objetos y clases. 61 | 3) Creación de la base de datos SQL: Utilizar como almacenamiento una base de datos SQL. 62 | 4) Implementación de la API en Flask: A través de este framework implementar una API que permita ser consumida desde el front. 63 | 5) Codificación del Front-End: Vistas que permitan realizar las operaciones del CRUD. 64 | 6) Despliegue en servidor PythonAnywhere: Hosting para aplicaciones web escritas en Python. 65 | 66 | ## Clonando el repositorio 67 | 68 | Para clonar este repo localmente, ejecuta en la terminal el siguiente comando. 69 | 70 | ```properties 71 | git clone https://github.com/maxisimonazzi/proyecto-fullstack-codo2024-1ercuatrimestre.git 72 | ``` 73 | 74 | Si queres replicar este repo en tu cuenta de github podes hacer un fork al proyecto o sino crear un nuevo repo en tu perfil y luego agregar los archivos de tu repo local. 75 | 76 | ```properties 77 | #En tu repo local 78 | git init 79 | git add . 80 | git commit -m "primer commit" 81 | git branch -M main 82 | git remote add origin https://github.com/{tu_usuario}/tu_repo.git 83 | git push -u origin main 84 | ``` 85 | 86 | ## Configuración 87 | 88 | La app hace uso del archivo **`datosconexion.py`** para obtener las variables de conexion a la base de datos y la API de PythonAnywhere. En el repo vas a encontrar el archivo **`datosconexion-template.py`** que tiene la siguiente estructura 89 | 90 | ```Python 91 | # Datos de acceso a la Base de Datos 92 | host='host' 93 | user='user' 94 | password='pass' 95 | database='database' 96 | 97 | # Credenciales API PythonAnywhere 98 | username = "xxxxxxx" 99 | api_token = "api_token" 100 | domain_name = "xxxxxxx.pythonanywhere.com" 101 | ``` 102 | 103 | Reemplazar los datos para conectar con la base de datos segun corresponda, de igual manera los datos de la API Key (si se utiliza) y renombrar el archivo a **`datosconexion.py`** para que pueda ser importado correctamente por la app. 104 | 105 | Para obtener los datos de acceso a la Base de datos en PythonAnywhere nos dirigimos a la seccion Database 106 | 107 | ![alt text](./static/imagenes/dbpythonanywhere.png) 108 | 109 | Para obtener la API Key en PythonAnywhere 110 | 111 | ![alt text](./static/imagenes/apikeypythonanywhere.png) 112 | 113 | ## Funcionamiento 114 | 115 | Explicaremos el funcionamiento dividido en dos partes, el FrontEnd y el BackEnd. 116 | 117 | ### BackEnd 118 | 119 | El archivo **`app.py`** contiene todo el backend sin modularizar. Lo primero que hay que hacer es especificar la ruta correcta donde seran guardadas las imagenes 120 | 121 | ```properties 122 | ruta_destino = './static/imagenes/' 123 | ``` 124 | 125 | Esta debe ser cambiada por la ruta a utilizar en PythonAnywhere, que sera del tipo: 126 | 127 | ```properties 128 | ruta_destino = '/home/{tu_usuario}/mysite/static/imagenes/' 129 | ``` 130 | 131 | Una vez realizado este cambio, podemos ver los metodos de la clase Catalogo como asi tambien los endpoints definidos en Flask. 132 | 133 | #### API 134 | 135 | ##### Base URL 136 | 137 | La URL base para tu API sera la que te entregue PythonAnywhere y dependera de tu nombre de usuario: 138 | 139 | ```properties 140 | https://{tu_usuario}.pythonanywhere.com/ 141 | ``` 142 | 143 | ##### Endpoints 144 | 145 |
146 | 147 | 148 | ###### `GET` `/productos` `(Lista todos los productos cargados y sus propiedades)` 149 | 150 | 151 | ###### Parametros 152 | 153 | > Ninguno 154 | 155 | ###### Respuesta 156 | 157 | > | Código HTTP | content-type | Respuesta | 158 | > | :---------- | :----------: | :-------: | 159 | > | 200 | application/json | JSON | 160 | 161 | ###### Ejemplo con cURL 162 | ```bash 163 | curl -X GET -H "Content-Type: application/json" https://{tu_usuario}.pythonanywhere.com/productos 164 | ``` 165 |
166 | 167 |
168 | 169 | 170 | ###### `GET` `/productos/` `(Muestra las propiedades del producto seleccionado)` 171 | 172 | 173 | ###### Parametros 174 | 175 | > Ninguno 176 | 177 | ###### Respuesta 178 | 179 | > | Código HTTP | content-type | Respuesta | 180 | > | :---------- | :----------: | :-------: | 181 | > | 200 | application/json | JSON | 182 | > | 404 | application/json | JSON | 183 | 184 | ###### Ejemplo con cURL 185 | ```bash 186 | curl -X GET -H "Content-Type: application/json" https://{tu_usuario}.pythonanywhere.com/productos/1 187 | ``` 188 |
189 | 190 |
191 | 192 | 193 | ###### `POST` `/productos` `(Agrega un producto a la lista)` 194 | 195 | 196 | ###### Parametros 197 | 198 | > | Nombre | Tipo | Tipo de dato | Descripción | 199 | > | :---------- | :----------: | :-------: | :------- | 200 | > | descripcion | requerido | string | Descripcion del producto | 201 | > | cantidad | requerido | int | Cantidad del producto a agregar | 202 | > | precio | requerido | float | Precio del producto a agregar | 203 | > | proveedor | requerido | int | Codigo del proveedor del producto a agregar | 204 | > | imagen | requerido | file | Imagen del producto a agregar | 205 | 206 | ###### Respuesta 207 | 208 | > | Código HTTP | content-type | Respuesta | 209 | > | :----------: | :----------: | :-------: | 210 | > | 201 | application/json | JSON | 211 | > | 400 | text/html; charset=utf-8 | None | 212 | > | 500 | application/json | JSON | 213 | 214 | ###### Ejemplo con cURL 215 | ```bash 216 | curl -X POST -H 'Content-Type: multipart/form-data' -F 'descripcion=Descripción del producto' -F 'cantidad=5' -F 'precio=10000' -F 'proveedor=123' -F imagen=@./static/imagenes/img.jpg https://{tu_usuario}.pythonanywhere.com/productos 217 | ``` 218 |
219 | 220 |
221 | 222 | 223 | ###### `PUT` `/productos/` `(Modifica el producto seleccionado)` 224 | 225 | 226 | ###### Parametros 227 | 228 | > | Nombre | Tipo | Tipo de dato | Descripción | 229 | > | :---------- | :----------: | :-------: | :------- | 230 | > | descripcion | requerido | string | Descripcion del producto | 231 | > | cantidad | requerido | int | Cantidad del producto a agregar | 232 | > | precio | requerido | float | Precio del producto a agregar | 233 | > | proveedor | requerido | int | Codigo del proveedor del producto a agregar | 234 | > | imagen | requerido | file | Imagen del producto a agregar | 235 | 236 | ###### Respuesta 237 | 238 | > | Código HTTP | content-type | Respuesta | 239 | > | :----------: | :----------: | :-------: | 240 | > | 200 | application/json | JSON | 241 | > | 404 | application/json | JSON | 242 | 243 | ###### Ejemplo con cURL 244 | ```bash 245 | curl -X PUT -H 'Content-Type: multipart/form-data' -F 'descripcion=Descripción del producto' -F 'cantidad=5' -F 'precio=10000' -F 'proveedor=123' -F imagen=@./static/imagenes/img.jpg https://{tu_usuario}.pythonanywhere.com/productos 246 | ``` 247 |
248 | 249 |
250 | 251 | 252 | ###### `DELETE` `/productos/` `(Elimina el producto seleccionado)` 253 | 254 | 255 | ###### Parametros 256 | 257 | > Ninguno 258 | 259 | ###### Respuesta 260 | 261 | > | Código HTTP | content-type | Respuesta | 262 | > | :----------: | :----------: | :-------: | 263 | > | 200 | application/json | JSON | 264 | > | 404 | application/json | JSON | 265 | 266 | ###### Ejemplo con cURL 267 | ```bash 268 | curl -X DELETE -H "Content-Type: application/json" https://{tu_usuario}.pythonanywhere.com/productos/1 269 | ``` 270 |
271 | 272 | #### Cerrando conexiones a la Base de datos 273 | 274 | Por cuestionas meramente pedagogicas, en el archivo **`app.py`** se abre una conexion a la Base de datos para luego ejecutar todas las consultas necesarias. Esta conexion no se cierra nunca, y como es de esperarse, el motor de Base de datos cierra automaticamente la conexion luego de un tiempo de inactividad, por lo que la app devuelve un error cuando intenta consultar la base de datos. 275 | 276 | Para sobrellevar esto, implementamos la alternativa de abrir una conexion antes de ejecutar cualquier consulta y cerrarla luego de terminarla, de esta manera, las conexiones son efimeras y solo se conectara cuando se necesite hacer una consulta. Esto lo tenemos resuelto dentro del archivo **`app_closingconnection.py`**. 277 | 278 | Alli veremos que la clase Catalogo tiene dos metodos nuevos, uno llamado **`conectar`** y otro llamado **`desconectar`**. El primero abre una conexion a la Base de datos y el segundo cierra la conexion a la Base de datos. Al momento de ejecutar una consulta, primero llamamos al metodo de conectar y luego al metodo desconectar. 279 | 280 | #### Usando API Key de PythonAnywhere para recargar el sitio 281 | 282 | Muchas veces, la app alojada en PythonAnywhere deja de responder, ya sea por una limitacion de los recursos gratuitos o por que no se usa la implementacion de cerrar la conexion a la base de datos. En esas situaciones la pagina debe recargarse en PythonAnywhere haciendo click en el boton **`Reload`**. Esto implica tener que dirigirnos a la pagina, loguearnos y luego buscar el boton reload de forma manual, lo cual puede ser tedioso. 283 | 284 | Para solucionar esto, podemos realizar este procedimiento haciendo uso de la API de PythonAnywhere y hacer la recarga directamente desde una peticion. 285 | 286 | ##### Endpoint 287 | 288 |
289 | 290 | 291 | ###### `POST` `/api/v0/user/{tu_usuario}/webapps/{tu_dominio}/reload/` `(Recarga la pagina web.)` 292 | 293 | 294 | ###### Parametros 295 | 296 | > Ninguno 297 | 298 | ###### Cabcera 299 | 300 | > La cabecera a mandar debe ser 'Authorization: Token {tu_token}' 301 | 302 | ###### Respuesta 303 | 304 | > | Código HTTP | content-type | Respuesta | 305 | > | :----------: | :----------: | :-------: | 306 | > | 200 | application/json | JSON | 307 | 308 | ###### Ejemplo con cURL 309 | ```bash 310 | curl -X POST -H 'Authorization: Token {tu_token}' https://www.pythonanywhere.com/api/v0/user/{tu_usuario}/webapps/{nomre_de_dominio}/reload/ 311 | ``` 312 |
313 | 314 | La documentacion de la API la encuentran en el siguiente link https://help.pythonanywhere.com/pages/API/ 315 | 316 | ### FrontEnd 317 | 318 | Los archivos que componen el mini FrontEnd son 5. 319 | 320 | ```properties 321 | - index.html 322 | - listado.html 323 | - altas.html 324 | - modificaciones.html 325 | - listadoEliminar.html 326 | ``` 327 | 328 | #### index.html 329 | 330 | Esta página actúa como un menú o punto de entrada para distintas funciones de la aplicación web, cada una relacionada con el manejo de productos a través de una API. El uso de una tabla para organizar los enlaces proporciona una estructura clara y sencilla para la navegación. 331 | 332 | ![index.html](./static/imagenes/indexhtml.png) 333 | 334 | #### listado.html 335 | 336 | Esta página es la encargada de mostrar un listado de productos. Crea una tabla de productos con sus detalles, utilizando JavaScript para realizar una solicitud al servidor y recuperar los datos de los productos, que luego agrega dinámicamente en la tabla. El script maneja tanto la recuperación exitosa de los datos como los posibles errores que puedan surgir durante el proceso. 337 | 338 | ![listado.html](./static/imagenes/listadohtml.png) 339 | 340 | #### altas.html 341 | 342 | Aqio podremos agregar productos al Catalogo. Incluye un formulario para introducir los detalles del producto y un script de JavaScript para manejar el envío de estos datos al servidor de manera asíncrona, sin recargar la página. 343 | 344 | ![altas.html](./static/imagenes/altashtml.png) 345 | 346 | #### modificaciones.html 347 | 348 | Esta página web le permite al usuario obtener los detalles de un producto específico, modificar esos detalles y enviar los cambios al servidor. 349 | 350 | ![modificaciones.html](./static/imagenes/modificarhtml.png) 351 | 352 | #### listadoEliminar.html 353 | 354 | Esta página le permite al usuario obtener un listado de los productos, con la posibilidad de eliminarlos. 355 | 356 | ![listadoEliminar.html](./static/imagenes/listadoeliminar.png) 357 | 358 | Estos archivos html pueden ser subidos a cualquier hosting/servidor gratuito. En clases vimos el ejemplo de subirlo a Netlify. 359 | 360 | Es importante que antes de subir los archivos al servidor, modificar las rutas en los html para que las peticiones las haga a la URL de nuestro app en PythonAnywhere, para ello modificamos la constante **`URL`** de la siguiente manera 361 | 362 | ```properties 363 | const URL = "https://{tu_usuario}.pythonanywhere.com/" 364 | ``` 365 | 366 | De igual manera hay que cambiar las secciones donde tiene que buscar las imagenes de los productos, que al ser enviadas mediante un formulario a la app, las mismas son guardadas en PythonAnywhere, por ello debemos colocar su ruta. Para ello, debemos buscar en los html la siguiente parte 367 | 368 | ```properties 369 | src=./static/imagenes/ 370 | ``` 371 | 372 | Y modificarla por la correspondiente 373 | 374 | ```properties 375 | src=https://{tu_usuario}.pythonanywhere.com/static/imagenes/ 376 | ``` 377 | 378 | **Observaciones:** Dentro de los html veran dos maneras distintas de armar las tablas con los productos. Para ello se utilizan dos formas distintas de manipulacion del DOM. En el listado.html vamos a ver como se modifica el cuerpo de la tabla directamente. 379 | 380 | ```js 381 | document.getElementById('tablaProductos') 382 | ``` 383 | 384 | En cambio en el listadoeliminar.html veremos que modificamos la tabla apuntando al TagName. 385 | 386 | ```js 387 | document.getElementById('productos-table').getElementsByTagName('tbody')[0]; 388 | ``` 389 | 390 | En ambos caso, lo que hacemos es modificar el cuerpo de la tabla y el resultado es similar. --------------------------------------------------------------------------------