domingo, 17 de septiembre de 2023

Explorando el Funcionamiento de la Base de Datos SQLite3 en Python

Explorando el Funcionamiento de la Base de Datos SQLite3 en Python


En el mundo de la programación, el manejo de datos es una parte esencial de muchas aplicaciones. Ya sea que estemos creando una aplicación web, una aplicación de escritorio o incluso una aplicación móvil, es probable que necesitemos almacenar y recuperar información de manera eficiente. Una de las herramientas más populares para manejar bases de datos en Python es SQLite. En este artículo, exploraremos en detalle el funcionamiento de la base de datos SQLite en el entorno de programación de Python.


¿Qué es SQLite?


SQLite es una biblioteca escrita en lenguaje C que proporciona un sistema de gestión de bases de datos relacionales de código abierto y sin servidor. A diferencia de otras bases de datos más robustas, SQLite es una base de datos de un solo archivo, lo que significa que toda la base de datos y el motor de gestión están contenidos en un solo archivo. Esto lo hace extremadamente ligero y fácil de integrar en aplicaciones Python sin la necesidad de configuraciones complejas.


Ventajas de SQLite en Python:


1. Sin configuración compleja: SQLite no requiere configuraciones de servidor complicadas. La base de datos se maneja como un archivo local, lo que facilita su gestión y distribución junto con la aplicación.


2. Eficiencia: Aunque SQLite es una base de datos de un solo archivo, ofrece un rendimiento sorprendentemente rápido para muchas aplicaciones. Es ideal para proyectos más pequeños o aplicaciones que no requieren una escalabilidad extrema.


3. Compatibilidad: Python tiene una integración incorporada con SQLite, lo que significa que no es necesario instalar bibliotecas adicionales. La mayoría de las distribuciones de Python vienen con la biblioteca SQLite lista para usar.


4. Transacciones ACID: SQLite garantiza la integridad de los datos utilizando el concepto de transacciones ACID (Atomicidad, Consistencia, Aislamiento y Durabilidad), lo que garantiza que las operaciones sean seguras y confiables.


Trabajando con SQLite en Python:


Esquema de funcionamiento:

1.- Importar la biblioteca.
2.- Abrir o Crear la conexión.
3.- Crear un cursor o puntero.
4.- Ejecutar una consulta o query SQL.
5.- Manejar los resultados de la query o consulta. CRUD (create, read, update and delete)
6.- Cerrar la conexión y el puntero.

En una base de datos de este tipo los datos se guardan en tablas. Imaginemos que tenemos una pequeña biblioteca y queremos llevar un registro de los libros que tenemos. En este caso nuestra entidad, el libro, tiene un montón de atributos que queremos guardar. (Titulo, Año, Precio, Descripción). Pues bien nuestra tabla de libros tendrá una serie de filas y columnas. En las columnas estarán los atributos de cada libro y en las filas guardaremos cada registro, es decir los atributos de cada libro.


A continuación, se presentan los pasos básicos para trabajar con la base de datos SQLite en Python:


1. Importar la biblioteca: Comenzamos importando el módulo 'sqlite3', que proporciona las funciones y clases necesarias para interactuar con la base de datos. Como hemos dicho antes forma parte de la biblioteca estándar de Python por lo que no hay que instalar ningún módulo con con 'pip'.


import sqlite3


2. Conectar a la base de datos: Usamos la función 'connect()' para establecer una conexión con la base de datos o crear una nueva si no existe.


conexion = sqlite3.connect('mi_base.db')


La primera vez que ejecutemos el programa, como la base de datos no existe la creará. Lo que veremos será un archivo llamado 'mi_base.db' que estará en el mismo directorio que la aplicación.


A partir de aquí vamos a usar el lenguaje SQL que es un lenguaje que se usa para manejar bases de datos. Más adelante lo veremos en mucho más detalle como funciona pero ahora mismo lo que pretendemos es ver una visión general de como usar sqlite con python.


3. Crear una tabla: Podemos ejecutar comandos SQL para crear tablas en la base de datos.

Podemos ejecutar instrucciones SQL directamente utilizando el objeto 'conexion' sin necesidad de usar un cursor ('c' en el ejemplo siguiente). Cuando ejecutamos instrucciones SQL a través del objeto de conexión, SQLITE automáticamente crea un cursor en segundo plano para ejecutar la consulta. Sin embargo es una buena práctica crear un cursos explícitamente para tener un mayor control sobre las operaciones y para manejar múltiples consultas en una transacción.


try:
    c.execute('''CREATE TABLE libros (
        id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
        título TEXT,
        año INTEGER,
        precio FLOAT,
        descripción TEXT);''')
    print("Tabla de libros creada con éxito")
    
except sqlite3.OperationalError as error:
    print(error)


Antes de seguir vamos a comentar brevemente que es un campo 'Primary Key'. Se trata de un elemento que sirve para identificar cada registro. Tienen una seria de características:

- Ningún registro puede tener la misma 'Primary Key' o clave primaria.

- La 'Primary Key' nunca se vuelve a reutilizar.

- Una 'Primary Key' no se puede modificar una vez creada.

- Una 'Primary Key' debe tener siempre un valor, no puede ser un valor nulo 'NULL'.

En el ejemplo de arriba creamos un campo llamado id, que será un número entero único para cada registro, en el que si nosotros no lo introducimos, automáticamente el programa lo hará por nosotros y además, esos números los irá incrementando automáticamente.

Visto lo anterior continuamos.

Creamos un try/except ya que la primera vez que se ejecute la instrucción se creará la tabla, pero la segunda nos mostrará un error porque ya está creada. Si no queremos usar el gestor de excepciones también podemos usar esta sentencia:


c.execute('''CREATE TABLE IF NOT EXISTS libros (
       id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
       título TEXT,
       año INTEGER,
       precio FLOAT,
       descripción TEXT);''') 

Nota: Aunque en sqlite3 en Python no es necesario, ya que solo se puede ejecutar una instrucción por cada "execute" normalmente todas las instrucciones SQL terminan con un ; porque en SQL si que se pueden ejecutar varias instrucciones simultáneamente en una instrucción.


Trabajando con Registros (CRUD)


- Crear o Insertar datos: Usamos el comando INSERT para agregar datos a la base de datos.

La forma general sería:

INSERT INTO tabla VALUES (valor_campo1, valor_campo2,...,valor_campo_n);

Lo tenemos que hacer en el mismo orden que estén creados los campos. Además tenemos que introducir tantos valores como campos tengamos. Si no queremos especificar un determinado valor deberemos poner Null en el valor de ese campo.

Si quisiéramos introducir los datos en otro orden distinto al que creamos los campos podemos hacerlos de la siguiente forma:

INSERT INTO tabla(campo_n,...,campo2, campo1) VALUES (valor_campo_n,...,valor_Campo2, valor_campo1);

En nuestro ejemplo el código de Python para agregar un valor a la base de datos sería:

c.execute("INSERT INTO libros VALUES (NULL,'El Final', 1998, 15.50, 'libro de ficción');")

Introducimos los campos en el mismo orden en los que lo creamos en la tabla. He puesto NULL en el id porque al crear la tabla, yo no quiero crear el id y será el programa el que lo introduzca por mi automáticamente.

Otra forma, sobre todo si se quiere pasar listas con datos a través de un bucle 'For' o una sentencia 'executemany', sería:

c.execute("INSERT INTO libros (titulo, año, precio, descripción) VALUES (?,?,?,?)", ('El final', 1998, 15.50, 'libro de ficción'))

Si utilizamos 'executemany' entonces para representar los valores nulos se utiliza None en lugar de Null

# Insertar varios registros.
    
set_data = [
        (None, 'El ocho', 1987, 23.50, 'libro de ficción'),
        (None, 'Historia del Arte', 2023, 60, 'libro didáctico'),
        (None,'Programar en Python', 2010, 25, 'libro de Informática'),
        ]

c.executemany("INSERT INTO libros VALUES (?,?,?,?,?);", set_data)


- Recuperar datos: Podemos ejecutar consultas SELECT para recuperar información de la base de datos.

# Recuperar todos los datos
c.execute("SELECT * FROM libros;")
for fila in c:
    print(fila)

La salida sería:

(1, 'El Final', 1998, 15.5, 'libro de ficción')

(2, 'El ocho', 1987, 23.5, 'libro de ficcion')

(3, 'Historia del Arte', 2023, 60.0, 'libro didáctico')

(4, 'Programar en Python', 2010, 25.0, 'libro de Informática')

Podemos tambien utilizar los métodos del cursor: fetchall(), fetchmany(número) y fetchone() para recuperar todos, algunos o uno solo de los registros. Por ejemplo para recuperar todos los registros sería:

# Recuperar todos los datos
c.execute("SELECT * FROM libros;")
datos = c.fetchall()
print(datos)

SALIDA:
[(1, 'El Final', 1998, 15.5, 'libro de ficción'), 
(2, 'El ocho', 1987, 23.5, 'libro de ficción'), 
(3, 'Historia del Arte', 2023, 60.0, 'libro didáctico'), 
(4, 'Programar en Python', 2010, 25.0, 'libro de Informática')]



- Actualizar un Registro. Para actualizar un registro ya existente en la base de datos usamos el comando UPDATE.

# Actualizar un registro
UPDATE tabla SET campo=valor, campo=valor WHERE campo_id=n

En el ejemplo de la librería.

# Actualizar un registro
c.execute("UPDATE libros SET título='otro libro', precio=18 WHERE id=2";)


- Borrar un Registro.  La instrucción DELETE se usa para borrar un registro de una tabla.

# Borrar un registro
DELETE FROM nombre_tabla WHERE condición; 

NOTA IMPORTANTE: ¡Mucho cuidado cuando borres registros en una tabla!. Ten en cuenta la clausula WHERE en la instrucción DELETE. La clausula WHERE especifica que registro debe ser borrado. SI la omites, todos los registros de la tabla se borrarán.

Vamos ahora a borrar el último registro:

# borrar un registro
c.execute("DELETE FROM libros WHERE id=4") 

Si lo que queremos es borrar la tabla y no solo los registros, por ejemplo para empezar de nuevo usaremos la instrucción:

# borrar una tabla
c.execute("DROP TABLE nombre_tabla") 


Después de ver las operaciones básicas CRUD, vamos a ver dos cosas más.

Modificar una tabla para añadir un nuevo campo.

Usamos la instrucción:

# Añadir un nuevo campo a la tabla
ALTER TABLE nombre_tabla ADD nuevo_campo tipo_de_dato;

Por ejemplo si queremos añadir un nuevo campo para poner nuestra opinión sobre los libros (un campo booleano con 0 si no nos ha gustado y 1 si nos ha gustado) haríamos los siguiente:

# Añadir un nuevo campo a la tabla
c.execute("ALTER TABLE libros ADD opinion BOOLEAN")

Todos los registros que ya estuvieran creados de antes aparecerán con el valor NULL para esta columna.


Crear una nueva tabla en la base de datos. (suponemos que ya hay una creada)


Por ejemplo vamos a poner una tabla llamada autor, que tenga los campos nombre, apellidos y editorial.

# Añadir una nueva tabla
    c.execute('''
        CREATE TABLE IF NOT EXISTS autor(
        id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
        nombre TEXT,
        apellidos TEXT,
        editorial TEXT)
''')

4. Confirmar y cerrar: Si no estamos en un entorno contextual (estamos utilizando el with) después de realizar las operaciones necesarias, confirmamos los cambios y cerramos la conexión.

conexion.commit()
c.close()
conexion.close()

¡IMPORTANTE! Utilizar el contexto with en combinación con sqlite3.connect es una excelente práctica para manejar la conexión a la base de datos de manera más segura y eficiente. El bloque with se encargará de cerrar automáticamente la conexión y el puntero una vez que el bloque se haya ejecutado, independientemente de si hay excepciones o no.

Con esta estructura, la conexión se cerrará automáticamente al finalizar el bloque 'with', y no necesitas llamar a conexion.commit() explícitamente, ya que SQLite realizará las confirmaciones automáticamente cuando se cierre la conexión. Esta es una forma segura y práctica de trabajar con bases de datos en Python.

Aquí tienes cómo se vería el código utilizando el contexto with:

import sqlite3

# Creamos la conexión

with sqlite3.connect('mi_base.db') as conexion:

    # Creamos el cursor

    c = conexion.cursor()

    c.execute('''CREATE TABLE IF NOT EXISTS libros (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
            título TEXT,
            año INTEGER,
            precio FLOAT,
            descripción TEXT);''')
        
    # Insertar un registro
    
    c.execute("INSERT INTO libros VALUES (NULL,'El Final', 1998, 15.50, 'libro de ficción');")
    
    # Insertar varios registros.
    
    set_data = [
        (None, 'El ocho', 1987, 23.50, 'libro de ficcion'),
        (None, 'Historia del Arte', 2023, 60, 'libro didáctico'),
        (None,'Programar en Python', 2010, 25, 'libro de Informática'),
        ]
    
    c.executemany("INSERT INTO libros VALUES (?,?,?,?,?);", set_data)

    # Recuperar todos los datos
    
    c.execute("SELECT * FROM libros;")
    datos = c.fetchall()
    print(datos)
    
    # Actualizar un registro.
    c.execute("UPDATE libros SET título='El ocho', precio=18 WHERE id=49")
    
    # Borrar un registro
    c.execute("DELETE FROM libros WHERE id=4")    
    
    # Añadir un nuevo campo a la tabla
    c.execute("ALTER TABLE libros ADD opinion BOOLEAN")
    
    # Añadir una nueva tabla
    c.execute('''
        CREATE TABLE IF NOT EXISTS autor(
        id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
        nombre TEXT,
        apellidos TEXT,
        editoria TEXT)
''')


Ahora que hemos visto lo básico vamos a profundizar más en las instrucciones SQL en si. Para trabajar más cómodamente vamos a necesitar un programa para ver las bases de datos creadas.

1.- En local podemos usar DB Browser for sqlite --> https://sqlitebrowser.org/

2.- Si preferimos trabajar online podemos ir a https://sqliteonline.com/

3.- Para diseñar las bases de datos de forma gráfica podemos utlizar https://drawsql.app/

Los elementos fundamentales de una base de datos son:

Tabla: Una tabla es una estructura de datos fundamental en una base de datos relacional como SQLite. Representa una colección organizada de datos relacionados. Puedes pensar en una tabla como una hoja de cálculo en la que los datos se almacenan en filas y columnas. Cada tabla tiene un nombre único y define la estructura de los datos que contendrá, incluyendo el nombre y el tipo de cada columna.

Columna: Una columna es una parte de una tabla que define el tipo de datos que se almacenan en ella. Cada columna tiene un nombre único y un tipo de datos asociado que especifica el tipo de valores que puede contener. Por ejemplo, una columna podría almacenar números enteros, cadenas de texto, fechas, etc. Las columnas permiten organizar y categorizar los datos en la tabla.

Fila: Una fila, a veces llamada registro o tupla, es una entrada individual en una tabla. Representa un conjunto completo de datos que pertenecen a un único elemento o entidad en el mundo real. Cada fila contiene valores para cada una de las columnas definidas en la tabla. En otras palabras, una fila es una instancia específica de los datos que se almacenan en la tabla.


SQLite admite varios tipos de datos que puedes utilizar en una base de datos en Python. Aquí tienes una lista de los tipos de datos más comunes que puedes emplear en SQLite.


TIPOS DE DATOS.


INTEGER vs. INT: Almacena números enteros. Puedes especificar la longitud, por ejemplo, INTEGER(4) o INT(4) para un entero de 4 bytes. 

REAL vs FLOAT: Almacena números de punto flotante, como números decimales. (FLOAT)

TEXT vs VARCHAR:  En SQLite, puedes utilizar TEXT para almacenar cadenas de texto. La diferencia principal entre TEXT y VARCHAR es que VARCHAR permite especificar una longitud máxima para la cadena, mientras que TEXT no impone una restricción de longitud. Sin embargo, en SQLite, incluso si declaras una columna como VARCHAR, SQLite tratará la columna de manera similar a TEXT, ya que no impone restricciones de longitud máxima de manera estricta. La mayoría de las bases de datos SQLite tratan VARCHAR y TEXT de manera intercambiable. 

BLOB: Almacena datos binarios, como imágenes o archivos.

NUMERIC: Puede almacenar cualquier valor numérico, incluidos enteros y decimales. SQLite intentará convertir automáticamente los valores en el formato adecuado.

BOOLEAN: Aunque SQLite no tiene un tipo BOOLEAN específico, generalmente se usa INTEGER o NUMERIC con valores 0 y 1 para representar valores booleanos (falso y verdadero).

DATE: Puedes almacenar fechas en formato 'YYYY-MM-DD'.

TIME: Para almacenar horas en formato 'HH:MM:SS'.

DATETIME: Combina fecha y hora en un solo campo, en formato 'YYYY-MM-DD HH:MM:SS'.

TIMESTAMP: Se utiliza para almacenar la marca de tiempo (timestamp) de cuándo se insertó o actualizó un registro en una tabla.

Como ejemplo vamos a utilizar esta base de datos que recrea algunos datos de los mundiales de futbol en la que se han definido las siguientes tablas:

- teams: representa las diferentes selecciones que han participado.
- countries: los distintos paises que existen y que están representados por su selección.
- events: los diferentes mundiales que se han disputado.
- events_teams: los mundiales en los que ha participado cada equipo.
- games: Partidos, que equipos jugaron y el resultado.


Aunque ahora no lo entiendas muy bien el esquema de esta base de datos es el siguiente:

diagrama de la base de datos



Seria conveniente que vieras el archivo sql1.py que es que he utilizado para crear la base de datos. En el se ve como conectar a la base de datos, la creación de las tablas y la inserción de datos de diferentes formas.  En el archivo sql2.py encontrarás diversas búsquedas hechas sobre esa base de datos.

A la hora de crear las tablas se pueden especificar ciertas restricciones a los campos y algunas reglas. 

Siempre cada registro de una tabla tiene que tener un identificador único o id al que se denomina, como ya hemos comentado antes, PRIMARY KEY

Por ejemplo en la tabla countries, que recoge algunos países que han participado en un mundial, la instrucción SQL para SQLITE3 de Python que la crea es la siguiente:

sql1.py: creación de la base de datos. primary_key

 c.execute('''
            CREATE TABLE IF NOT EXISTS countries (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT Null,
            name VARCHAR(255) NOT Null            
            )''')

Como vemos hemos creado dos campos, el campo "id" y el campo "name". Le hemos especificado a la base de datos que el campo id será un número entero que será el "primary_key" de cada registro. Si nosotros no lo especificamos se creará automáticamente, no podrá ser un valor nulo y al ser un primary_key será único y no se podrá repetir. 

También puede ocurrir que un campo de una tabla, haga referencia a otro campo de otra tabla, es lo que se conoce como foreign_key. Por ejemplo en la tabla "games" los campos team1_id y team2_id hacen referencia a registros que están en la tabla "teams" y events_id hace referencia al id del mundial en el que se jugo ese partido y en el que participaron esos equipos y esta en la tabla "events".

sql1.py: creación de la base de datos. foreign_key

c.execute('''
            CREATE TABLE IF NOT EXISTS games (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT Null,
            event_id INTEGER NOT Null,
            round VARCHAR(255) NOT NULL,
            team1_id INTEGER NOT Null,
            team2_id INTEGER NOT Null,
            play_at DATETIME NOT Null,
            score1 INTEGER,
            score2 INTEGER,
            winner INTEGER,
            FOREIGN KEY(event_id) REFERENCES events(id),
            FOREIGN KEY(team1_id) REFERENCES teams(id),
            FOREIGN KEY(team2_id) REFERENCES teams(id))''')

Las claves externas o FOREIGN KEY se añaden al final de la instrucción una vez definidos los campos.

    ELEMENTOS DEL LENGUAJE SQL.


- Mostrar un elemento.


La estructura de la instrucción SQL para mostrar un elemento es:

   SELECT campo FROM tabla;
Nos mostrará el campo seleccionado de la tabla que le hayamos indicado. 
Si sustituimos campo por * un asterisco nos mostrará todos los campos de esa tabla.

En la base de datos de ejemplo si queremos mostrar el "title", el nombre de los países, de la tabla "teams" equipos:

consulta a la base de datos


Si queremos mostrar todos los campos de esa tabla utilizamos el wildcard *.

mostrar todos los campos de la tabla


Podemos usar también una serie de condicionantes para el registro.

Por ejemplo para que nos muestra en esta tabla "teams" que registro o registros tienen un valor del campo "title" igual a Spain.

   SELECT * FROM teams WHERE title='Spain';

clausula WHERE

Como el id de Spain en la tabla "teams" es 129 podemos contar las veces que en esta base de datos España ha jugado un partido en el mundial. 

 SELECT count(*) FROM games WHERE team1_id=129 or team2_id=129;
clausula COUNT(*)

En este caso han sido en 11 ocasiones, es decir hay 11 registro en el que se cumple la condición que hemos especificado. 

COUNT(*) -> devuelve el número de registros que cumplen la condición.

Podemos ordenar la forma en la que se muestran los registros con la clausula ORDER BY. Por ejemplo en la tabla "teams" se nos muestren los registros ordenados por "title".

SELECT id, title FROM teams ORDER BY title;
clausula ORDER BY


Lo mismo pero en orden descendente:

SELECT id, title FROM teams ORDER BY title DESC;
clausula ORDER BY DESC


También en vez del nombre del campo o columna se puede poner un número que será el que le corresponda según la posición que ocupe en la tabla. Lo mismo que antes se puede conseguir con esta instrucción:

SELECT id, title FROM teams ORDER BY 3 DESC;
donde 3 es el orden del campo "title" en la tabla "teams".

Operadores que se pueden utilizar con la clausula WHERE.


=igual
>mayor que
>= mayor o igual que
!>no más grande que
<menos que
<=menor o igual que
!<no menor que
<>no igual que
!= no igual que
BETWEENentre los valores especificados incluidos los mismos
IS NULLsi es un missing value

Un ejemplo podría ser:

SELECT campo, campo, campo FROM tabla WHERE campo<>1;


Filtros Avanzados AND, OR o IN.


Se pueden anidar sentencias usando el operador AND como en los siguientes ejemplos. (AND = y )

SELECT campo, campo, campo FROM tabla WHERE campo>2 AND campo>3;
Que se cumpla una condición u otra con OR:

SELECT campo, campo, campo FROM tabla WHERE campo>2 OR campo>3;
Se pueden utilizar paréntesis para agrupar opciones:

SELECT campo, campo, campo FROM tabla WHERE (campo=1 OR campo=2) AND campo>=2;
La clausula "IN" en SQL se utiliza para filtrar filas en una consulta en función de una lista de valores especificados. Básicamente, nos permite seleccionar filas cuyos valores en una columna coincidan con uno o más valores en una lista. Es útil cuando queremos recuperar registros que cumplan con múltiples condiciones en una sola consulta .

SELECT columnas FROM tabla WHERE columna IN (valor1, valor2, valor3...);

WILD CARDS.


Las "WILD CARDS" o comodines en SQL son caracteres especiales que se utilizan en las consultas para buscar patrones de texto en una columna de una tabla. Los comodines más comunes en SQL son el asterisco '*', el signo de '%' y el guion bajo '_'.

1.- Asterisco '*' El asterisco se utiliza para seleccionar todas las columnas de una tabla en una consulta. Esto ya lo hemos visto anteriormente en múltiples casos:

 SELECT * FROM teams WHERE title='Spain';

2.- Porcentaje '%': El signo de porcentaje se usa como comodín en combinación con la clausula 'LIKE' para buscar patrones de texto en una columna. Por ejemplo:


SELECT name FROM countries WHERE name LIKE 'F%';

uso wild card %


Nos devuelve el nombre de todos los países de la tabla 'countries' que empiecen por la letra 'F'

O por ejemplo:

'%texto': encuentra los registros que termine con texto.
'texto%': encuentra los registros que comiencen con texto.
'%texto%': encuentra los registros que contengan la palabra 'texto' en cualquier parte.

3.- Guiones Bajos (_): El guion bajo _ se utiliza como comodín junto con la clausula LIKE para buscar patrones de texto, pero en lugar de representar múltiples caracteres, representa un único carácter. Por ejemplo:

SELECT name FROM countries WHERE name LIKE '_o%';
wild card guión bajo

Esta consulta selecciona todos los nombres de países donde el segundo carácter es una 'o' y dando igual el texto que haya después.

Foreign Keys.


FOREIGN KEY(nombre_campo) REFERENCES nombre_tabla(nombre_campo)
Se definen al final de la instrucción SQL que crea esa tabla.

Cada tabla debería tener una Primary Key que es única para cada registro de la tabla. Las Foreigns Keys son como las bases de datos, representan las relaciones entre las tablas. Mientras que una tabla solo tiene una Primary Key, puede tener múltiples Foreign Keys, una por cada tabla con la que tenga relación.

Tipos de relaciones.

One to Many. Un registro en una tabla puede relacionarse con uno o más registros en otra tabla. Esto se puede implementar utilizando una clave foránea en la tabla "many" que hace referencia a la clave principal en la tabla "one". El ejemplo tipico es que un autor puede haber escrito varios libros pero un libro solo puede tener un autor.

import sqlite3

# Conectarse a la base de datos SQLite
conn = sqlite3.connect('biblioteca.db')
cursor = conn.cursor()

# Crear la tabla de Autores
cursor.execute('''CREATE TABLE autores (
                    id INTEGER PRIMARY KEY,
                    nombre TEXT)''')

# Crear la tabla de Libros
cursor.execute('''CREATE TABLE libros (
                    id INTEGER PRIMARY KEY,
                    titulo TEXT,
                    autor_id INTEGER,
                    FOREIGN KEY (autor_id) REFERENCES autores(id))''')

# Insertar datos
cursor.execute("INSERT INTO autores (nombre) VALUES ('J.K. Rowling')")
cursor.execute("INSERT INTO libros (titulo, autor_id) VALUES ('Harry Potter y la piedra filosofal', 1)")
cursor.execute("INSERT INTO libros (titulo, autor_id) VALUES ('Harry Potter y la cámara secreta', 1)")

# Consulta para obtener todos los libros de un autor específico (por ejemplo, J.K. Rowling)
cursor.execute("SELECT libros.titulo FROM libros JOIN autores ON libros.autor_id = autores.id WHERE autores.nombre = 'J.K. Rowling'")
libros_jk_rowling = cursor.fetchall()
print("Libros de J.K. Rowling:")
for libro in libros_jk_rowling:
    print(libro[0])

# Cerrar la conexión
conn.commit()
conn.close()



One to one. Un registro en una tabla solo puede estar relacionado con un registro en otra. Esto se consigue generalmente mediante la inclusión de una clave foránea en una de las tablas. En el ejemplo del futbol un país solo puede tener un equipo que lo represente un equipo nacional puede pertenecer a un solo paí.



# tabla countries
c.execute('''
            CREATE TABLE IF NOT EXISTS countries (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT Null,
            name VARCHAR(255) NOT Null            
            )''')

# tabla teams
c.execute('''
            CREATE TABLE IF NOT EXISTS teams (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT Null,
            key VARCHAR(255) NOT Null,
            title VARCHAR(255) NOT Null,
            country_id INTEGER NOT Null,
            FOREIGN KEY(country_id) REFERENCES countries (id))''')
Otro ejemplo aquí.

Many to many. Uno o más registros pueden relacionarse con uno o mas registros en otra tabla.
Esto se logra creando una tabla intermedia que conecta las dos tablas. En nuestro ejemplo del futbol un equipo (teams) puede participar en varios mundiales (events) y en un mundial pueden participar varios equipos.

many to many relation

# tabla events
c.execute('''
            CREATE TABLE IF NOT EXISTS events (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT Null,
            key VARCHAR(255) NOT Null,
            start_at DATE NOT Null,
            end_at DATE)''')

# tabla temas
c.execute('''
            CREATE TABLE IF NOT EXISTS teams (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT Null,
            key VARCHAR(255) NOT Null,
            title VARCHAR(255) NOT Null,
            country_id INTEGER NOT Null,
            FOREIGN KEY(country_id) REFERENCES countries (id))''')

# tabla many to many events_teams
c.execute('''
            CREATE TABLE IF NOT EXISTS events_teams (
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT Null,
            event_id INTEGER NOT Null,
            team_id INTEGER NOT Null,
            FOREIGN KEY(event_id) REFERENCES events (id),
            FOREIGN KEY(team_id) REFERENCES teams (id))''')
Otro ejemplo aquí.

Seleccionando datos en múltiples tablas.


Una vez que hemos visto como se relacionan las tablas en una base de datos, vamos a ver como combinar datos de dos o más tablas en función de una columna común.

Supongamos que tienes dos tablas en tu base de datos: "clientes" y "pedidos", y ambas tienen una columna llamada "cliente_id" que se utiliza para relacionar los registros. Vamos a crear las tablas y poner algunos datos.

import sqlite3

# Conectarse a la base de datos o crearla si no existe
conn = sqlite3.connect("mi_base_de_datos.db")
cursor = conn.cursor()

# Crear la tabla "clientes"
cursor.execute("""
    CREATE TABLE IF NOT EXISTS clientes (
        cliente_id INTEGER PRIMARY KEY,
        nombre TEXT,
        email TEXT
    );
""")

# Crear la tabla "pedidos"
cursor.execute("""
    CREATE TABLE IF NOT EXISTS pedidos (
        pedido_id INTEGER PRIMARY KEY,
        cliente_id INTEGER,
        producto TEXT,
        cantidad INTEGER,
        FOREIGN KEY (cliente_id) REFERENCES clientes (cliente_id)
    );
""")

# Confirmar los cambios y cerrar la conexión
conn.commit()
conn.close()
Ahora vamos a ver como mostrar los nombres de los clientes y los productos de los pedidos que tienen coincidencias en ambas tablas.  Para ello utilizaremos la sentencia INNER JOIN que devuelve solo los registros que tienen coincidencias en ambas tablas.

Ejemplo de INNER JOIN.

import sqlite3

# Conectarse a la base de datos
conn = sqlite3.connect("mi_base_de_datos.db")
cursor = conn.cursor()

# Consulta SQL utilizando INNER JOIN
query = """
SELECT clientes.nombre, pedidos.producto
FROM clientes
INNER JOIN pedidos ON clientes.cliente_id = pedidos.cliente_id;
"""

# Ejecutar la consulta y obtener resultados
cursor.execute(query)
resultados = cursor.fetchall()

# Imprimir resultados
for resultado in resultados:
    print(resultado)

# Cerrar la conexión
conn.close()
El resultado sería algo como esto:

antonio	| alicates
antonio	| tenazas
ana	| llave inglesa
Vamos a ver como funciona:

  • 'SELECT clientes.nombre, pedidos.producto': Esta parte de la consulta especifica las columnas que deseas seleccionar en el resultado. En este caso, estás seleccionando las columnas "nombre" de la tabla "clientes" y "producto" de la tabla "pedidos".

  • 'FROM clientes': Aquí se indica la tabla principal de la que deseas seleccionar datos. En este caso, estás seleccionando datos de la tabla "clientes".

  • 'INNER JOIN pedidos ON clientes.cliente_id = pedidos.cliente_id': Esta es la parte clave de la consulta que realiza la combinación entre las dos tablas. Aquí se está utilizando INNER JOIN para combinar registros de las tablas "clientes" y "pedidos" en función de una condición. La condición de unión es clientes.cliente_id = pedidos.cliente_id, lo que significa que los registros se combinarán cuando el valor de la columna "cliente_id" en la tabla "clientes" sea igual al valor de la columna "cliente_id" en la tabla "pedidos".

Cuando se ejecuta esta consulta, obtendrás un conjunto de resultados que incluirá el nombre de los clientes y los productos de los pedidos que coinciden en la columna "cliente_id". En otras palabras, esta consulta te dará una lista de clientes y los productos que han pedido.

Esencialmente, INNER JOIN se utiliza para seleccionar datos de dos o más tablas relacionadas únicamente cuando hay coincidencias en las columnas especificadas en la condición de unión. Si no hay coincidencias, esos registros no aparecerán en el resultado.


Ejemplo de LEFT JOIN.

Devuelve todos los registros de la tabla1 (clientes) y los registros coincidentes de la tabla2 (pedidos), si los hay. Si no los hay los datos de la tabla2(pedidos) serán NULL. He puesto un cliente nuevo que aun no tiene pedidos y he ejecutado la instrucción.

import sqlite3

# Conectarse a la base de datos
conn = sqlite3.connect("mi_base_de_datos.db")
cursor = conn.cursor()

# Consulta SQL utilizando LEFT JOIN
query = """
SELECT clientes.nombre, pedidos.producto
FROM clientes
LEFT JOIN pedidos ON clientes.cliente_id = pedidos.cliente_id;
"""

# Ejecutar la consulta y obtener resultados
cursor.execute(query)
resultados = cursor.fetchall()

# Imprimir resultados
for resultado in resultados:
    print(resultado)

# Cerrar la conexión
conn.close()
En este caso la salida es:

ana	| llave inglesa
antonio	| alicates
antonio	| tenazas
teresa	| NULL
Existen más instrucciones pero no vamos a entrar de momento en ellas.

OPERACIONES CON BASES DE DATOS.


 - CONTAR el numero de filas o registros.

SELECT count(*) FROM Nombre_Tabla;
Para que la salida no aparezca count(*) sino algo más legible podemos poner un alias usando la clave AS.

SELECT count(*) AS Numero_Registos FROM Nombre_Tabla;
También podemos utilizar la clausula WHERE para filtrar los resultados.

SELECT count(*) AS Numero_Registos FROM Nombre_Tabla WHERE condición;


- SUMAR una columna.

SELECT SUM(columna) AS Valor_Total FROM Nombre_Tabla WHERE condición;

- MEDIA de una Columna. 

SELECT AVG(columna) AS Valor_Total FROM Nombre_Tabla WHERE condición;

- AGRUPAR datos.

Agrupa registros o filas basándose en una determinada columna.

SELECT columna_1, count(*) FROM Nombre_Tabla GROUP BY columna_1;

CHULETA de declaraciones SQL básicas.


Declaración Descripción Código de Ejemplo
SELECT  Recupera datos de una o más tablas basadas en columnas especificadas.  SELECT column1, column2 FROM table_name;
FROM  Especifica la tabla de la que se obtienen los datos recuperados.  SELECT column1, column2 FROM table_name;
WHERE  Filtra los datos según las condiciones especificadas.  SELECT column1, column2 FROM table_name WHERE condition;
ORDER BY  Ordena el conjunto de resultados según la columna especificada en orden ascendente (ASC) o descendente (DESC).  SELECT column1, column2 FROM table_name ORDER BY column1 ASC;
GROUP BY  Agrupa las filas o registros en base a la columna especificada.  SELECT column1, COUNT(*) FROM table_name GROUP BY column1;
HAVING  Filtra datos agrupados según lo especificado en las condiciones.  SELECT column1, COUNT(*) FROM table_name GROUP BY column1 HAVING COUNT(*) > 1;
INSERT INTO  Inserta datos en una tabla.  INSERT INTO table_name (column1, column2) VALUES (value1, value2);
UPDATE  Modifica o actualiza los datos en una tabla en base a lo especificado en las condiciones.  UPDATE table_name SET column1=value1 WHERE condition;
DELETE FROM  Elimina datos de una tabla según lo especificado en las condiciones.  DELETE FROM table_name WHERE condition;
JOIN  Combina filas o registros de varias tablas según tengan columnas relacionadas.  SELECT column1, column2 FROM table1 JOIN table2 ON table1.column = table2.column
INNER JOIN  Devuelve solo las filas coincidentes de ambas tablas.   SELECT column1, column2 FROM table1 INNER JOIN table2 ON table1.column = table2.column;
LEFT JOIN  Devuelve todas las filas de la tabla izquierda o tabla 1 y las coincidentes en la tabla derecha o tabla 2.   SELECT column1, column2 FROM table1 LEFT JOIN table2 ON table1.column = table2.column;
RIGHT JOIN  Devuelve todas las filas de la tabla derecha o tabla 2 y las coincidentes de la tabla derecha o tabla 1.  SELECT column1, column2 FROM table1 RIGHT JOIN table2 ON table1.column = table2.column;
FULL JOIN  Devuelve todas las filas o registros de ambas tablas.  SELECT column1, column2 FROM tabke1 FULL JOIN table2 ON table1.column = table2.column;
DISTINCT  Devuelve los valores únicos de una columna  SELECT DISTINCT column1 FROM table_name;
COUNT  Cuenta el número de filas o valores no nulos en una columna.  SELECT COUNT(*) FROM table_name;
or
 SELECT COUNT(column1) FROM table_name;
SUM  Calcula la suma de valores en una columna.  SELECT SUM(column1) FROM table_name; 
AVG  Calcula el valor promedio de una columna.  SELECT AVG(column1) FROM table_name;
MAX  Encuentra el valor máximo en una columna.  SELECT MAX(column1) FROM table_name;
MIN  Encuentra le valor mínimo en una columna.  SELECT MIN(column1) FROM table_name;


....

jueves, 29 de junio de 2023

19.- Creación de la aplicación Cesta de la Compra (variables permanentes o context_processor)

Ponte cómodo y prepárate para sumergirte en la creación del carro de la compra de nuestra tienda en línea. En este capítulo, daremos vida a un widget que se ubicará en la parte superior derecha de nuestra página. Este widget permitirá a los usuarios acceder al carro de la compra de forma rápida y sencilla. Nuestro objetivo es brindar una experiencia de compra fluida, por lo que la aplicación 'carro' que crearemos conservará los artículos seleccionados, tanto mientras los usuarios navegan como cuando regresan más tarde y abren nuevamente el navegador. Podrán añadir, eliminar y gestionar productos de manera intuitiva y conveniente.

Durante este proceso de desarrollo, exploraremos nuevas funcionalidades de Django. Aprenderemos a almacenar y mantener los artículos del carro de la compra, asegurando que los usuarios siempre tengan acceso a su selección.  ¡Comencemos!


Construyendo un Carro de la Compra.


Después de construir el catálogo de productos, el siguiente paso es crear un carrito de compras para que los usuarios puedan seleccionar los productos que desean comprar. Un carrito de compras permite a los usuarios elegir productos y establecer la cantidad que desean ordenar, almacenando esta información temporalmente mientras navegan por el sitio, hasta que finalmente realicen un pedido. El carrito debe persistir en la sesión para que los elementos del carrito se mantengan durante la visita de un usuario.

Utilizarás el framework de sesiones de Django para persistir el carrito. El carrito se mantendrá en la sesión hasta que se complete o el usuario realice el pago. También necesitarás crear modelos adicionales de Django para el carrito y sus elementos.


Usando sesiones en Django


Django proporciona un framework de sesiones que soporta sesiones anónimas y de usuario. El framework de sesiones te permite almacenar datos arbitrarios para cada visitante. Los datos de sesión se almacenan en el servidor y las cookies contienen el ID de sesión a menos que uses el motor de sesión basado en cookies. El middleware de sesión gestiona el envío y la recepción de cookies. El motor de sesión predeterminado almacena los datos de sesión en la base de datos, pero puedes elegir otros motores de sesión.

Para usar sesiones, debes asegurarte de que la configuración MIDDLEWARE de tu proyecto contenga 'django.contrib.sessions.middleware.SessionMiddleware'. Este middleware gestiona las sesiones. Se agrega por defecto a la configuración MIDDLEWARE cuando creas un nuevo proyecto usando el comando startproject.

El middleware de sesión pone la sesión actual a disposición en el objeto de solicitud (request). Puedes acceder a la sesión actual usando request.session, tratándola como un diccionario de Python para almacenar y recuperar datos de sesión. El diccionario de sesión acepta cualquier objeto de Python por defecto que pueda serializarse a JSON. Puedes establecer una variable en la sesión de esta manera:

request.session['foo'] = 'bar'
Para recuperar una clave de sesión lo puedes hacer de la siguiente manera:

request.session.get('foo')
Eliminar una clave que previamente habías guardado en la sesión de usuario se hace así:

del request.session['foo']
Es decir, se trata igual que cualquier diccionario de Python.

Cuando los usuarios inician sesión en el sitio, su sesión anónima se pierde y se crea una nueva sesión para los usuarios autenticados. Si has almacenado elementos en una sesión anónima que necesitas conservar después de que el usuario inicie sesión, deberás copiar los datos de la sesión antigua a la nueva sesión. Puedes hacer esto recuperando los datos de sesión antes de iniciar sesión del usuario utilizando la función login() del sistema de autenticación de Django y almacenándolos en la sesión después de eso.

Configuración de sesiones


Existen varias configuraciones que puedes utilizar para configurar las sesiones de tu proyecto. La más importante es SESSION_ENGINE. Esta configuración te permite establecer el lugar donde se almacenan las sesiones. Por defecto, Django almacena las sesiones en la base de datos utilizando el modelo Session de la aplicación django.contrib.sessions.

Django ofrece las siguientes opciones para almacenar datos de sesión:

- Sesiones en la base de datos: Los datos de sesión se almacenan en la base de datos. Este es el motor de sesión predeterminado.

- Sesiones basadas en archivos: Los datos de sesión se almacenan en el sistema de archivos.

- Sesiones en caché: Los datos de sesión se almacenan en un almacén de caché. Puedes especificar los almacenes de caché utilizando la configuración CACHES. Almacenar datos de sesión en un sistema de caché proporciona el mejor rendimiento.

- Sesiones en caché con base de datos: Los datos de sesión se almacenan en una caché de escritura y en la base de datos. Las lecturas solo utilizan la base de datos si los datos no están ya en la caché.

- Sesiones basadas en cookies: Los datos de sesión se almacenan en las cookies que se envían al navegador.

Para mejorar el rendimiento, se recomienda utilizar un motor de sesión basado en caché. Django admite Memcached de manera nativa, y puedes encontrar almacenes de caché de terceros para Redis y otros sistemas de caché. Utilizar un sistema de caché como Memcached o Redis puede mejorar significativamente el rendimiento de las sesiones en tu aplicación.

Puedes personalizar las sesiones con configuraciones específicas. Aquí hay algunas de las configuraciones importantes relacionadas con las sesiones:

- `SESSION_COOKIE_AGE`: La duración de las cookies de sesión en segundos. El valor predeterminado es 1209600 (dos semanas).

- `SESSION_COOKIE_DOMAIN`: El dominio utilizado para las cookies de sesión. Establece esto en mydomain.com para habilitar cookies entre dominios o usa None para una cookie de dominio estándar.

- `SESSION_COOKIE_HTTPONLY`: Indica si se debe utilizar la bandera HttpOnly en la cookie de sesión. Si se establece en True, JavaScript del lado del cliente no podrá acceder a la cookie de sesión. El valor predeterminado es True para aumentar la seguridad contra el secuestro de sesiones de usuario.

- `SESSION_COOKIE_SECURE`: Un booleano que indica que la cookie solo debe enviarse si la conexión es HTTPS. El valor predeterminado es False.

- `SESSION_EXPIRE_AT_BROWSER_CLOSE`: Un booleano que indica que la sesión debe expirar cuando se cierra el navegador. El valor predeterminado es False.

- `SESSION_SAVE_EVERY_REQUEST`: Un booleano que, si es True, guardará la sesión en la base de datos en cada solicitud. La expiración de la sesión también se actualiza cada vez que se guarda. El valor predeterminado es False.

Puedes ver todas las configuraciones de sesión y sus valores predeterminados en https://docs.djangoproject.com/en/5.0/ref/settings/#sessions.


Finalización de la Sesión.


La configuración `SESSION_EXPIRE_AT_BROWSER_CLOSE` te permite elegir entre sesiones que duran tanto como el navegador está abierto o sesiones persistentes. Por defecto, está establecida en `False`, lo que obliga a que la duración de la sesión sea el valor almacenado en la configuración `SESSION_COOKIE_AGE`. Si configuras `SESSION_EXPIRE_AT_BROWSER_CLOSE` en `True`, la sesión expirará cuando el usuario cierre el navegador, y la configuración `SESSION_COOKIE_AGE` no tendrá ningún efecto.

Además, puedes usar el método `set_expiry()` de `request.session` para sobrescribir la duración de la sesión actual. Esto te permite ajustar dinámicamente la duración de la sesión según sea necesario.


Almacenar el Carro de la compra en una sesión.


Necesitas crear una estructura simple que pueda serializarse a JSON para almacenar elementos del carrito en una sesión. El carrito debe incluir los siguientes datos para cada artículo contenido en él:


- El ID de una instancia de Producto.
- La cantidad seleccionada para el producto.
- El precio unitario del producto.


Como los precios de los productos pueden variar, tomemos el enfoque de almacenar el precio del producto junto con el propio producto cuando se añade al carrito. Al hacerlo de esta manera, se utiliza el precio actual del producto cuando los usuarios lo agregan a su carrito, sin importar si el precio del producto cambia posteriormente. Esto significa que el precio que tiene el artículo cuando el cliente lo añade al carrito se mantiene para ese cliente en la sesión hasta que se complete la compra o finalice la sesión.


A continuación, debes construir la funcionalidad para crear carritos de compra y asociarlos con sesiones. Esto tiene que funcionar de la siguiente manera:


- Cuando se necesite un carrito, se comprueba si se ha establecido una clave de sesión personalizada. Si no hay ningún carrito en la sesión, se crea uno nuevo y se guarda en la clave de sesión del carrito.


- Para solicitudes sucesivas, se realiza la misma comprobación y se obtienen los artículos del carrito a partir de la clave de sesión del carrito. Se recuperan los artículos del carrito de la sesión y sus objetos de Producto relacionados de la base de datos.


Edita el archivo settings.py de tu proyecto y añade la siguiente configuración:


CART_SESSION_ID = 'carro'


Esta es la clave que vas a utilizar para almacenar el carrito en la sesión del usuario. Dado que las sesiones de Django se gestionan por visitante, puedes utilizar la misma clave de sesión de carrito para todas las sesiones.


Creemos una aplicación para gestionar los carritos de compra. Abre la terminal y crea una nueva aplicación ejecutando el siguiente comando desde el directorio del proyecto:


venv$ python manage.py startapp Carro

Para que no se nos olvide, la registramos dentro del proyecto:


PracticaDjango/PracticaDjango/settings.py

...
# Application definition

INSTALLED_APPS = [
    # my applications
    'Proyecto_web_app.apps.ProyectoWebAppConfig',
    'Servicios.apps.ServiciosConfig',
    'Blog.apps.BlogConfig',
    'Contacto.apps.ContactoConfig',
    'Tienda.apps.TiendaConfig',
    'Carro.apps.CarroConfig',
    # third party applications
    'django_bootstrap5',
    # Mapa del Sitio
    'django.contrib.sites', # add sites to installed_apps
    'django.contrib.sitemaps',  # add Django sitemaps to installed app
    # PostgreSQL
...

Y ahora ya si, comencemos a construir el carro. Tenemos que crear una clase nueva que sea la que maneje lo que se supone que debe poder hacerse en un carro. Para ello dentro del directorio de la aplicación "Carro" creamos el archivo carro.py. 


PracticaDjango/PracticaDjango/carro.py

from django.conf import settings
from decimal import Decimal
from Tienda.models import Producto

class Carro:
    def __init__(self, request) -> None:
        """
        Inicializa el carro.
        """
        self.session = request.session
        # construimos un carro de la compra para esta sesión.
        carro = self.session.get(settings.CART_SESSION_ID)
        if not carro:
            # save an empty cart in the session
            carro = self.session[settings.CART_SESSION_ID] = {}
            # guardamos el carro en la sesión y va a ser un diccionario
            # con los productos que el usuario va a comprar
            # la clave es el id del producto y el valor son las características del producto
        self.carro = carro    
Esta es la clase Carro que te permitirá gestionar el carrito de compras. Requiere que el carrito sea inicializado con un objeto de solicitud. Almacenas la sesión actual usando self.session = request.session para hacerla accesible a los otros métodos de la clase Carro.

Primero, intentas obtener el carrito de la sesión actual usando self.session.get(settings.CART_SESSION_ID). Si no hay un carrito presente en la sesión, creas un carrito vacío estableciendo un diccionario vacío en la sesión.

Construirás tu diccionario del carrito con IDs de productos como claves, y para cada clave de producto, un diccionario será un valor que incluya cantidad, precio y otros atributos. Al hacer esto, puedes garantizar que un producto no se añada más de una vez al carrito. De esta manera, también puedes simplificar la recuperación de elementos del carrito.

Aquí vamos a hacer un inciso para comentar el objeto "request". En el contexto de Django, un objeto request representa la solicitud que se realiza a una vista o función de vista. Contiene información sobre la solicitud HTTP realizada por el cliente, como los parámetros enviados, las cookies, la información del usuario, el método HTTP utilizado, entre otros datos.

El objeto request se pasa automáticamente a las vistas de Django cuando se reciben solicitudes HTTP. Al definir una función de vista o una clase de vista en Django, se puede incluir un parámetro llamado request para recibir este objeto. Django se encarga de instanciar y pasar el objeto request automáticamente cuando se llama a la vista.

Se puede decir que el objeto request en Django comparte algunas similitudes con un diccionario de Python. El objeto request contiene información relacionada con la solicitud HTTP, y se puede acceder a los datos de la misma manera que se accede a los elementos de un diccionario.

El objeto request en Django proporciona métodos y atributos que permiten acceder a diferentes aspectos de la solicitud, como los parámetros GET y POST, las cookies, las cabeceras, la información del usuario autenticado y más. Estos datos se pueden acceder utilizando la sintaxis de diccionario de Python.

Por ejemplo, para acceder a los parámetros GET de una solicitud, se puede utilizar:

request.GET['nombre_parametro']

Para acceder a los datos POST, se puede usar:

request.POST['nombre_parametro']

Asimismo, las cookies se pueden acceder mediante:

request.COOKIES['nombre_cookie']

Además, el objeto request también permite asignar valores, al igual que un diccionario. Por ejemplo, request.session['clave'] = valor se utiliza para almacenar un valor en la sesión del usuario.

Si bien el objeto request comparte similitudes con un diccionario, es importante tener en cuenta que no es un diccionario puro de Python. Es un objeto especializado de Django que proporciona funcionalidades específicas para manejar solicitudes HTTP en el contexto de un proyecto Django.

Dicho lo anterior, continuamos.

Agregar nuevos productos al carro.

Creemos un método para añadir productos al carrito o actualizar su cantidad. Añade los siguientes métodos agregar() y guardar_carro() a la clase Carro:

PracticaDjango/PracticaDjango/carro.py

class Carro:
    #...
    def agregar(self, producto, cantidad=1, override_cantidad=False):
        # Si el producto no esta en el carro
        producto_id = str(producto.id)
        if producto_id not in self.carro:
            self.carro[producto_id] = {
                "producto_id": producto.id,
                "nombre": producto.nombre,
                "precio": str(producto.precio),
                "cantidad": 0,
                "imagen": producto.imagen.url,
            }
            # si el producto esta en el carro
            # solo sumamos la cantidad
        if override_cantidad:
            self.carro[producto_id]["cantidad"] = cantidad
        else:
            self.carro[producto_id]["cantidad"] += cantidad

        self.guardar_carro()

    def guardar_carro(self):
        self.session.modified = True

El método 'agregar()' toma los siguientes parámetros como entrada:

- `producto`: La instancia del producto que se va a agregar o actualizar en el carrito.
- `cantidad`: Un entero opcional que representa la cantidad del producto. Por defecto, es 1.
- `override_quantity`: Un booleano que indica si se debe sobrescribir la cantidad con la cantidad proporcionada (True), o si la nueva cantidad debe sumarse a la cantidad existente (False).

Se utiliza el ID del producto como clave en el diccionario de contenido del carrito. Se convierte el ID del producto a una cadena porque Django utiliza JSON para serializar datos de sesión, y JSON solo permite nombres de claves como cadenas.

El ID del producto es la clave, y el valor que se persiste es un diccionario con las cantidades y precios del producto. El precio del producto se convierte de decimal a cadena para serializarlo. Finalmente, se llama al método 'guardar_carro()' para guardar el carrito en la sesión.

El método 'guardar_carro()' marca la sesión como modificada mediante `session.modified = True`. Esto le indica a Django que la sesión ha cambiado y necesita ser guardada.


Eliminar los productos del Carro.


PracticaDjango/PracticaDjango/carro.py

class Carro:    
    #...
    def eliminar(self, producto):
        """
        Elimina el producto del carro.
        """
        producto_id = str(producto.id)
        if producto_id in self.carro:
            del self.carro[producto_id]
            self.guardar_carro()          

El método eliminar() elimina un producto dado del diccionario del carrito y llama al método guardar_carro() para actualizar el carrito en la sesión.

Restar un producto del carrito.


PracticaDjango/PracticaDjango/carro.py

class Carro:    
    #...
    def restar_producto(self, producto):
        """
        Resta el producto del carro.
        """
        producto_id = str(producto.id)
        if producto_id in self.carro:
            self.carro[producto_id]["cantidad"] = (
                self.carro[producto_id]["cantidad"] - 1
            )
            if self.carro[producto_id]["cantidad"] < 1:
                self.eliminar(producto)
            self.guardar_carro()

Este código se encarga de restar la cantidad de un producto en el carrito de compras.

- El método `restar_producto` toma un parámetro `producto`, que se espera que sea un objeto de la clase `Producto`.

- Si el id del producto se encuentra en el diccionario "carro" entonces se busca la clave del producto con producto_id y se busca dentro de ese diccionario la clave cantidad y se le resta una unidad. Se verifica que si la cantidad es menor que uno entonces se elimine el producto del carro.

- Finalmente, se llama al método `guardar_carro` para guardar los cambios en el carrito actualizado.

En resumen, este código disminuye la cantidad de un producto en el carrito de compras. Busca el producto en el diccionario `self.carro`, resta 1 a su cantidad y, si la cantidad resultante es menor que 1, elimina el producto del carrito. Luego, se guarda el carrito actualizado utilizando el método `guardar_carro`.


Limpiar el carro.


No se elimina el carro de la sesión pero si que se eliminan todas sus claves dejando el carro vacío.

PracticaDjango/PracticaDjango/carro.py

class Carro:    
    #...
    def limpiar_carro(self):
        self.session["carro"] = {}
        self.session.modified = True

Otros métodos auxiliares de la clase Carro.


PracticaDjango/PracticaDjango/carro.py

class Carro:    
    #...
    def __iter__(self):
        """
        Iterar sobre los artículos en el carrito y obtener los productos.
         de la base de datos.
        """
        product_ids = self.carro.keys()
        # Obtener los productos y añadirlos al carro
        products = Producto.objects.filter(id__in=product_ids)
        carro = self.carro.copy()
        for product in products:
            carro[str(product.id)]["producto"] = product
        for item in carro.values():
            item["precio"] = Decimal(item["precio"])
            item["precio_total"] = item["precio"] * item["cantidad"]
            yield item

    def __len__(self):
        """
        Cuenta todos los elementos del carro.
        """
        return sum(item["cantidad"] for item in self.carro.values())

    def get_total_price(self):
        return sum(
            Decimal(item["precio"]) * item["cantidad"] for item in self.carro.values()
        )

    def clear(self):
    # elimina el carro de la sesión.
        del self.session[settings.CART_SESSION_ID]
        self.guardar_carro()


Tendremos que iterar varias veces a través de los elementos contenidos en el carrito y acceder a las instancias de Producto relacionadas.

Para hacerlo, definiremos un método iter() en la clase.

En el método iter(), recuperas las instancias de Producto que están presentes en el carrito para incluirlas en los elementos del carrito. Copias el carrito actual en la variable 'carro' y añades las instancias de Producto a este.

filter(id__in=product_ids) es un método de consulta que filtra los productos basándose en la condición especificada. En este caso, estás filtrando los productos por su ID, utilizando el operador __in, que verifica si el ID del producto está dentro de la lista product_ids. Finalmente, iteras sobre los elementos del carrito, convirtiendo el precio de cada elemento de nuevo a decimal y añadiendo un atributo 'precio_total' a cada elemento. Este método iter() te permitirá iterar fácilmente sobre los elementos en el carrito en vistas y plantillas.

También necesitas una manera de devolver el número total de elementos en el carrito. Cuando se ejecuta la función len() en un objeto, Python llama a su método len() para recuperar su longitud. Hemos definido un método len() personalizado para devolver el número total de elementos almacenados en el carrito fácilmente.

También hemos añadido el método "get_total_price()" que devuelve el coste total del carrito que resulta de multiplicar el precio de los productos del carrito por sus cantidades.

Y finalmente agregamos el método clear() que lo que hace es eliminar el carrito de la sesión.


Colocación del carrito a la derecha en la página de la tienda.


Quiero colocar el carrito en la parte superior derecha, al lado de las fichas. La idea es mostrar un carrito de la compra en el que aparezca la cantidad de productos y el coste total. Posteriormente si pinchas en él, te llevará a una página donde se especifique en detalle el contenido del carrito. Algo como esto:


carro



Para ello justo antes de colocar las pestañas con los juegos, colocamos el código del carrito.

PracticaDjango/Tienda/templates/Tienda/tienda.html

...
</div>

  <div class="tab-content">
    
   <!--colocamos el carrito en un float en la parte derecha -->
    <div style="float:right">
      <div>CESTA</div>
      {% include "carro/widget.html" %}
   </div>


    <div class="tab-pane fade show active" id="ps4" role="tabpanel" aria-labelledby="ps4-tab">
      <h3>Juegos PS4</h3>
      <!-- Contenido para juegos PS4 -->
...

- En la primera línea, se crea un contenedor `div` con el atributo `style="float:right"`. Esto se utiliza para colocar el carrito en un formato flotante en la parte derecha de la página web. El uso de `float:right` permite que el carrito aparezca alineado a la derecha, dejando el contenido restante a la izquierda.

- En la tercera línea, se utiliza la etiqueta `{% include "carro/widget.html" %}`. Esta etiqueta de Django se utiliza para incluir y renderizar el contenido de un archivo de plantilla llamado "widget.html" que se encontrará, porque tenemos que crearla todavía, posteriormente en la carpeta "carro" dentro de los templates de la tienda. 

Crea el archivo widget.html dentro de las plantillas de la tienda y para que esté más organizado dentro de un subdirectorio llamado "carro". Su código es el siguiente:

PracticaDjango/Tienda/templates/carro/widget.html

{% with total_items=carro|length %}
<a href="{% url 'carro:mostrar' %}">
    <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="currentColor" class="bi bi-cart-fill"
        viewBox="0 0 16 16">
        <path
            d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .491.592l-1.5 8A.5.5 0 0 1 13 12H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
    </svg>
    <span class="badge rounded-pill bg-danger">{{total_items}}</span>
    <div>
        <span class="badge rounded-pill bg-danger mt-1">{{carro.get_total_price}} €</span>
    </div>
</a>
{% endwith %}

Esta es una de las formas para colocar iconos con Bootstrap. Aquí colocamos el icono de un carrito y a su derecha un circulo rojo con la cantidad de elementos añadidos al carrito. Esto lo conseguimos estableciendo en la variable total_items la longitud de los elementos que están en el carro. Acuérdate que habíamos establecido un __len__ personalizado en la clase Carro, precisamente para esto. En la parte inferior aparecerá el coste total de los productos del carro {{carro.get_total_price}}

Ahora vamos con las vistas. 


Para ello vamos al archivo views.py de la aplicación "Carro". Para poder trabajar con el carro de la compra necesitamos importar su clase, la clase Carro con todos sus métodos y propiedades y también tenemos que importar los productos. 

PracticaDjango/Carro/views.py

from django.shortcuts import render

# Importamos la clase Carro
from .carro import Carro

# Importamos los productos
from Tienda.models import Producto

# cada vez que modifiquemos algo en el carrito tendrá que reflejarse en la 
# pagina de la tienda por lo que tendremos que importar el redirect
from django.shortcuts import redirect

from django.contrib.auth.decorators import login_required

# Create your views here.

# vista para agregar un producto al carro
@login_required
def agregar_producto(request, producto_id):
    carro = Carro(request)
    producto = Producto.objects.get(id=producto_id)
    # agregamos el producto al carro
    carro.agregar(producto=producto)
    # redireccionamos a la tienda
    return redirect("Tienda:tienda")

# vista para eliminar un producto del carro
def eliminar_producto(request, producto_id):
    carro = Carro(request)
    producto = Producto.objects.get(id=producto_id)
    # eliminamos el producto del carro
    carro.eliminar(producto=producto)
    # redireccionamos a la tienda
    return redirect("Tienda:tienda")

# restar un producto del carro
def restar_producto(request, producto_id):
    carro = Carro(request)
    producto = Producto.objects.get(id=producto_id)
    # restamos el producto del carro
    carro.restar_producto(producto=producto)
    # redireccionamos a la tienda
    return redirect("Tienda:tienda")

# vista para limpiar el carro
def limpiar_carro(request):
    carro = Carro(request)
    # limpiamos el carro
    carro.limpiar_carro()
    # redireccionamos a la tienda
    return redirect("Tienda:tienda")


# vista para mostrar los productos del carro
def mostrar_carro(request):
    productos = Producto.objects.all()
    carro = Carro(request)
    contexto = {
        "productos": productos,
        "carro": carro
    }
    return render(request, "Tienda/carro_detalle.html", contexto)


Agregar el botón "Agregar al Carro" a cada tarjeta.


Si queremos añadir cosas al carro, lógicamente cada tarjeta de producto tiene que tener un botón que realice esta opción. Nos vamos al templates de la tienda y añadimos en la sección de cada consola el código necesario. Voy a mostrar el código para la PS4 pero habría que hacer lo mismo para cada una de las demás. 

PracticaDjango/Tienda/templates/Tienda/tienda.html

...
<div class="tab-pane fade show active" id="ps4" role="tabpanel" aria-labelledby="ps4-tab">
      <h3>Juegos PS4</h3>
      <!-- Contenido para juegos PS4 -->
      <div class="row g-4">
        {% for producto in productos %}
        {% if producto.categoria_id == 1 %}
        <div class="col-md-3">
          <div class="card h-100" style="width:200px">
            <img class="card-img-top" src="{{producto.imagen.url}}" alt="Card image">
            <div class="card-body">
              <h4 class="card-title text-center">{{producto.nombre}}</h4>
              <p class="card-text text-center">{{producto.precio}} €</p>
            </div>
            <div class="card-footer text-center">
              <a href="{% url 'carro:agregar' producto.id %}" class="btn btn-success">Agregar al Carro</a>
            </div>
          </div>
        </div>
...


Urls correspondientes a la aplicación carro.

Crea el archivo urls.py dentro de la aplicación del Carro y añade el siguiente código.

PracticaDjango/Carro/urls.py

from django.urls import path
# load views of these applications.
from . import views

app_name = 'carro'

urlpatterns = [
    path('agregar/<int:producto_id>/', views.agregar_producto, name='agregar'),
    path('eliminar/<int:producto_id>/', views.eliminar_producto, name='eliminar'),
    path('restar/<int:producto_id>/', views.restar_producto, name='restar'),
    path('limpiar/', views.limpiar_carro, name='limpiar'),
    path('mostrar/', views.mostrar_carro, name='mostrar'),
    ]

Este código configura el enrutamiento de URLs en Django. 

1. `from django.urls import path`: Esto importa la función `path` del módulo `django.urls`, que se utiliza para definir las rutas URL en Django.

2. `from . import views`: Esto importa las vistas definidas en el archivo `views.py` del mismo directorio.

3. `app_name = 'carro'`: Esto establece el nombre de la aplicación, que se utiliza para evitar conflictos en la resolución de nombres de URL si tienes múltiples aplicaciones en tu proyecto Django.

4. `urlpatterns`: Es una lista de patrones de URL que se deben asociar a las vistas correspondientes.

   - `path('agregar/<int:producto_id>/', views.agregar_producto, name='agregar')`: Este patrón de URL se asocia a la vista `agregar_producto` cuando se accede a la URL `/agregar/<producto_id>/`. El `<int:producto_id>` define una parte variable de la URL que debe ser un entero y se pasa como argumento a la vista.

   - `path('eliminar/<int:producto_id>/', views.eliminar_producto, name='eliminar')`: Este patrón de URL se asocia a la vista `eliminar_producto` cuando se accede a la URL `/eliminar/<producto_id>/`. Al igual que en el caso anterior, `<int:producto_id>` define una parte variable de la URL que debe ser un entero.

   - `path('restar/<int:producto_id>/', views.restar_producto, name='restar')`: Este patrón de URL se asocia a la vista `restar_producto` cuando se accede a la URL `/restar/<producto_id>/`. Igual que antes, `<int:producto_id>` define una parte variable de la URL que debe ser un entero.

   - `path('limpiar/', views.limpiar_carro, name='limpiar')`: Este patrón de URL se asocia a la vista `limpiar_carro` cuando se accede a la URL `/limpiar/`. No hay partes variables en esta URL.

     - `path('mostrar/', views.mostrar_carro, name='mostrar')`: Este patrón de URL se asocia a la vista `limpiar_carro` cuando se accede a la URL `/limpiar/`. No hay partes variables en esta URL.

Cada patrón de URL se asocia a una vista específica definida en el archivo `views.py`, y el parámetro `name` se utiliza para referirse a las URLs de manera fácil y legible en otras partes del código.

Y como siempre tenemos que entrar en urls.py de la aplicación PracticaDjango y añadir las urls de la aplicación carro:

PracticaDjango/PracticaDjango/urls.py

from django.contrib import admin
from django.urls import path, include

from django.conf import settings
from django.conf.urls.static import static

from django.contrib.sitemaps.views import sitemap
from .sitemaps import BlogSitemap, StaticSitemap

sitemaps = {
    'static':StaticSitemap, #add StaticSitemap to the dictionary
    'blog':BlogSitemap #add DynamicSitemap to the dictionary
}

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),
    path('servicios/', include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path('contacto/', include('Contacto.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('tienda/', include('Tienda.urls')),
    path('carro/', include('Carro.urls')),
]
urlpatterns+=static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Lo mas importante context_processor.


Un context processor es una función en Python que toma el objeto de solicitud como argumento y devuelve un diccionario que se agrega al contexto de la solicitud. Los context processors son útiles cuando necesitas hacer algo disponible globalmente para todas las plantillas.

Por defecto, cuando creas un nuevo proyecto usando el comando startproject, tu proyecto contiene los siguientes context processors de plantilla en la opción context_processors dentro de la configuración TEMPLATES:
- django.template.context_processors.debug: Esto establece las variables Boolean debug y sql_queries en el contexto, representando la lista de consultas SQL ejecutadas en la solicitud.
- django.template.context_processors.request: Esto establece la variable request en el contexto.
- django.contrib.auth.context_processors.auth: Esto establece la variable user en la solicitud.
- django.contrib.messages.context_processors.messages: Esto establece una variable messages en el contexto que contiene todos los mensajes generados usando el framework de mensajes.

Django también habilita django.template.context_processors.csrf para evitar ataques de falsificación de solicitudes entre sitios (CSRF). Este context processor no está presente en la configuración, pero siempre está habilitado y no se puede desactivar por razones de seguridad.

Puedes ver la lista de todos los context processors integrados en https://docs.djangoproject.com/en/5.0/ref/templates/api/#built-in-template-context-processors.

En resumen, es una variable que esta disponible en todo el proyecto a nivel global de forma que este disponible o se ejecute cada vez que se cargue cualquier vista. Nos va a servir para que el carro muestre el número de productos del carrito y su importe total. 


Configurando el carrito en el contexto de la solicitud


Creemos un context processor para establecer el carrito actual en el contexto de la solicitud. Con esto, podrás acceder al carrito en cualquier plantilla.

Crea un archivo nuevo dentro del directorio de la aplicación del Carro y llámalo context_processors.py. Los context processors pueden estar en cualquier lugar de tu código, pero al colocarlos aquí mantendrás tu código bien organizado.

Añade el siguiente código al archivo:


PracticaDjango/Carro/context_processor.py

from .carro import Carro

def carro(request):
    return {'carro': Carro(request)}

En tu context processor, instancias el carrito utilizando el objeto de solicitud y lo pones a disposición de las plantillas como una variable llamada carro.


Para que esto sea accesible desde cualquier parte del proyecto tenemos que registrarlo en el archivo settings.py del proyecto, dentro del apartado "TEMPLATES".


PracticaDjango/PracticaDjango/settings.py

...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                # para que la variable global este disponible para todo el proyecto
                'Carro.context_processor.carro',
            ],
        },
    },
]
...

El context processor del carrito se ejecutará cada vez que se renderice una plantilla usando el RequestContext de Django. La variable carro se establecerá en el contexto de tus plantillas. Puedes obtener más información sobre RequestContext en https://docs.djangoproject.com/en/5.0/ref/templates/api/#django.template.RequestContext.

Los context processors se ejecutan en todas las solicitudes que utilizan RequestContext. Si tu funcionalidad no es necesaria en todas las plantillas, especialmente si implica consultas a la base de datos, puede que quieras crear una etiqueta de plantilla personalizada en lugar de un context processor.

Bueno, una vez que hemos creado el icono del carro y hemos visto como agregar los productos, nos vamos a poner con como mostrar el contenido del carrito para poder eliminar los productos o restar un producto de la lista, así como un botón para volver a comprar más productos o finalizar la misma.

Y ya solamente nos queda construir la plantilla html que mostrará el detalle de los productos del carro.

Creamos un archivo llamado carro_detalle.html.

PracticaDjango/Tienda/templates/Tienda/carro_detalle.html

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}
{% load thumbnail %}

<!-- Establecemos el titulo de la página -->
{% block title %}Carro{% endblock %}

<!-- Definimos su contenido -->
{% block content %}
<div class="container">
    <div class="bg-white text-black">
        {% with total_items=carro|length %}
        {% if total_items > 0 %}
        <h1>Tu Carro de la Compra</h1>
        <p class="ms-3">
            <a href="{% url 'carro:limpiar' %}" style="color: #0fa4b8; text-decoration: none;" class="link">
                Anular la selección de todos los artículos</a>
        </p>
        <hr>
    </div>
    <table class="table table-dark table-striped">
        <thead>
            <tr>
                <th>Imagen</th>
                <th>Producto</th>
                <th>Cantidad</th>
                <th>Eliminar</th>
                <th>Precio Unitario</th>
                <th>total</th>
            </tr>
        </thead>
        <tbody>
            {% for item in carro %}
            {% with producto=item.producto %}
            <tr>
                <td>
                    <img src="{% thumbnail producto.imagen 100x0 %}">
                </td>
                <td>{{ producto.nombre }}</td>
                <td>{{ item.cantidad }}</td>
                <td>
                    <a href="{% url 'carro:eliminar' producto.id %}" class="btn btn-danger">Eliminar</a>
                    <a href="{% url 'carro:restar' producto.id %}" class="btn btn-primary">Restar 1 ud</a>

                </td>
                <td class="num">{{ item.precio }} €</td>
                <td class="num">{{item.precio_total}}</td>
            </tr>
            {% endwith %}
            {% endfor %}
            <tr class="total">
                <td>Total</td>
                <td colspan="4"></td>
                <td class="num">{{ carro.get_total_price }} €</td>
            </tr>
        </tbody>
    </table>
    <div class="d-flex justify-content-end">
        <p class="text-right">
            <a href="{% url 'Tienda:tienda' %}" class="btn btn-primary">Continua comprando</a>
            <a href="#" class="btn btn-success ml-2">Checkout</a>
        </p>
    </div>
    
    
    {% else %}
    <div class="container py-4">
        <div class="bg-white text-black">
            <h3 class="ms-3 pt-3">Tu cesta de UnikGAME está vacía.</h3>
            <p class="ms-3 pb-3">Revisa tus productos guardados para más tarde o
                <a href="{% url 'Tienda:tienda' %}">continúa comprando.</a>
            </p>
        </div>
    </div>
    {% endif %}
    {% endwith %}

</div>
{% endblock %}
   

Y ya está. Si el carro no contiene ningún producto, se mostrara un mensaje de advertencia como este.

cesta vacía


y si tiene algún producto la imagen será parecida a esta.


imagen de la cesta de la compra



Registrar los pedidos de los clientes.


Cuando se finaliza una compra en el carrito de compras, es necesario guardar un pedido en la base de datos. Los pedidos contendrán información sobre los clientes y los productos que están comprando.

Crea una nueva aplicación para gestionar pedidos de clientes usando el siguiente comando:

python manage.py startapp Orders

Edita el archvio settings.py del proyecto y registra la aplicación de la siguiente manera:

PracticaDjango/PracticaDjango/settings.py

...
NSTALLED_APPS = [
    # Nuestras aplicaciones
    'Proyecto_web_app.apps.ProyectoWebAppConfig',
    'Servicios.apps.ServiciosConfig',
    'Blog.apps.BlogConfig',
    'Contacto.apps.ContactoConfig',
    'Tienda.apps.TiendaConfig',
    'Carro.apps.CarroConfig',
    'Orders.apps.OrdersConfig',
    # Aplicaciones de terceros

Creando modelos de pedido

Necesitarás un modelo para almacenar los detalles del pedido y un segundo modelo para almacenar los artículos comprados, incluyendo su precio y cantidad. Edita el archivo models.py de la aplicación de pedidos y agrega el siguiente código a él:

PracticaDjango/Orders/models.py

from django.db import models
from Tienda.models import Producto

# Create your models here.


class Order(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    address = models.CharField(max_length=250)
    postal_code = models.CharField(max_length=20)
    city = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    paid = models.BooleanField(default=False)

    class Meta:
        ordering = ["-created"]
        indexes = [
            models.Index(fields=["-created"]),
        ]

    def __str__(self):
        return f"Orden {self.id}"

    def get_total_cost(self):
        return sum(item.get_cost() for item in self.items.all())


class OrderItem(models.Model):
    order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE)
    product = models.ForeignKey(
        Producto, related_name="order_items", on_delete=models.CASCADE
    )
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)

    def __str__(self):
        return str(self.id)

    def get_cost(self):
        return self.price * self.quantity

El modelo Order contiene varios campos para almacenar información del cliente y un campo Booleano llamado 'paid', que por defecto se establece en False. Más adelante, usarás este campo para diferenciar entre pedidos pagados y no pagados. También hemos definido un método get_total_cost() para obtener el costo total de los artículos comprados en este pedido.

El modelo OrderItem te permite almacenar el producto, la cantidad y el precio pagado por cada artículo. Hemos definido un método get_cost() que devuelve el costo del artículo multiplicando el precio del artículo por la cantidad.

Para crear las migraciones iniciales para la aplicación de pedidos, ejecuta el siguiente comando:

python manage.py makemigrations


Verás una salida similar a la siguiente:

```
Migraciones para 'orders':
orders/migrations/0001_initial.py
- Crear modelo Order
- Crear modelo OrderItem
- Crear índice orders_orde_created_743fca_idx en el campo(s) -created del modelo order
```

Luego, ejecuta el siguiente comando para aplicar la nueva migración:

python manage.py migrate


Esto aplicará las migraciones recién creadas a tu base de datos.


Incluyendo los modelos en el panel de administración.


Para incluir los modelos de pedido en el sitio de administración, edita el archivo admin.py de la aplicación de pedidos y agregar el siguiente código:

PracticaDjango/Orders/admin.py

from django.contrib import admin
from .models import Order, OrderItem

# Register your models here.
class OrderItemInline(admin.TabularInline):
    model = OrderItem
    raw_id_fields = ['product']

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email',
                    'address', 'postal_code', 'city', 'paid',
                    'created', 'updated']
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]

Se está utilizando una clase ModelInline para el modelo OrderItem para incluirlo como un "inline" en la clase OrderAdmin. Un inline te permite incluir un modelo en la misma página de edición que su modelo relacionado.

El comando `python manage.py runserver` se utiliza para ejecutar el servidor de desarrollo. Al acceder a http://127.0.0.1:8000/admin/Orders/order/add/en tu navegador, verás la página correspondiente.

The Add order form, including OrderItemInline



Creación de órdenes de clientes


Utilizarás los modelos de pedido que creaste para guardar los artículos contenidos en el carrito de compras cuando el usuario finalmente realiza un pedido. Se creará un nuevo pedido siguiendo estos pasos:

1. Presentar al usuario un formulario de pedido para que complete sus datos

2. Crear una nueva instancia de Pedido con los datos ingresados y crear una instancia asociada de OrderItem para cada ítem en el carrito

3. Borrar todo el contenido del carrito y redirigir al usuario a una página de finalización del pedido correcta.

Primero, necesitas un formulario para ingresar los detalles del pedido. Crea un nuevo archivo dentro del directorio de la aplicación de pedidos y nómbralo forms.py. Agrega el siguiente código:

PracticaDjango/Orders/forms.py

from django import forms
from .models import Order


class OrderCreateForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = ["first_name", "last_name", "email", "address", "postal_code", "city"]
Este es el formulario que vas a utilizar para crear nuevos objetos Order. Ahora necesitas una vista para manejar el formulario y crear un nuevo pedido. Edita el archivo views.py de la aplicación de pedidos y agrega el siguiente código resaltado:

PracticaDjango/Orders/views.py

from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from Carro.carro import Carro


# Create your views here.
def order_create(request):
    carro = Carro(request)
    if request.method == "POST":
        form = OrderCreateForm(request.POST)
        if form.is_valid():
            order = form.save()
            for item in carro:
                OrderItem.objects.create(
                    order=order,
                    product=item["producto"],
                    price=item["precio"],
                    quantity=item["cantidad"],
                )
            # Limpia el carro
            carro.clear()
            return render(request, "Orders/order/created.html", {"order": order})
    else:
        form = OrderCreateForm()
    return render(request, "Orders/order/create.html", {"cart": carro, "form": form})
En la vista order_create, obtienes el carrito actual de la sesión con carro = Carro(request).

Dependiendo del método de solicitud, realizas las siguientes tareas:

- Solicitud GET: Instancias el formulario OrderCreateForm y renderizas la plantilla orders/order/create.html.

- Solicitud POST: Validas los datos enviados en la solicitud. Si los datos son válidos, creas un nuevo pedido en la base de datos usando order = form.save(). Iteras sobre los elementos del carrito y creas un OrderItem para cada uno de ellos. Finalmente, borras el contenido del carrito y renderizas la plantilla orders/order/created.html.

Crea un archivo nuevo dentro del directorio de la aplicación de pedidos y llámalo urls.py. Agrega el siguiente código:

PracticaDjango/Orders/urls.py

from django.urls import path
from . import views

app_name = 'orders'

urlpatterns = [
path('create/', views.order_create, name='order_create'),
]
Este es el patrón de URL para la vista order_create.

Edita el archivo urls.py de la aplicación principal e incluye el siguiente patrón. La nueva línea está resaltada en negrita:

PracticaDjango/PracticaDjango/urls.py

urlpatterns = [
    #...
    path('orders/', include('Orders.urls', namespace='orders')),
]
Edita la plantilla Tienda/carro_detalle.html de la aplicación Tienda y encuentra esta línea:

PracticaDjango/Tienda/templates/Tienda/carro_detalle.html

<div class="d-flex justify-content-end">
        <p class="text-right">
            <a href="{% url 'Tienda:tienda' %}" class="btn btn-primary">Continua comprando</a>
            <a href="#" class="btn btn-success ml-2">Checkout</a>
        </p>
</div>
y cambia el # de la línea resaltada por este código:

PracticaDjango/Tienda/templates/Tienda/carro_detalle.html

<div class="d-flex justify-content-end">
        <p class="text-right">
            <a href="{% url 'Tienda:tienda' %}" class="btn btn-primary">Continua comprando</a>
            <a href="{% url 'orders:order_create' %}" class="btn btn-success ml-2">Checkout</a>
        </p>
</div>

Los usuarios ahora pueden navegar desde la página de detalle del carrito hasta el formulario de pedido.

Aún necesitas definir plantillas para crear pedidos. Crea la siguiente estructura de archivos dentro del directorio de la aplicación de órdenes (Orders):

templates/
        Orders/
                order/
                        create.html
                        created.html

Edita la plantilla Orders/order/create.html y agrega el siguiente código:

PracticaDjango/Orders/templates/Orders/order/create.html

{% extends "Proyecto_web_app/base.html" %}
{% block title %}Checkout{% endblock %}

{% block content %}
<div class="custom-container">
    <h1>Checkout</h1>
    <div class="row">
        <div class="col-md-5 text-end">
            <div class="card">
                <div class="card-body">
                    <h3>Formulario Pedido</h3>
                    <form method="post" class="order-form">
                        {% csrf_token %}
                        <div class="mb-3">
                            {{ form.first_name.errors }}
                            <label for="{{ form.first_name.id_for_label }}">Nombre:</label>
                            {{ form.first_name }}
                        </div>
                        <div class="mb-3">
                            {{ form.last_name.errors }}
                            <label for="{{ form.last_name.id_for_label }}">Apellidos:</label>
                            {{ form.last_name }}
                        </div>
                        <div class="mb-3">
                            {{ form.email.errors }}
                            <label for="{{ form.email.id_for_label }}">Email:</label>
                            {{ form.email }}
                        </div>
                        <div class="mb-3">
                            {{ form.address.errors }}
                            <label for="{{ form.address.id_for_label }}">Dirección:</label>
                            {{ form.address }}
                        </div>
                        <div class="mb-3">
                            {{ form.postal_code.errors }}
                            <label for="{{ form.postal_code.id_for_label }}">Código Postal:</label>
                            {{ form.postal_code }}
                        </div>
                        <div class="mb-3">
                            {{ form.city.errors }}
                            <label for="{{ form.city.id_for_label }}">Localidad:</label>
                            {{ form.city }}
                        </div>
                        <!-- Agrega los campos restantes del formulario de manera similar -->
                        <div class="mb-3">
                            <input type="submit" class="btn btn-primary" value="Realizar Pedido">
                        </div>
                    </form>
                </div>
            </div>
        </div>
        <div class="col-md-7">
            <div class="order-info">
                <h3>Your order</h3>
                <ul>
                    {% for item in cart %}
                    <li>
                        {{ item.cantidad }}x {{ item.producto.nombre }}
                        <span>{{ item.precio_total }} €</span>
                    </li>
                    {% endfor %}
                </ul>
                <p>Total: {{ cart.get_total_price }} €</p>
            </div>
        </div>
    </div>
</div>
{% endblock %}
Esta plantilla muestra los elementos del carrito, incluyendo totales y el formulario para realizar un pedido.

Edita la plantilla Orders/order/created.html y agrega el siguiente código:

PracticaDjango/Orders/templates/Orders/order/create.html

{% extends "Proyecto_web_app/base.html" %}
{% block title %}Gracias{% endblock %}

{% block content %}
<h1>¡Muchas Gracias!</h1>
<p>Tu pedido se ha completado exitosamente. El número de tu pedido es el
<strong>{{ order.id }}</strong>.</p>
{% endblock %}
Esta es la plantilla que se muestra cuando se crea el pedido correctamente.

Inicia el servidor de desarrollo web para cargar los nuevos archivos. Abre http://127.0.0.1:8000/ en tu navegador, ve a la tienda, agrega un par de productos al carrito y continúa hacia la página de pago. 

Verás el siguiente formulario:

formulario para completar el pedido



Completa el formulario con datos válidos y haz clic en el botón 'Realizar pedido'. Se creará el pedido y verás una página de éxito como esta:

pedido realizado con éxito




Puedes encontrar el contenido de este capítulo en este enlace de github.