Mostrando entradas con la etiqueta csv. Mostrar todas las entradas
Mostrando entradas con la etiqueta csv. Mostrar todas las entradas

sábado, 27 de abril de 2024

Campeones de Gráficos.

 Basado en el proyecto de la RapsberryPi Org "Charting Champions".

¿Qué es lo que haremos?

Descubriremos el poder de las listas en Python creando un gráfico interactivo de la medallas olímpicas conseguidas por los diferentes países.

Los juegos olímpicos empezaron en el año 1896: miles de atletas representaron a centenares de naciones de todo el mundo. Los juegos olímpicos modernos se inspiraron en las olimpiadas que se realizaban en Grecia en la antigüedad.

Lo que haremos:

- Usar listas para almacenar los datos.

- Crear un gráfico usando la librería Pygal.

- Cargar los datos haciendo que tu programa los lea desde un archivo.


Creando un gráfico.

Lo primero que haremos será instalar la librería que vamos a utilizar para crear el gráfico, la librería Pygal. Para presentar los gráficos por defecto se utiliza el formato SVG (escalable vector graphics), que incluso nos servirá para incluirlo en una página web.

Información: INSTALANDO PYGAL

En WINDOWS:

En la consola de comandos teclea lo siguiente y pulsa la tecla Enter.

pip install pygal

Espera que la instalación se complete y luego continua con el proyecto.

En un MAC:

En una ventana de terminal teclea lo siguiente y pulsa la tecla Enter.

pip3 install pygal

Espera que la instalación se complete y luego continua con el proyecto.

En LINUX, incluyendo el sistema operativo de la Rapsberry Pi.

En una ventana de terminal teclea lo siguiente y pulsa la tecla Enter.

pip3 install pygal

Espera que la instalación se complete y luego continua con el proyecto.
Para comenzar el proyecto importaremos algo de código de la librería pygal que utilizaremos para dibujar el gráfico. Empieza creando un nuevo archivo, por ejemplo main.py, y teclea el siguiente código:

from pygal import Bar

Después vamos a crear un gráfico de barras para mostrar los resultados. Añade el siguiente código y luego ejecútalo:

from pygal import Bar
import webbrowser

# Creando un gráfico
grafico = Bar(title="Medallas Olímpicas")
grafico.render_to_file('prueba.svg')
webbrowser.open('prueba.svg')

Si todo ha ido bien se abrirá tu navegador predeterminado y te mostrará la siguiente imagen:


grafico creado sin datos


Vamos a pararnos un momento para explicar el código. Aunque en teoría para renderizar el gráfico simplemente se podría usar grafico.render()  a la fecha de este post no lo he conseguido en la versión que uso de Debian o Ubuntu. No obtengo ningún error, pero tampoco se renderiza el gráfico. Así que he optado por pasar el gráfico renderizado a un archivo con grafico.renter_to_file('prueba.svg') y luego abrirlo con el navegador para visualizarlo. Para ello he importado la librería webbrowser y luego he utilizado el método open para abrir el grafico creado en el navegador. No obstante también se permite abrir directamente el grafico en el navegador usando grafico.render_in_browser(), si prefieres pero seguramente tendrás que instalar una librería dependiente (lxlm).

Otro inciso antes de seguir es que además de diagramas de barras también podemos usar muchos más tipos de gráficos como líneas, gráficos circulares, etc. Para más información consulta la documentación


Añadiendo algunos datos.


Para almacenar los datos vamos a utilizar las listas de Python. Puedes crearlas poniendo los datos separados por comas entre corchetes [ ]. 

Vamos a crear cuatro listas con datos para mostrar en nuestro gráfico.

Cada lista guardará el nombre de la nación y el número de medallas que ha ganado.

# Añadiendo datos
us = ['Estados Unidos', 2655]
gb = ['Gran Bretaña', 931]
fr = ['Francia', 772]
sp = ['España', 169]
Cuando guardas algo en una lista, cada uno de los elementos tiene un índice. Un índice es un número que nos dice la posición del elemento en la lista. Los índices comienzan en el 0 en vez de en el 1.

Puedes obtener cualquier elemento de una lista a partir de su índice. Por ejemplo, mi_lista[3] nos devolverá el cuarto elemento de esa lista, porque recuerda que el índice comienza en el cero. 

Utiliza los índices de tu lista y grafico.add() para mostrar los datos. El nombre del país que está en el primer elemento de la lista (indice 0) se utilizará como etiqueta de la categoría y la cantidad de medallas que es el segundo elemento de la lista (índice 1) determinará la altura de la barra.

# Añadiendo datos
us = ['Estados Unidos', 2655]
gb = ['Gran Bretaña', 931]
fr = ['Francia', 772]
sp = ['España', 169]

grafico.add(us[0], us[1])
grafico.add(gb[0], gb[1])
grafico.add(fr[0], fr[1])
grafico.add(sp[0], sp[1])

Ejecuta el código para ver el gráfico. 


gráfico creado con algunos datos

Si obtienes un mensaje de ERROR del tipo IndexError, es que tu código esta intentando obtener un valor desde un indice que no existe en la lista (por ejemplo us[2]). Para solucionarlo:

- comprueba cada línea grafico.add() y comprueba que solo estás utilizando los indices 0 y 1.

- comprueba las líneas donde creaste las listas. Verifica que cada lista tenga solo dos elementos, separados por una coma.

Ahora, introduciremos dos nuevos paises, añadiendo nuevas listas y luego las cargaremos en el gráfico usando grafico.add().

# Añadiendo datos
us = ['Estados Unidos', 2655]
gb = ['Gran Bretaña', 931]
fr = ['Francia', 772]
sp = ['España', 169]
ch = ['China', 634]
al = ['Alemania', 797]

grafico.add(us[0], us[1])
grafico.add(gb[0], gb[1])
grafico.add(fr[0], fr[1])
grafico.add(sp[0], sp[1])
grafico.add(ch[0], ch[1])
grafico.add(al[0], al[1])

Vuelve a ejecutar el código para actualizar el gráfico.


gráfico con más datos añadidos


Cargando los datos desde un archivo.


¡Nuestro gráfico tiene buena pinta! Pero, casi 150 naciones han competido en los juegos olimpico de verano. Para hacer un gráfico con todos, vamos a cargar sus datos desde un archivo lo que nos hará ahorrar un montón de tiempo en teclear datos.

Si vas a la carpeta del proyecto en Github encontrarás un archivo csv llamado medals.csv. Ábrelo y echa un vistazo a los datos que contiene.


imagen del archivo csv

Como ves en cada línea aparece el nombre del equipo, y el número de medallas que ha ganado hasta los últimos juegos olímpicos de verano de Tokio 2020. (Oro, plata, bronce y total de medallas)

Información: Archivos CSV
Los archivos CSV (Comma Separated Values) o archivos con valores separados
por comas, contienen datos en filas y columnas como en una tabla. Cada lí-
nea es una fila, en la que los datos (columnas) están separados por comas.

Pais, Abreviatura, Oro, Plata, Bronce, Total

United States,USA,1070,841,744,2655
Soviet Union,URS,395,319,296,1010
Great Britain,GBR,292,324,315,931
Germany,GER,239,267,291,797

Para hacer el gráfico tenemos que volcar la información de cada línea al programa que estamos haciendo, al igual que hicimos con las listas anteriormente.

Para ello vuelve al archivo donde tienes el código del programa y añade el siguiente código que nos permitirá cargar los datos del archivo en una variable, para ello utilizaremos "with open() as". Después mediante un bucle for imprimiremos los datos de cada línea.

El usar el bucle for hace que se repita el código. Así que cargaremos cientos de líneas con los equipos participantes y sus datos ¡con unas pocas líneas de código!

Añade el siguiente código.

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        print(linea)


¿Cómo se lee un archivo con Python?

Para leer un archivo de texto en Python debes abrir el archivo y leer su contenido. 

Cuando abras un archivo, utiliza los comandos with con as. Con esto te asegurarás de que una vez ejecutado el código que contiene, el archivo se cerrará automáticamente. El argumento dentro de open() en este caso 'medals.csv' es el nombre del archivo de texto que queremos abrir. 

Una vez que hemos cargado el archivo, tenemos dos opciones. La primera es volcarlo íntegramente en una variable y una segunda que es usar un bucle for para recorrer el archivo línea por línea.

La primera opción no la hemos usado en este caso pero sería algo como esto:

with open(nombre_archivo) as f:
  archivo_texto = f.read()
  # Hacer algo con el texto

La segunda opción es la que hemos usado nosotros para iterar sobre cada línea del código:

with open(nombre_archivo) as f:
  for línea_archivo in f:
    # hacer algo con la línea.

Si ejecutas ahora el código verás como cada línea tiene seis valores separados por comas.

United States,USA,1070,841,744,2655

Soviet Union,URS,395,319,296,1010

Great Britain,GBR,292,324,315,931

Germany,GER,239,267,291,797

France,FRA,231,256,285,772

People's Republic,of China,CHN,263,199,174,636

Italy,ITA,222,195,215,632

Australia,AUS,162,170,209,541

Hungary,HUN,182,156,177,515

Sweden,SWE,149,177,181,507

Japan,JPN,169,150,180,499

Cada cadena que imprime el bucle se compone de seis valores separados por comas. La función grafico.add() necesita cada una de esas piezas pero como entradas separadas. 

La función split() divide una cadena en una lista, al igual que las listas que hicimos al principio del post. Si usamos split(',') se creara una lista poniendo un nuevo elemento cada vez que encuentre una coma, es decir separa el texto en diferentes valores usando para ello la coma. 

Consejo: split() puede dividir una cadena en una lista usando como separador cualquier texto que se desee, no tiene porque ser una coma, puede ser un punto, una letra e incluso una palabra.

Modifica un poco el código para que recoja esto que hemos comentado:

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        dato = linea.split(',')
        print(dato)

Si lo ejecutas verás lo siguiente:

>>> %Run main.py
['United States', 'USA', '1070', '841', '744', '2655\n']
['Soviet Union', 'URS', '395', '319', '296', '1010\n']
['Great Britain', 'GBR', '292', '324', '315', '931\n']
['Germany', 'GER', '239', '267', '291', '797\n']
['France', 'FRA', '231', '256', '285', '772\n']
["People's Republic", 'of China', 'CHN', '263', '199', '174', '636\n']
['Italy', 'ITA', '222', '195', '215', '632\n']
['Australia', 'AUS', '162', '170', '209', '541\n']
['Hungary', 'HUN', '182', '156', '177', '515\n']
...

Como te habrás dado cuenta cada línea finaliza con "\n" al final. "\n" es normalmente invisible cuando se imprime un texto, por eso no lo has visto antes. Es un salto de línea, le dice al ordenador cuando imprime un texto, que se ha alcanzado el final de la línea.

Sin embargo como para hacer nuestro gráfico necesitamos el primer valor, el nombre del equipo, y el último, el número total de medallas, tenemos que quitar el salto de línea "\n". Para ello usaremos el método strip() que utilizado sin argumentos elimina los espacios en blanco y saltos de línea que pudiera haber tanto al principio como al final de la cadena.

El código sería de momento este:

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        linea = linea.strip()
        dato = linea.split(",")
        print(dato)

Salida:

>>> %Run main.py
['United States', 'USA', '1070', '841', '744', '2655']
['Soviet Union', 'URS', '395', '319', '296', '1010']
['Great Britain', 'GBR', '292', '324', '315', '931']
['Germany', 'GER', '239', '267', '291', '797']
['France', 'FRA', '231', '256', '285', '772']
["People's Republic", 'of China', 'CHN', '263', '199', '174', '636']
['Italy', 'ITA', '222', '195', '215', '632']
['Australia', 'AUS', '162', '170', '209', '541']

Antes de cargar los datos en el gráfico nos queda una cuestión. Todos los valores que nos pasa la cadena son de tipo string. Eso no nos importa para el nombre del equipo puesto que el primer argumento que le pasamos a grafico.add() es un texto para que lo use como etiqueta. Sin embargo para el número total de medallas necesitamos que el dato se un número entero. Podemos usar la función int() para pasar ese dato de tipo texto a número. 

Ten además en cuenta que el nombre del equipo es el primer elemento de la lista (indice 0)  y que el número total de medallas es el elemento seis (indice 5). Con esto ya podemos construir el grafico. El programa completo quedaría de la siguiente forma:

from pygal import Bar
import webbrowser

# Creando un gráfico
grafico = Bar(title="Medallas Olímpicas")

# Añadiendo datos
# us = ['Estados Unidos', 2655]
# gb = ['Gran Bretaña', 931]
# ch = ['China', 634]
# al = ['Alemania', 797]
# fr = ['Francia', 772]
# sp = ['España', 169]

# grafico.add(us[0], us[1])
# grafico.add(gb[0], gb[1])
# grafico.add(fr[0], fr[1])
# grafico.add(sp[0], sp[1])
# grafico.add(ch[0], ch[1])
# grafico.add(al[0], al[1])

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        linea = linea.strip()
        dato = linea.split(',')
        grafico.add(dato[0], int(dato[5]))
        
        
grafico.render_to_file('prueba.svg')
webbrowser.open('prueba.svg')

Ejecuta el código y mira como se crea el gráfico:


gráfico final del proyecto

¿Qué podemos hacer a mayores?

Para profundizar algo más en la forma de hacer gráficos, podemos cambiar la forma de representar los datos. 

Podríamos por ejemplo:

Crear un gráfico circular -  Para crear un gráfico circular en vez de un gráfico de barras solo tenemos que cambiar el código de importación, para usar Pie en vez de Bar. Una vez importada la creación o instancia del gráfico la haremos usando Pie.

gráfico circular




Puedes encontrar los enlaces a este proyecto en esta dirección de Github.

lunes, 1 de abril de 2024

¿Donde está la Estación Espacial Internacional?

 Basado en el proyecto de la rapsberry pi org ¿Where is the Space Station?

Introducción.


En este proyecto vamos a usar un servicio web para encontrar la localización actual de la Estación Espacial Internacional o ISS por sus siglas en inglés. Con esos datos mostraremos su posición en un mapa.


Instrucciones.


El icono de la Estación Espacial Internacional aparecerá en el mapa mostrando donde está la ISS actualmente. El texto en amarillo nos indicará las personas que actualmente están en el espacio.


¿Quién está en el espacio?


Para saber el número de astronautas que están en la estación espacial internacional, vamos a usar un servicio web que nos va a proporcionar esa información y más.

Un servicio web tiene una dirección web (URL) al igual que muchas de las páginas web que visitas habitualmente. Lo único que en vez de devolvernos una página HTML  lo que nos devuelve son datos.

Prueba a entrar en la siguiente dirección web http://open-notify.org/Open-Notify-API/. Aquí encontraremos toda la información que necesitaremos. Esta página es de uso libre y nos la facilita y mantiene Nathan Bergey.

Comencemos buscando información sobre los astronautas que están en la estación. 

Abre está dirección en el navegador. http://api.open-notify.org/astros.json

Si todo ha ido bien deberías ver algo como esto:

{
  "message": "success",
  "people": [
    {
      "name": "Jasmin Moghbeli",
      "craft": "ISS"
    },
    {
      "name": "Andreas Mogensen",
      "craft": "ISS"
    },
    {
      "name": "Satoshi Furukawa",
      "craft": "ISS"
    },
    {
      "name": "Konstantin Borisov",
      "craft": "ISS"
    },
    {
      "name": "Oleg Kononenko",
      "craft": "ISS"
    },
    {
      "name": "Nikolai Chub",
      "craft": "ISS"
    },
    {
      "name": "Loral O'Hara",
      "craft": "ISS"
    }
  ],
  "number": 7
}

Los datos cambian continuamente, así que seguramente si estás consultando este post verás algo diferente. El formato en el que recibimos los datos se denomina "JSON". Se parecen bastante a los diccionario en Python, aunque ojo que no son iguales.

Vamos a construir un script de Python para poder usar esos datos.

Empieza creando un archivo de python y llámalo como quieras.

Lo primero que haremos es importar las librerías que necesitaremos para nuestro proyecto que son urllib.request, json y turtle

Ahora copia el siguiente código:

""" ¿Donde está la Estación Espacial Internacional? """

import urllib.request
import json
import turtle

# http://open-notify.org/Open-Notify-API/
url = 'http://api.open-notify.org/astros.json'
respuesta = urllib.request.urlopen(url)
astronautas = json.loads(respuesta.read())
print(astronautas)

Salida:

{'message': 'success', 'people': [{'name': 'Jasmin Moghbeli', 'craft': 'ISS'}, {'name': 'Andreas Mogensen', 'craft': 'ISS'}, {'name': 'Satoshi Furukawa', 'craft': 'ISS'}, {'name': 'Konstantin Borisov', 'craft': 'ISS'}, {'name': 'Oleg Kononenko', 'craft': 'ISS'}, {'name': 'Nikolai Chub', 'craft': 'ISS'}, {'name': "Loral O'Hara", 'craft': 'ISS'}], 'number': 7}


Este código utiliza la API (servicio web) de Open Notify para obtener información sobre la Estación Espacial Internacional (ISS, por sus siglas en inglés) y los astronautas que se encuentran en ella en ese momento. Vamos a ver como funciona.

1. `import urllib.request`: Importa el módulo `urllib.request`, que se utiliza para abrir y leer URL.

2. `import json`: Importa el módulo `json`, que se utiliza para trabajar con datos en formato JSON (JavaScript Object Notation).

3. `import turtle`: Importa el módulo `turtle`, que se utiliza para crear gráficos mediante programación.

4. `url = 'http://api.open-notify.org/astros.json'`: Define la URL de la API de Open Notify que proporciona información sobre los astronautas que se encuentran en la Estación Espacial Internacional en ese momento.

5. `respuesta = urllib.request.urlopen(url)`: Abre la URL definida anteriormente y almacena la respuesta en la variable `respuesta`. Es un objeto de Python.

6. `astronautas = json.loads(respuesta.read())`: Lee el contenido de la respuesta HTTP y lo carga como un objeto Python utilizando la función `loads` del módulo `json`. Esto convierte los datos JSON en un diccionario de Python, que se almacena en la variable `astronautas`.

7. `print(astronautas)`: Imprime en la consola el diccionario de Python que contiene la información sobre los astronautas en la ISS.

El diccionario astronautas, como puedes ver, tiene tres claves: message, people y number.

La clave message tiene el valor success lo que nos está diciendo que accedimos correctamente a la información del servicio web. Ten en cuenta que tu verás diferentes valores para la clave people y number dependiendo de quien esté actualmente en el espacio.

Para que la información sea más clara, ignoraremos los nombres de los astronautas de momento y nos centraremos solo en su número. Cambia la función print por:

print(f"Número actual de astronautas: {astronautas['number']}")

Para que los nombres de los astronautas sean más legibles utilizaremos un bucle for para recorrer el diccionario.

""" ¿Donde está la Estación Espacial Internacional? """

import urllib.request
import json
import turtle

# http://open-notify.org/Open-Notify-API/
url = 'http://api.open-notify.org/astros.json'
respuesta = urllib.request.urlopen(url)
astronautas = json.loads(respuesta.read())

print(f"Número actual de astronautas: {astronautas['number']}")
for persona in astronautas['people']:
    print(persona['name'])
Si lo ejecutas verás una salida parecida a esta:

Número actual de astronautas: 7
Jasmin Moghbeli
Andreas Mogensen
Satoshi Furukawa
Konstantin Borisov
Oleg Kononenko
Nikolai Chub
Loral O'Hara

Además de los nombres de los astronautas, la API o servicio web también nos proporciona información sobre la nave en la que están. ¿Sabrías modificar el código para que se muestre esto?

Por ejemplo:

Número actual de astronautas: 7
Jasmin Moghbeli está en ISS
Andreas Mogensen está en ISS
Satoshi Furukawa está en ISS
Konstantin Borisov está en ISS
Oleg Kononenko está en ISS
Nikolai Chub está en ISS
Loral O'Hara está en ISS

¿Dónde está la ISS?


La Estación Espacial Internacional orbita alrededor de la tierra. Completa una orbita a la misma aproximadamente cada hora y media, ya que viaja a una velocidad aproximada de 7,66 km por segundo. Bastante rápido ¿Verdad?

Vamos a volver a usar la API para saber las cordenadas de la ISS. Lo primero es abir el siguiente link en el navegador http://open-notify.org/Open-Notify-API/ISS-Location-Now/. Ahí encontraras la información que necesitas. 

Si ahora introduces esta dirección en el navegador http://api.open-notify.org/iss-now.json verás algo parecido a esto:

{
  "iss_position": {
    "longitude": "55.8408",
    "latitude": "4.2601"
  },
  "message": "success",
  "timestamp": 1711653120
}
El resultado contiene las coordenadas de la tierra sobre la que actualmente está la ISS. Recordemos:

- La latitud indica la posición sobre el eje norte-sur y puede tener valores entre 90 y -90 grados, estando el cero en el ecuador.
- La longitud indica la posición sobre el eje este-oeste y puede tomar valores entre 180 y -180 grados. El cero marca el primer meridiano que es el que atraviesa Greenwich en londres (UK)

Las coordenadas normalmente se dan como (latitud, longitud). En esta web https://www.latlong.net/ puedes encontrar las coordenadas de tu lugar de residencia o de cualquier otro. 

Necesitamos llamar al mismo servicio web desde Python para que nos facilite las coordenadas que necesitamos. Añade el siguiente código al programa, es muy parecido a lo que hicimos antes:

#...
url = 'http://api.open-notify.org/iss-now.json'
respuesta = urllib.request.urlopen(url)
iss_posicion = json.loads(respuesta.read())
print(iss_posicion)
Verás unos datos similares a estos:

{'iss_position': {'longitude': '67.5407', 'latitude': '-36.2401'}, 
'message': 'success', 
'timestamp': 1711659534}
Crearemos unas variables para guardar la latitud y la longitud de la nave. Reorganicemos el código, el programa será algo como esto:

""" ¿Donde está la Estación Espacial Internacional? """

import urllib.request
import json
import turtle

# http://open-notify.org/Open-Notify-API/
url = 'http://api.open-notify.org/astros.json'
respuesta = urllib.request.urlopen(url)
astronautas = json.loads(respuesta.read())

print(f"Número actual de astronautas: {astronautas['number']}")
for persona in astronautas['people']:
    print(f"{persona['name']} está en {persona['craft']}")
    
url = 'http://api.open-notify.org/iss-now.json'
respuesta = urllib.request.urlopen(url)
iss_posicion = json.loads(respuesta.read())
coordenadas = iss_posicion['iss_position']
lat = float(coordenadas['latitude'])
long = float(coordenadas['longitude'])
print(f"Latitud: {lat}, Longitud: {long}")

Posicionando la ISS en un mapa.


Sería útil mostrar su posición en un mapa. Podemos hacer esto fácilmente usando el módulo turtle. Lo primero que haremos será cargar una imagen de fondo facilitada por la NASA.

mapa del mundo


El mapa ya esta centrado en las coordenadas (0, 0) que es justo lo que necesitamos.

También tenemos que hacer coincidir el tamaño de la ventana con el tamaño de nuestra imagen que es de 720 x 360 pixels.

En definitivas cuentas el código que utilizaremos es el siguiente:

# image source:
# map.jpg: https://visibleearth.nasa.gov/view.php?id=57752 Credit: NASA
screen = turtle.Screen()
screen.setup(720, 360)
Por otra parte también necesitamos que sea posible enviar a la tortuga (el puntero del módulo turtle) a una determinada posición en el mapa. Para hacerlo lo más sencillo posible, haremos que coincidan las coordenadas con el tamaño del mapa de la siguiente forma, utilizando el método setworldcoordinates:

# map.jpg: https://visibleearth.nasa.gov/view.php?id=57752 Credit: NASA
screen = turtle.Screen()
screen.setup(720, 360)
screen.setworldcoordinates(-180, -90, 180, 90)
screen.bgpic('map.gif')
 Con esto la coordenada inferior (-90, -180) coincidirá con la esquina inferior izquierda del mapa y (90, 180) con la esquina superior derecha del mismo.

Para que el puntero no sea una tortuga, parece más lógico que usemos otro icono que se parezca más a la ISS. Para ello la imagen tiene que ser pequeña, para que no ocupe una gran parte de la pantalla.  Un tamaño de 50 * 50 pixeles ya es bastante grande. 

Si por ejemplo tenemos el icono en un archivo llamado "station.gif" lo primero que tenemos que hacer es registrar la imagen con screen

screen = turtle.Screen()
screen.register_shape('station.gif')

Luego lo establecemos con shape.

turtle.shape('station.gif')

Normalmente el puntero de la tortuga mira hacia la derecha, pero podemos hacer que empiece mirando hacia arriba con:

turtle.setheading(90) 

La tortuga empieza siempre en el centro de la pantalla, con lo cual tenemos que moverlo a las coordenadas correctas, las que nos proporciona la longitud y latitud dada. Tendremos que levantar el lápiz o el puntero de la tortuga para que no se marque el trazo y luego llevarla hasta el punto final:

iss.penup()
iss.goto(long, lat)


Con todo esto nuestro código quedaría de la siguiente forma:

""" ¿Donde está la Estación Espacial Internacional? """

import urllib.request
import json
import turtle

# http://open-notify.org/Open-Notify-API/
url = 'http://api.open-notify.org/astros.json'
respuesta = urllib.request.urlopen(url)
astronautas = json.loads(respuesta.read())

print(f"Número actual de astronautas: {astronautas['number']}")
for persona in astronautas['people']:
    print(f"{persona['name']} está en {persona['craft']}")
    
url = 'http://api.open-notify.org/iss-now.json'
respuesta = urllib.request.urlopen(url)
iss_posicion = json.loads(respuesta.read())
coordenadas = iss_posicion['iss_position']
lat = float(coordenadas['latitude'])
long = float(coordenadas['longitude'])
print(f"Latitud: {lat}, Longitud: {long}")
    
# map.jpg: https://visibleearth.nasa.gov/view.php?id=57752 Credit: NASA
screen = turtle.Screen()
screen.setup(720, 360)
screen.setworldcoordinates(-180, -90, 180, 90)
screen.bgpic('map.gif')

screen.register_shape('station.gif')
iss = turtle.Turtle()
iss.shape('station.gif')
# iss.setheading(90) La tortuga mira a la derecha por defecto.

iss.penup()
iss.goto(long, lat)
NOTA: La latitud normalmente se pone en primer lugar, pero nosotros necesitamos hacerlo al revés ya que las coordenadas se expresan en los ejes (x, y)

Prueba el programa. La ISS debería aparecer en su actual situación sobre la tierra.


posición actual de la ISS


Espera unos pocos segundos y vuelve a ejecutarlo para ver como la ISS se desplaza.

Añadiendo datos al mapa.


Ahora que hemos obtenido los datos que necesitábamos y hemos dibujado la posición de la ISS, podemos añadir algo de información al mapa. 

Empezaremos creando una nueva tortuga para escribir algo de texto.

num_personas = turtle.Turtle()


La nueva tortuga no debería dibujar líneas mientras está en la pantalla por lo que debemos ocultarla.

num_personas = turtle.Turtle()
num_personas.penup()
num_personas.hideturtle()

Escoge un color para el texto que quieras escribir y también la posición en el mapa donde quieras que se escriba.

# color del texto
num_personas.color('yellow')
# Ir al punto en el mapa
num_personas.goto(-175,-25)

Una vez escogido el color y la posición vamos a escribir el texto que queramos en el mapa. En este caso el texto informará al usuario del número de astronautas en el espacio.

num_personas.write(f"Nº Astronautas: {astronautas['number']}")

Puedes escribir el texto que tu quieras.

Al final del proyecto quedaría una imagen como esta:

imagen final del proyecto



Escribiendo texto con turtle.


Como has visto podemos usar turtle para escribir texto.

turtle.write('¡Hola!')

También podemos elegir el color del mismo.

turtle.color('deep pink')
turtle.write('¡Hola!')

Y también podemos cambiar la fuente y la alineación del mismo.

estilo = ('Courier', 30, 'italic')
turtle.write('Hello!', font=estilo, align='center')

La fuente es una tupla que contiene:
  • El nombre de la fuente: "Arial", "Courier" o "Times New Roman".
  • El tamaño de la fuente en pixeles.
  • El tipo de fuente: "normal", "bold" o"italic".
La alineación controla como se posiciona el texto basándose en la posición de la tortuga. La alineación puede tener tres opciones: "left", "center" o "right".

La ISS en movimiento.


Vamos a hacer una pequeña variación del código anterior para que la ISS actualice su posición en el mapa cada 5 segundos y además vaya dejando una traza en color amarillo de los lugares por donde ha pasado. Algo como esto:

trazado de la ruta de la ISS



Haremos solamente unos pequeños cambios. En vez de la librería urllib.request que viene de forma predeterminada con Python usaremos la librería requests.

Para ello comenzaremos instalando la librería.

python install requests

Capturando datos de páginas web con requests.

Algunas veces necesitaremos capturar datos desde una página web. Estos pueden ser datos proporcionados por una web API o quizás código HTML de la página. En cualquier caso, el módulo de Python requests hace que esta tarea sea muy fácil.

Veamos un poco como funciona. Como siempre, lo primero es importar el módulo.

import requests

Crearemos una variable que guarde la URL de la cual queremos capturar los datos. Para este ejemplo vamos a capturar la página de Inicio de Rapsberry pi org.

URL = "https://www.raspberrypi.org"

A continuación, le diremos al programa que capture la página web y la guarde en una variable.

r = requests.get(URL)

Esta variable "r" contiene un objeto con una gran cantidad de información. Puedes examinarla usando el shell. Por ejemplo:

>>> r
<RESPONSE [200]>
Este es un código de respuesta HTTP.  En este caso el código 200 es que todo ha ido bien. puedes obtener más información sobre los diferentes códigos de respuesta aquí.

Para obtener el contenido actual de la página, puedes usar r.text. Si quieres guardar el contenido puedes hacer algo como esto:

import requests
url = "https://www.raspberrypi.org"
r = requests.get(url)
data = r.text

Con frecuencia, los datos que obtenemos de una página web suelen estar en formato json(). De forma muy sencilla podemos convertirlos en diccionarios de Python usando el método json().

import requests
url = "https://www.raspberrypi.org"
r = requests.get(url)
data = r.json()


Ahora que hemos visto como funciona el módulo requests, vamos a modificar el código del programa para lograr que refleje el movimiento de la ISS:

"""¿Donde está la Estación Espacial Internacional?."""

import requests
import turtle
import time

# http://open-notify.org/Open-Notify-API/
URL = 'http://api.open-notify.org/astros.json'
with requests.get(URL) as respuesta:
    print(respuesta)
    print(respuesta.text) # en formato json
    astronautas = respuesta.json() # en formato diccionario
    print(astronautas)

print(f"Número actual de astronautas: {astronautas['number']}")
for persona in astronautas['people']:
    print(f"{persona['name']} está en {persona['craft']}")
    
# map.jpg: https://visibleearth.nasa.gov/view.php?id=57752 Credit: NASA
screen = turtle.Screen()
screen.setup(720, 360)
screen.setworldcoordinates(-180, -90, 180, 90)
screen.bgpic('map.gif')

screen.register_shape('station.gif')
iss = turtle.Turtle()
iss.shape('station.gif')
# iss.setheading(90) La tortuga mira a la derecha por defecto
iss.penup()
screen.colormode(255)
iss.pencolor(255,255,0)

while True:
    URL = 'http://api.open-notify.org/iss-now.json'
    with requests.get(URL) as respuesta:
        iss_posicion = respuesta.json()
    coordenadas = iss_posicion['iss_position']
    lat = float(coordenadas['latitude'])
    long = float(coordenadas['longitude'])
    print(f"Latitud: {lat}, Longitud: {long}")    
    iss.goto(long, lat)
    iss.pendown()
    time.sleep(5)


Capturando los datos de vuelo de la ISS.


He modificado un poco el programa para que guarde los datos de latitud y longitud de la ISS mientras da una vuelta completa a la tierra. Para analizarlos he guardado los datos en un archivo CSV.

¿Qué es un archivo CSV?

Un archivo CSV contiene líneas de datos separados por comas. Puede también haber una cabecerá que describa de que datos se tratan. Este tipo de datos son ampliamente utilizados para guardar tablas de datos. Para leerlos y analizarlos puedes usar Python o también cualquier hoja de datos.

El código que puedes usar para guardar los datos es el siguiente:

"""¿Donde está la Estación Espacial Internacional?."""

import requests
import turtle
import time
import csv

# http://open-notify.org/Open-Notify-API/
URL = 'http://api.open-notify.org/astros.json'
with requests.get(URL) as respuesta:
    print(respuesta)
    print(respuesta.text) # en formato json
    astronautas = respuesta.json() # en formato diccionario
    print(astronautas)

print(f"Número actual de astronautas: {astronautas['number']}")
for persona in astronautas['people']:
    print(f"{persona['name']} está en {persona['craft']}")
    
# map.jpg: https://visibleearth.nasa.gov/view.php?id=57752 Credit: NASA
screen = turtle.Screen()
screen.setup(720, 360)
screen.setworldcoordinates(-180, -90, 180, 90)
screen.bgpic('map.gif')

screen.register_shape('station.gif')
iss = turtle.Turtle()
iss.shape('station.gif')
# iss.setheading(90) La tortuga mira a la derecha por defecto
iss.penup()
screen.colormode(255)
iss.pencolor(255,255,0)

# contador para guardar la long y latitud
contador = 0

while True:
    contador += 1
    URL = 'http://api.open-notify.org/iss-now.json'
    with requests.get(URL) as respuesta:
        iss_posicion = respuesta.json()
    coordenadas = iss_posicion['iss_position']
    lat = float(coordenadas['latitude'])
    long = float(coordenadas['longitude'])
    print(f"Latitud: {lat}, Longitud: {long}")    
    iss.goto(long, lat)
    iss.pendown()
    # Escribimos los datos en un archivo csv
    with open('ejemplo.csv', 'a') as file:
        datos_a_escribir = csv.writer(file)
        datos_a_escribir.writerow([lat, long])
    time.sleep(5)
    # La ISS tarda aproximadamente 91 minutos en dar
    # una vuelta a la tiera. 91*12 = 1092
    # Si los datos se toman cada 5 segundos hay que guardar
    # 60 : 5 = 12 veces cada minuto.
    if contador == 1092:
        break
El comienzo del archivo ejemplo.csv mostrará algo como esto:

-51.1014,142.0155
-51.0416,142.5997
-50.9841,143.1338
-50.9295,143.6179
-50.8669,144.1493
-50.8017,144.679
-50.7339,145.2071
-50.6635,145.7336
-50.5973,146.2108
...
Tenía la curiosidad de comprobar con estos datos si podemos encontrar una aproximación a la velocidad que lleva la ISS que según la WIKIPEDIA es de unos 7,60 Km/s.

Buscando en internet aparece que para calcular la velocidad de un satélite basándonos en los datos de la latitud y longitud junto con el tiempo transcurrido entre cada medición (5 segundos) tenemos que utilizar la fórmula de la velocidad promedio. Esta se define como el cambio de posición dividido por el tiempo transcurrido. 

Dado que tenemos los datos de la latitud y longitud cada 5 segundos, podemos calcular la distancia entre dos puntos consecutivos utilizando la fórmula de la distancia entre dos puntos de la superficie de una esfera (ya que más o menos podemos considerar la tierra como una esfera). Esta formula se conoce como la fórmula de Haversine. 

Luego, podemos calcular la velocidad promedio entre dos puntos consecutivos dividiendo la distancia entre ellos por el tiempo transcurrido. Sumando todas las velocidades promedio y dividiendo por el número total de intervalos de tiempo, obtendremos una velocidad promedio general. 

Aquí tienes el código de como podríamos implementar esto en Python:

import csv
from math import radians, sin, cos, sqrt, atan2

# Función para calcular la distancia entre dos puntos en la superficie de la Tierra
def calcular_distancia(lat1, lon1, lat2, lon2):
    # Radio de la Tierra en kilómetros
    radio_tierra = 6371.0

    # Convertir coordenadas de grados a radianes
    lat1 = radians(lat1)
    lon1 = radians(lon1)
    lat2 = radians(lat2)
    lon2 = radians(lon2)

    # Diferencia de latitud y longitud
    dlat = lat2 - lat1
    dlon = lon2 - lon1

    # Fórmula de Haversine
    a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
    c = 2 * atan2(sqrt(a), sqrt(1 - a))

    # Distancia entre los dos puntos
    distancia = radio_tierra * c

    return distancia

# Coordenadas y tiempo
with open('ejemplo.csv', 'r') as file:
    datos = csv.reader(file)
    coordenadas = []
    for dato in datos:
        dato_x, dato_y = dato
        # convertimos las cadenas en numeros
        dato_x = float(dato_x)
        dato_y = float(dato_y)
        coordenadas.append((dato_x,dato_y))
        
# coordenadas = [
#     (-40.2785, 56.2131),
#     (-40.4843, 56.5698),
#     # Insertamos nuestras coordenadas reales de esta forma.
# ]

tiempo_por_punto = 5  # segundos

# Calcular la velocidad promedio
distancias = []
for i in range(1, len(coordenadas)):
    lat1, lon1 = coordenadas[i - 1]
    lat2, lon2 = coordenadas[i]
    distancia = calcular_distancia(lat1, lon1, lat2, lon2)
    distancias.append(distancia)

# Calcular la velocidad promedio
velocidades = [distancia / tiempo_por_punto for distancia in distancias]
velocidad_promedio = sum(velocidades) / len(velocidades)

print("La velocidad promedio del satélite es:", velocidad_promedio, "km/s")

SALIDA:

La velocidad promedio del satélite es: 7.467342537150308 km/s

¡Bastante parecido a la velocidad real!

Puede encontrar el código de este proyecto en el siguiente enlace de Github.

domingo, 14 de enero de 2024

24.- Exportando los pedidos a archivos CSV. Personalización del panel de Administración y generación y envio de PDFs.

 A veces, es posible que desees exportar la información contenida en un modelo a un archivo para poder importarla en otro sistema. Uno de los formatos más ampliamente utilizados para exportar/importar datos es el de Valores Separados por Comas (CSV). Un archivo CSV es un archivo de texto plano que consta de varios registros. Normalmente, hay un registro por línea y algún carácter delimitador, generalmente una coma literal, que separa los campos del registro. Vamos a personalizar el sitio de administración para poder exportar pedidos a archivos CSV.


Añadiendo acciones personalizadas al sitio de administración 


Django ofrece una amplia gama de opciones para personalizar el sitio de administración. Vas a modificar la vista de lista de objetos para incluir una acción de administración personalizada. Puedes implementar acciones de administración personalizadas para permitir que los usuarios apliquen acciones a varios elementos a la vez en la vista de lista de cambios. 

Una acción de administración funciona de la siguiente manera: un usuario selecciona objetos de la página de lista de objetos de administración con casillas de verificación, luego selecciona una acción para realizar en todos los elementos seleccionados y ejecuta las acciones. La siguiente imagen muestra dónde se encuentran las acciones en el sitio de administración:


The drop-down menu for Django administration actions



Puedes crear una acción personalizada escribiendo una función regular que reciba los siguientes parámetros:

• El ModelAdmin actual que se está mostrando
• El objeto de solicitud actual como una instancia de HttpRequest
• Un QuerySet para los objetos seleccionados por el usuario

Esta función se ejecutará cuando se active la acción desde el sitio de administración.

Vamos a crear una acción personalizada de administración para descargar una lista de pedidos como un archivo CSV.

Edita el archivo admin.py de la aplicación de pedidos y agrega el siguiente código antes de la clase OrderAdmin:

PracticaDjango/Orders/admin.py

# Para exportar los pedidos como archivos csv
import csv
import datetime
from django.http import HttpResponse

def export_to_csv(modeladmin, request, queryset):
    opts = modeladmin.model._meta
    content_disposition = f'attachment; filename={opts.verbose_name}.csv'
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = content_disposition
    writer = csv.writer(response)
    fields = [field for field in opts.get_fields() if not \
              field.many_to_many and not field.one_to_many]
    # Escribe la primera línea con información de la cabecera
    writer.writerow([field.verbose_name for field in fields])
    # Escribe las filas con los datos
    for obj in queryset:
        data_row = []
        for field in fields:
            value = getattr(obj, field.name)
            if isinstance(value, datetime.datetime):
                value = value.strftime('%d/%m/%Y')
            data_row.append(value)
        writer.writerow(data_row)
    return response
export_to_csv.short_description = 'Exportar a CSV'


En este código, realizas las siguientes tareas:

1. Creas una instancia de HttpResponse, especificando el tipo de contenido text/csv, para indicar al navegador que la respuesta debe tratarse como un archivo CSV. También agregas una cabecera Content-Disposition para indicar que la respuesta HTTP contiene un archivo adjunto.

2. Creas un objeto escritor CSV que escribirá en el objeto de respuesta.

3. Obtienes dinámicamente los campos del modelo utilizando el método get_fields() de las opciones _meta del modelo. Excluyes las relaciones muchos a muchos y uno a muchos.

4. Escribe una fila de encabezado que incluye los nombres de los campos.

5. Iteras sobre el conjunto de consultas dado y escribes una fila para cada objeto devuelto por el conjunto de consultas. Te ocupas del formato de objetos datetime porque el valor de salida para CSV debe ser una cadena o string.

6. Personalizas el nombre de visualización para la acción en el elemento desplegable de acciones del sitio de administración estableciendo un atributo short_description en la función.

Has creado una acción de administración genérica que se puede agregar a cualquier clase ModelAdmin.

Finalmente, añade la nueva acción de administración export_to_csv a la clase OrderAdmin, de la siguiente manera. El nuevo código está resaltado en azul:

PracticaDjango/Orders/admin.py

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

Abre la siguiente dirección en tu navegador http://127.0.0.1:8000/admin/Orders/order/. Si has introducido pedidos, deberías ver algo como esto:

Using the custom Export to CSV administration action

Selecciona algunos pedidos y haz clic en "Exportar a CSV" en el menú que pone "acción" en la parte superior, y haz clic en el botón "Ir". Tu navegador descargará el archivo CSV que hemos generado con el nombre de order.csv. Abre este archivo usando un editor de texto. Verás algo parecido a esto:

archivo css

Como puedes ver, crear acciones personalizadas en el panel de administración es bastante sencillo. Puedes obtener más información sobre la generación de archivos CSV con Django en https://docs.djangoproject.com/en/5.0/howto/outputting-csv/.

A continuación, personalizaremos aún más el panel de administración mediante la creación de una vista personalizada.


Ampliando el sitio de administración con vistas personalizadas

En ocasiones, es posible que desees personalizar el sitio de administración más allá de lo que es posible mediante la configuración de ModelAdmin, la creación de acciones personalizadas de administración y la anulación de plantillas de administración. Puede que desees implementar funcionalidades adicionales que no estén disponibles en las vistas o plantillas de administración existentes. Si este es el caso, necesitas crear una vista de administración personalizada. Con una vista personalizada, puedes construir cualquier funcionalidad que desees; solo asegúrate de que solo los usuarios con privilegios de staff puedan acceder a tu vista y de que mantengas el aspecto y la sensación de administración al hacer que tu plantilla extienda una plantilla de administración.

Vamos a crear una vista personalizada para mostrar información sobre un pedido. Edita el archivo views.py de la aplicación de pedidos y agrega el siguiente código resaltado en azul:

PracticaDjango/Orders/views.py

#...
# Para crear una vista personalizada en el panel de administración.
from django.shortcuts import get_object_or_404
from django.contrib.admin.views.decorators import staff_member_required
from .models import Order

def order_create(request):
    #...

@staff_member_required
def admin_order_detail(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    return render(request, 'admin/Orders/order/detail.html', {'order': order})

El decorador `staff_member_required` verifica que tanto los campos `is_active` como `is_staff` del usuario que solicita la página estén configurados como Verdaderos. En esta vista, obtienes el objeto Order con el ID proporcionado y renderizas una plantilla para mostrar la orden.

A continuación, edita el archivo urls.py de la aplicación de órdenes y agrega el siguiente patrón de URL resaltado en azul:

PracticaDjango/Orders/urls.py

from django.urls import path
from . import views

app_name = 'orders'

urlpatterns = [
path('create/', views.order_create, name='order_create'),
path('admin/Order/<int:order_id>/', views.admin_order_detail,
name='admin_order_detail'),
]
Crea la siguiente estructura de directorios dentro de las plantillas (templates) de la aplicación de pedidos:

admin/

    Order/

        order/

            detail.html

Edita la plantilla "detail.html" que hemos creado y añade el siguiente código:

PracticaDjango/Orders/templates/admin/Orders/order/detail.html

<html>

<body>
    <h1>Unikgame</h1>
    <p>
        Factura no. {{ order.id }}<br>
        <span class="secondary">
            {{ order.created|date:"M d, Y" }}
        </span>
    </p>
    <h3>Facturar a:</h3>
    <p>
        {{ order.first_name }} {{ order.last_name }}<br>
        {{ order.email }}<br>
        {{ order.address }}<br>
        {{ order.postal_code }}, {{ order.city }}
    </p>
    <h3>Elementos Comprados</h3>
    <table>
        <thead>
            <tr>
                <th>Producto</th>
                <th>Precio</th>
                <th>Cantidad</th>
                <th>Coste</th>
            </tr>
        </thead>
        <tbody>
            {% for item in order.items.all %}
            <tr class="row{% cycle '1' '2' %}">
                <td>{{ item.product.name }}</td>
                <td class="num">{{ item.price }} €</td>
                <td class="num">{{ item.quantity }}</td>
                <td class="num">{{ item.get_cost }} €</td>
            </tr>
            {% endfor %}
            <tr class="total">
                <td colspan="3">Total</td>
                <td class="num">{{ order.get_total_cost }} €</td>
            </tr>
        </tbody>
    </table>
    <span class="{% if order.paid %}paid{% else %}pending{% endif %}">
        {% if order.paid %}Pagado{% else %}Pendiente de Pago{% endif %}
    </span>
</body>

</html>

Asegúrate de que ninguna etiqueta de plantilla esté dividida en varias líneas.

Este es el modelo para mostrar los detalles de un pedido en el sitio de administración. Este modelo extiende el modelo admin/base_site.html del sitio de administración de Django, que contiene la estructura principal de HTML y estilos CSS. Utilizas los bloques definidos en el modelo padre para incluir tu propio contenido. Muestras información sobre el pedido y los artículos comprados.

Cuando deseas extender un modelo de administración, necesitas conocer su estructura e identificar los bloques existentes. Puedes encontrar todos los modelos de administración en https://github.com/django/django/tree/5.0/django/contrib/admin/templates/admin.

También puedes anular un modelo de administración si es necesario. Para hacerlo, copia un modelo en tu directorio de plantillas (templates/), manteniendo la misma ruta y nombre de archivo. El sitio de administración de Django utilizará tu modelo personalizado en lugar del predeterminado.

Finalmente, agreguemos un enlace a cada objeto Order en la página de visualización de la lista del sitio de administración. Edita el archivo admin.py de la aplicación de pedidos y agrega el siguiente código, encima de la clase OrderAdmin:

PracticaBlog/PracticaDjango/Orders/admin.py

#...
from django.urls import reverse

#...

def order_detail(obj):
    url = reverse('orders:admin_order_detail', args=[obj.id])
    return mark_safe(f'<a href="{url}">View</a>')


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    #...

Esta es una función que toma un objeto Order como argumento y devuelve un enlace HTML para la URL admin_order_detail. Django escapa la salida HTML de forma predeterminada. Debes utilizar la función mark_safe para evitar el autoescape.

Luego, edita la clase OrderAdmin para mostrar el enlace de la siguiente manera. El código nuevo está resaltado en azul:

PracticaBlog/PracticaDjango/Orders/admin.py

#...
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email',
                    'address', 'postal_code', 'city', 'paid', order_payment,
                    'created', 'updated', order_detail]
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]
    actions = [export_to_csv]
Para finalizar ejecuta el servidor de desarrollo con :
env()$ python manage.py runserver

Abre la dirección http://127.0.0.1:8000/admin/Orders/order/ en tu navegador. Cada fila de datos incluye un link view a la derecha del todo, como se puede apreciar en al siguiente imagen:


nueva opción view en las vistas.

Después de haber visto esto vamos a continuación a ver como podemos generar facturas dinámicas en PDF.


Generando facturas en PDF dinámicamente


Ahora que tienes un sistema completo de pago y compra, puedes generar una factura en PDF para cada pedido. Existen varias bibliotecas de Python para generar archivos PDF. Una biblioteca popular para generar PDFs con código Python es ReportLab. Puedes encontrar información sobre cómo generar archivos PDF con ReportLab en https://docs.djangoproject.com/en/5.0/howto/outputting-pdf/.

En la mayoría de los casos, deberás agregar estilos y formato personalizados a tus archivos PDF. Encontrarás más sencillo renderizar una plantilla HTML y convertirla en un archivo PDF, manteniendo a Python alejado de la capa de presentación. Vas a seguir este enfoque y utilizar un módulo para generar archivos PDF con Django. Utilizaremos WeasyPrint, que es una biblioteca de Python que puede generar archivos PDF a partir de plantillas HTML.


Instalando WeasyPrint

Primero, instala las dependencias de WeasyPrint para tu sistema operativo desde https://doc.courtbouillon.org/weasyprint/stable/first_steps.html. Luego, instala WeasyPrint a través de pip utilizando el siguiente comando:

pip install weasyprint


Creando la plantilla para el PDF.


Tenemos que crear una plantilla HTML que servirá de base para el PDF. Crearemos la plantilla, la renderizaremos usando Django y la pasaremos a WeasyPrint para generar el PDF. Crearemos una nueva plantilla dentro de la aplicación de pedidos en templates/Orders/order/ y la llamaremos pdf.html. Añádele el siguiente código:

PracticaDjango/Orders/templates/Orders/order/pdf.html

<html>

<body>
    <h1>Unikgame</h1>
    <p>
        Factura no. {{ order.id }}<br>
        <span class="secondary">
            {{ order.created|date:"M d, Y" }}
        </span>
    </p>
    <h3>Facturar a:</h3>
    <p>
        {{ order.first_name }} {{ order.last_name }}<br>
        {{ order.email }}<br>
        {{ order.address }}<br>
        {{ order.postal_code }}, {{ order.city }}
    </p>
    <h3>Elementos Comprados</h3>
    <table>
        <thead>
            <tr>
                <th>Producto</th>
                <th>Precio</th>
                <th>Cantidad</th>
                <th>Coste</th>
            </tr>
        </thead>
        <tbody>
            {% for item in order.items.all %}
            <tr class="row{% cycle '1' '2' %}">
                <td>{{ item.product.name }}</td>
                <td class="num">${{ item.price }}</td>
                <td class="num">{{ item.quantity }}</td>
                <td class="num">${{ item.get_cost }}</td>
            </tr>
            {% endfor %}
            <tr class="total">
                <td colspan="3">Total</td>
                <td class="num">{{ order.get_total_cost }} €</td>
            </tr>
        </tbody>
    </table>
    <span class="{% if order.paid %}pagado{% else %}pending{% endif %}">
        {% if order.paid %}Paid{% else %}Pendiente de Pago{% endif %}
    </span>
</body>

</html>    


Esta es la plantilla de la factura que usaremos de base para crear el PDF. En ella mostraremos todos los detalles de la factura, así como un mensaje de si está pagada o no.


Renderizando archivos PDF.


Vamos a crear la vista para renderizar los archivos PDF para los pedidos existentes y usaremos para ello el panel de administración de Django. Edita el archivo views.py de la aplicación de pedidos y añade el siguiente código.

PracticaBlog/PracticaDjango/Orders/views.py

#...
# Para renderizar el archivo PDF de las facturas.
from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
import weasyprint

# Create your views here.


@staff_member_required
def admin_order_pdf(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    html = render_to_string('Orders/order/pdf.html', {'order': order})
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename=order_{order.id}.pdf'
    weasyprint.HTML(string=html).write_pdf(response, stylesheets=[
        weasyprint.CSS(settings.STATIC_ROOT / 'Proyecto_web_app/css/pdf.css')])
    return response


Esta es la vista para generar una factura en formato PDF para un pedido. Se utiliza el decorador staff_member_required para asegurarse de que solo los usuarios del personal tengan acceso a esta vista. Se obtiene el objeto Order con el ID proporcionado y se utiliza la función render_to_string() proporcionada por Django para renderizar Orders/order/pdf.html. El HTML resultante se guarda en la variable html. Luego, se genera un nuevo objeto HttpResponse especificando el tipo de contenido application/pdf e incluyendo la cabecera Content-Disposition para especificar el nombre de archivo. Se utiliza WeasyPrint para generar un archivo PDF a partir del código HTML renderizado y se escribe el archivo en el objeto HttpResponse.

Se utiliza el archivo de estilo CSS pdf.css ubicado en archivos estáticos para agregar estilos CSS al archivo PDF generado. Luego, se carga desde la ruta local utilizando la configuración STATIC_ROOT. Finalmente, se devuelve la respuesta generada.

Dado que necesitas usar la configuración de STATIC_ROOT, debes agregarla a tu proyecto. Esta es la ruta del proyecto donde residen los archivos estáticos. Edita el archivo settings.py del proyecto PracticaDjango y agrega la siguiente configuración:

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'static'

Después ejecuta el siguiente comando:

python manage.py collectstatic

La salida del comando debería ser parecida a esto:

 142 static files copied to '/home/chema/PycharmProjects/PracticaBlog/PracticaDjango/static'.


El comando collectstatic copia todos los archivos estáticos de tus aplicaciones al directorio definido en la configuración STATIC_ROOT. Esto permite que cada aplicación proporcione sus propios archivos estáticos utilizando un directorio static/ que los contenga. También puedes proporcionar fuentes adicionales de archivos estáticos en la configuración STATICFILES_DIRS. Todos los directorios especificados en la lista STATICFILES_DIRS también se copiarán al directorio STATIC_ROOT cuando se ejecute collectstatic. Cada vez que ejecutas collectstatic nuevamente, se te preguntará si deseas sobrescribir los archivos estáticos existentes.

Edita el archivo urls.py dentro del directorio de la aplicación Orders y agrega el siguiente patrón de URL resaltado en azul:

PracticaBlog/PracticaDjango/Orders/urls.py

from django.urls import path
from . import views

app_name = 'orders'

urlpatterns = [
    path('create/', views.order_create, name='order_create'),
    path('admin/Order/<int:order_id>/', views.admin_order_detail,
         name='admin_order_detail'),
    path('admin/Order/<int:order_id>/pdf/',
         views.admin_order_pdf, name='admin_order_pdf'),
]

Ahora puedes editar la página de visualización de la lista de administración para el modelo Order para agregar un enlace al archivo PDF para cada resultado. Edita el archivo admin.py dentro de la aplicación Orders y agrega el siguiente código encima de la clase OrderAdmin:


PracticaBlog/PracticaDjango/Orders/admin.py

def order_pdf(obj):
    url = reverse('orders:admin_order_pdf', args=[obj.id])
    return mark_safe(f'<a href="{url}">PDF</a>')
order_pdf.short_description = 'Factura'

Si especificas un atributo short_description para tu función, Django lo utilizará como nombre de la columna.

Añade order_pdf al atributo list_display de la clase OrderAdmin, de la siguiente manera:

PracticaBlog/PracticaDjango/Orders/admin.py

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

Asegúrate de que el servidor de desarrollo esté en ejecución. Abre http://127.0.0.1:8000/admin/Orders/order/ en tu navegador. Ahora, cada fila debería incluir un enlace PDF, similar a esto:enlace a pdfs



Haz clic en el enlace PDF para cualquier pedido. Deberías ver un archivo PDF generado, similar al siguiente:

fra generada en pdf



Enviando el archivo PDF de la factura por email.


Lo que vamos a hacer ahora es preparar el programa para que cuando se realice un pago exitosamente se envíe un email al cliente con la factura que hemos generado en PDF. Lo haremos a través de una tarea asincrónica. 

Crea un nuevo archivo en la aplicación de pago y llámalo task.py. Añade el siguiente código:

PracticaBlog/PracticaDjango/Payment/tasks.py

from io import BytesIO
from celery import shared_task
import weasyprint
from django.template.loader import render_to_string
from django.core.mail import EmailMessage
from django.conf import settings
from Orders.models import Order


@shared_task
def payment_completed(order_id):
    """
    Tarea que envia una notificación por email cuando el 
    pedido ha sido pagado con exito.
    """
    order = Order.objects.get(id=order_id)
    # crea la factura por email
    subject = f'Unikgame - Factura no. {order.id}'
    message = 'Te enviamos la factura de tu compra en el archivo adjunto.'
    email = EmailMessage(subject,
                         message,
                         'admin@unikgame.com',
                         [order.email])
    # genera el PDF
    html = render_to_string('Orders/order/pdf.html', {'order': order})
    out = BytesIO()
    stylesheets = [weasyprint.CSS(
        settings.STATIC_ROOT / 'Proyecto_web_app/css/pdf.css')]
    weasyprint.HTML(string=html).write_pdf(out,
                                           stylesheets=stylesheets)
    # adjunta el archivo PDF a la notificación
    email.attach(f'order_{order.id}.pdf',
                 out.getvalue(),
                 'application/pdf')
    # send e-mail
    email.send()
Definimos la tarea payment_completed utilizando el decorador @shared_task. En esta tarea, utilizas la clase EmailMessage proporcionada por Django para crear un objeto de correo electrónico. Luego, renderizas la plantilla en la variable html. Generas el archivo PDF a partir de la plantilla renderizada y lo envías a una instancia de BytesIO, que es un búfer de bytes en memoria. Después, adjuntas el archivo PDF generado al objeto EmailMessage utilizando el método attach(), incluyendo el contenido del búfer de salida. Finalmente, envías el correo electrónico. 

Recuerda configurar tus ajustes del Protocolo Simple de Transferencia de Correo (SMTP) en el archivo settings.py del proyecto para enviar correos electrónicos. Si no deseas configurar los ajustes de correo electrónico, puedes indicarle a Django que escriba los correos electrónicos en la consola agregando la siguiente configuración al archivo settings.py

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'


Añadamos la tarea `payment_completed` al punto final del webhook que maneja los eventos de finalización de pagos. Edita el archivo `webhooks.py` de la aplicación de pagos y modifícalo para que se vea de la siguiente manera:

PracticaBlog/PracticaDjango/Payment/tasks.py

import stripe
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from Orders.models import Order
from .tasks import payment_completed

#...
            order.paid = True
            # store Stripe payment ID
            order.stripe_id = session.payment_intent
            order.save()
            # lanza la tarea de forma asincrona para enviar un correo al usuario
            payment_completed.delay(order.id)
            
    return HttpResponse(status=200)


La tarea `payment_completed` se manda a la cola al llamar a su método 'delay()'. La tarea se agregará a la cola y se ejecutará de forma asíncrona por un trabajador de Celery tan pronto como sea posible.

Ahora puedes completar un nuevo proceso de pago para recibir la factura en formato PDF en tu correo electrónico. Si estás utilizando 'console.EmailBackend' como tu servicio de correo electrónico, en la consola donde estás ejecutando Celery, podrás ver la siguiente salida:

Message-ID: <170525972355.5337.6997908321665427625@machine>

--===============2428656861460200554==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

Te enviamos la factura de tu compra en el archivo adjunto.
--===============2428656861460200554==
Content-Type: application/pdf
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="order_34.pdf"

JVBERi0xLjcKJfCflqQKNSAwIG9iago8PC9GaWx0ZXIgL0ZsYXRlRGVjb2RlL0xlbmd0aCA5Nzc+

Y con esto finalizamos este capítulo. 

Puedes encontrar el código del mismo en este enlace de GITHUB.