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:
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:
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:
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'), ]
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]
env()$ python manage.py runserver
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:
Haz clic en el enlace PDF para cualquier pedido. Deberías ver un archivo PDF generado, similar al siguiente:
Enviando el archivo PDF de la factura por email.
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()
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.
No hay comentarios:
Publicar un comentario