Saltar a contenido

Extras

Django avanzado

Existe un ecosistema enorme de paquetes de terceros que ofrecen funcionalidades extras a Django. En esta sección veremos algunos de los más interesantes.

INSTALLED_APPS

La mayoría de paquetes requiere añadir sus aplicaciones a settings.py. Es por ello que se recomienda seguir una mínima estructura similar a la siguiente:

main/settings.py
INSTALLED_APPS = [
    # DJANGO APPS
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    ...

    # THIRD PARTY APPS
    'django_browser_reload',
    'django_rq',
    ...

    # CUSTOM APPS
    'posts.apps.PostsConfig',
    'accounts.apps.AccountsConfig',
    ...
]

Django Reload

django-browser-reload es un paquete Python que recarga la web del proyecto en el navegador cada vez que detecta un cambio en los ficheros de código, sin necesidad de hacerlo manualmente.

Instalación

La instalación del paquete es muy sencilla:

$ pip install django-browser-reload
$ uv add --dev django-browser-reload

Configuración

Para configurar django-browser-reload debemos añadir ciertas líneas a settings.py:

main/settings.py
INSTALLED_APPS = (
    # ...
    'django_browser_reload',
    # ...
)

MIDDLEWARE = [
    # ...
    'django_browser_reload.middleware.BrowserReloadMiddleware',
    # ...
]

También debemos añadir cierta configuración a las URLs de primer nivel:

main/urls.py
from django.urls import include, path


urlpatterns = [
    # ...
    path('__reload__/', include('django_browser_reload.urls')),
    # ...
]

Modo de uso

Una vez que lancemos el servidor de desarrollo ya estaremos en disposición de trabajar con nuestro proyecto y ver los cambios en el navegador con recarga automática cada vez que modifiquemos algún archivo.

Crispy Forms

django-crispy-forms es un paquete Python que proporciona utilidades para renderizar formularios de una manera elegante y reutilizable en Django.

Este paquete permite trabajar con distintos «frameworks» CSS. Uno de los más utilizados es Bootstrap. En esta sección veremos cómo manejar formularios e integrarlos con estas herramientas.

Instalación

Lo primero será integrar Bootstrap en nuestro proyecto.

Hecho esto y dado que vamos a trabajar con Bootstrap, podemos utilizar directamente el paquete crispy-bootstrap5 que, como su nombre indica, nos va a permitir usar Bootstrap v5 y que también nos instalará (como dependencia) el paquete django-crispy-forms.

La instalación del paquete es muy sencilla:

$ pip install crispy-bootstrap5
$ uv add crispy-bootstrap5

Configuración

Para configurar crispy-bootstrap5 debemos añadir ciertas líneas a settings.py:

main/settings.py
INSTALLED_APPS = (
    # ...
    'crispy_forms',
    'crispy_bootstrap5',
    # ...
)

CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5'
CRISPY_TEMPLATE_PACK = 'bootstrap5'

Modo de uso

La forma más simple de utilizar este paquete es mediante el filtro |crispy.

Si tomamos como ejemplo el formulario de modelo para añadir un «post» en un «blog», la plantilla nos quedaría de la siguiente manera:

posts/templates/posts/post/add.html
{% load crispy_forms_tags %}<!--(1)!-->

<form method="post">
    {{ form|crispy }}<!--(2)!-->
    <button type="submit" class="btn btn-primary">Add post</button>
</form>

  1. Cargamos el filtro «crispy».
  2. Renderizamos el formulario mediante crispy-forms.

Pero existe una aproximación más personalizable y es utilizar la etiqueta de plantilla {% crispy %}.

Como ejemplo de uso de esta etiqueta vamos a implementar formularios y plantillas para inicio de sesión y registro de usuario.

Login

Veamos la implementación del inicio de sesión:

accounts/forms.py
from crispy_bootstrap5.bootstrap5 import FloatingField
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
from django import forms


class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput)

    def __init__(self, *args, **kwargs):#(1)!
        super().__init__(*args, **kwargs)#(2)!
        self.helper = FormHelper()#(3)!
        self.helper.attrs = {'novalidate': True}#(4)!
        self.helper.layout = Layout(#(5)!
            FloatingField('username'),#(6)!
            FloatingField('password'),#(7)!
            Submit('login', 'Login', css_class='w-100 mt-2 mb-2'),#(8)!
        )

  1. Será necesario sobreescribir el constructor del formulario para definir las características del renderizado.
  2. No puede faltar la llamada al constructor de la clase base.
  3. La clase FormHelper define el comportamiento del renderizado del formulario en django-crispy-forms.
  4. Añadimos el atributo novalidate al formulario para indicar que no se valide desde HTML.
  5. Utilizamos la clase Layout que permite cambiar la forma en la que se renderizan los campos del formulario en django-crispy-forms.
  6. Incluimos en el «layout» el campo username del formulario como un FloatingField (presente en el paquete crispy-bootstrap5) que permite usar las nuevas etiquetas flotantes de Bootstrap.
  7. Incluimos en el «layout» el campo password del formulario como un FloatingField (presente en el paquete crispy-bootstrap5) que permite usar las nuevas etiquetas flotantes de Bootstrap.
    • Incluimos en el «layout» un botón para enviar el formulario utilizando Submit.
    • Es posible incluir clases CSS al control HTML mediante el parámetro css_class.

accounts/templates/accounts/login.html
{% extends "base.html" %}
{% load crispy_forms_tags %}<!--(1)!-->

{% block content %}
<div class="row justify-content-center mt-5">
    <div class="col-md-4">
        <div class="card border-dark">
        <h4 class="card-header">
            Login
        </h4>
        <div class="card-body">
            {% crispy form %}<!--(2)!-->
        </div>
        <div class="card-footer">
            Don't have an account? <a href="{% url 'signup' %}">Sign up</a> here.
        </div>
        </div>
    </div>
</div>
{% endblock %}

  1. Cargamos las utilidades para plantillas del paquete crispy-forms.
  2. Así de fácil se renderiza TODO el formulario

Registro

Veamos la implementación del inicio de sesión (con todos los campos requeridos):

accounts/forms.py
class SignupForm(forms.ModelForm):
    class Meta:
        model = get_user_model()
        fields = ('username', 'password', 'first_name', 'last_name', 'email')
        required = ('username', 'password', 'first_name', 'last_name', 'email')
        widgets = {'password': forms.PasswordInput}
        help_texts = {'username': None}

    def __init__(self, *args, **kwargs):#(1)!
        super().__init__(*args, **kwargs)#(2)!

        for field in self.Meta.required:
            self.fields[field].required = True

        self.helper = FormHelper()#(3)!
        self.helper.attrs = {'novalidate': True}#(4)!
        self.helper.layout = Layout(#(5)!
            FloatingField('username'),#(6)!
            FloatingField('password'),#(7)!
            FloatingField('first_name'),#(8)!
            FloatingField('last_name'),#(9)!
            FloatingField('email'),#(10)!
            Submit('signup', 'Sign up', css_class='btn-info w-100 mt-2 mb-2'),#(11)!
        )

    def save(self, *args, **kwargs):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data['password'])
        user = super().save(*args, **kwargs)
        return user

  1. Será necesario sobreescribir el constructor del formulario para definir las características del renderizado.
  2. No puede faltar la llamada al constructor de la clase base.
  3. La clase FormHelper define el comportamiento del renderizado del formulario en django-crispy-forms.
  4. Añadimos el atributo novalidate al formulario para indicar que no se valide desde HTML.
  5. Utilizamos la clase Layout que permite cambiar la forma en la que se renderizan los campos del formulario en django-crispy-forms.
  6. Incluimos en el «layout» el campo username del formulario como un FloatingField (presente en el paquete crispy-bootstrap5) que permite usar las nuevas etiquetas flotantes de Bootstrap.
  7. Incluimos en el «layout» el campo password del formulario como un FloatingField (presente en el paquete crispy-bootstrap5) que permite usar las nuevas etiquetas flotantes de Bootstrap.
  8. Incluimos en el «layout» el campo first_name del formulario como un FloatingField (presente en el paquete crispy-bootstrap5) que permite usar las nuevas etiquetas flotantes de Bootstrap.
  9. Incluimos en el «layout» el campo last_name del formulario como un FloatingField (presente en el paquete crispy-bootstrap5) que permite usar las nuevas etiquetas flotantes de Bootstrap.
  10. Incluimos en el «layout» el campo email del formulario como un FloatingField (presente en el paquete crispy-bootstrap5) que permite usar las nuevas etiquetas flotantes de Bootstrap.
    • Incluimos en el «layout» un botón para enviar el formulario utilizando Submit.
    • Es posible incluir clases CSS al control HTML mediante el parámetro css_class.

accounts/templates/accounts/signup.html
{% extends "base.html" %}
{% load crispy_forms_tags %}<!--(1)!-->

{% block content}
<div class="row justify-content-center mt-5">
    <div class="col-md-4">
        <div class="card border-dark">
        <h4 class="card-header">
            Sign up
        </h4>
        <div class="card-body">
            {% crispy form %}<!--(2)!-->
        </div>
        <div class="card-footer">
            Already have an account? <a href="{% url 'login' %}">Login</a> here.
        </div>
        </div>
    </div>
</div>
{% endblock %}

  1. Cargamos las utilidades para plantillas del paquete crispy-forms.
  2. Así de fácil se renderiza TODO el formulario

Campos de fichero

Cuando implementamos un formulario que incluye campos de fichero, crispy-forms lo renderiza mostrando el fichero actual asignado y un botón para «limpiar» el contenido del mismo (siempre que no sea obligatorio).

Para modificar este comportamiento y simplemente mostrar un control de selección de fichero, podemos modificar el «widget». Veamos un ejemplo con la imagen de «avatar» en un perfil de un usuario:

users/forms.py
class EditProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ['avatar', 'bio']
        widgets = {
            'avatar': forms.FileInput(attrs={'accept': 'image/*'}),
        }

Sorl Thumbnail

sorl-thumbnail es un paquete Python que se integra con Django y permite generar miniaturas («thumbnails») de imágenes de manera sencilla.

Instalación

La instalación del paquete es muy sencilla:

$ pip install sorl-thumbnail
$ uv add sorl-thumbnail

Configuración

Para configurar sorl-thumbnail debemos «instalar» la aplicación en settings.py:

main/settings.py
INSTALLED_APPS = (
    # ...
    'sorl.thumbnail',
    # ...
)

Cuidado con el nombre

Cuidado porque la cadena que debemos añadir a INSTALLED_APPS es 'sorl.thumbnail' (con punto en el medio).

Por último aplicamos las migraciones correspondientes a la aplicación:

$ ./manage.py migrate thumbnail
Operations to perform:
  Apply all migrations: thumbnail
Running migrations:
  Applying thumbnail.0001_initial... OK
$ uv run manage.py migrate thumbnail
Operations to perform:
  Apply all migrations: thumbnail
Running migrations:
  Applying thumbnail.0001_initial... OK

Esta migración creará una nueva tabla en la base de datos llamada thumbnail_kvstore donde se almacenarán las referencias a las miniaturas.

Modo de uso

Aunque existen múltiples casos de uso la forma más habitual de usar sorl-thumbnail es crear una miniatura en una plantilla.

Imaginemos por ejemplo que estamos desarrollando una aplicación tipo «blog» donde cada «post» dispone de una imagen de portada (atributo cover) que queremos mostrar en la plantilla pero en forma de miniatura:

posts/templates/posts/post/detail.html
{% load thumbnail %}<!--(1)!-->

<div class="post">
  <h1>{{ post.title }}</h1>
  {% thumbnail post.cover "200x200" crop="center" format="PNG" as thumb %}<!--(2)!-->
    <img src="{{ thumb.url }}" alt="Post cover"><!--(3)!-->
  {% endthumbnail %}<!--(4)!-->
  <p>{{ post.content }}</p>
</div>

  1. Cargamos las etiquetas/filtros del paquete sorl-thumbnail.
  2. Usamos la etiqueta {% thumbnail %} indicando lo siguiente:
    • La imagen a transformar es post.cover.
    • El tamaño de la miniatura será de 200x200 píxeles.
    • Recorte en la zona central mediante crop="center"
    • Usar formato de imagen PNG.
    • Asignar el objeto miniatura a una variable thumb con as thumb.
  3. Utilizamos la variable thumb creada anteriormente y mostramos la imagen.
  4. Hay que cerrar la etiqueta.

De esta forma habremos creado una miniatura de 200x200 píxeles para mostrar la imagen de portada del «post» en cuestión.

Gestión de miniaturas

El paquete sorl-thumbnail almacena las miniaturas en la ruta MEDIA_ROOT/THUMBNAIL_PREFIX:

Por tanto la carpeta resultante donde se guardan las miniaturas generadas sería /media/cache/.

Comandos de gestión de miniaturas

El paquete sorl-thumbnail ofrece distintos comandos de gestión para borrar miniaturas, resetear la base de datos, hacer limpieza etc.

De menor a mayor grado de «agresividad» en el borrado tenemos estos comandos:

Django Markdownify

django-markdownify es un paquete Python que se integra con Django y ofrece un filtro de plantilla para convertir Markdown en HTML.

Instalación

La instalación del paquete es muy sencilla:

$ pip install django-markdownify
$ uv add django-markdownify

Este paquete depende de Python-Markdown que se instala automáticamente. En este último paquete encontramos la función markdown.markdown que puede ser útil en vistas u otros componentes.

Configuración

Para configurar django-markdownify debemos «instalar» la aplicación en settings.py:

main/settings.py
INSTALLED_APPS = (
    # ...
    'markdownify.apps.MarkdownifyConfig',
    # ...
)
Whitelist

Un detalle importante a tener en cuenta es que este paquete trabaja con una «whitelist» de etiquetas que por defecto son: a, abbr, acronym, b, blockquote, code, em, i, li, ol, strong, ul.

Si queremos modificar las etiquetas tendremos que tocar el fichero settings.py. Por ejemplo para incluir también la etiqueta <pre> tendremos que hacer lo siguiente:

main/settings.py
MARKDOWNIFY = {
    "default": {
        "WHITELIST_TAGS": [
            'a',
            'abbr',
            'acronym',
            'b',
            'blockquote',
            'em',
            'i',
            'li',
            'ol',
            'p',
            'pre',
            'strong',
            'ul',
        ]
    }
} 

Modo de uso

El modo de uso es realmente sencillo. Veamos un ejemplo en el que partimos de un objeto «post» cuyo contenido está en formato markdown. Con el siguiente fragmento de código conseguiremos que el contenido del «post» se convierta a HTML:

posts/templates/posts/post/detail.html
{% load markdownify %}

<h1>{{ post.title }}</h1>
<p>{{ post.content|markdownify }}</p>

Django-RQ

django-rq es un paquete Python que se integra con Django y permite desacoplar tareas enviándolas a una cola de mensajes gestionada por Redis.

Entre los casos de uso más habituales están aquellas tareas que consumen mucha CPU o realizan gran cantidad de operaciones de entrada/salida. No es recomendable tener al usuario esperando a que finalicen estas tareas para dar una respuesta HTTP.

Lo habitual es indicar al usuario de que la tarea «en cuestión» ya se está procesando y notificar a posteriori cuando se haya completado.

Instalación

La instalación del paquete es muy sencilla:

$ pip install django-rq
$ uv add django-rq

Redis

Es necesario igualmente tener instalado el servicio Redis . Para probar si tienes el servicio instalado y bien configurado en tu sistema basta con hacer:

$ redis-cli ping
PONG

Configuración

Para configurar django-rq debemos añadir ciertas líneas a settings.py:

main/settings.py
INSTALLED_APPS = (
    # ...
    'django_rq',#(1)!
    # ...
)

RQ_QUEUES = {#(2)!
    'default': {#(3)!
        'HOST': 'localhost',#(4)!
        'PORT': 6379,#(5)!
        'DB': 0,#(6)!
    },
}

  1. Se «instala» la aplicación para que Django la reconozca.
  2. Se definen las distintas colas de mensajes.
  3. Existen la posibilidad de crear distintas prioridades. Con default tenemos suficiente (según el contexto).
  4. Máquina en la que está instalado el servicio redis (en este caso localhost).
  5. Puerto en el cual está escuchando el servicio redis (el habitual es 6379).
    • Número (identificador) de base de datos a utilizar dentro de redis (en este caso 0). Se podría usar cualquier otro identificador.
    • Especialmente para entornos de producción, si ya existe otro proceso RQ usando DB=0 hay que usar un identificador no «ocupado», por ejemplo DB=1.

Aunque no es obligatorio, es muy recomendable añadir las URLs de gestión:

main/urls.py
from django.contrib import admin
from django.urls import include, path

import notices.views

urlpatterns = [
    path('admin/', admin.site.urls),
    # ...
    path('django-rq/', include('django_rq.urls')),#(1)!
]

  1. Accediendo a http://localhost:8000/django-rq/ (o la URL correspondiente de producción) podremos gestionar las tareas que se envían a RQ.

Por último aplicamos las migraciones correspondientes a la aplicación:

$ ./manage.py migrate django_rq
Operations to perform:
Apply all migrations: django_rq
Running migrations:
Applying django_rq.0001_initial... OK
$ uv run manage.py migrate django_rq
Operations to perform:
Apply all migrations: django_rq
Running migrations:
Applying django_rq.0001_initial... OK

Modo de uso

Para desacoplar una tarea y enviarla a la cola de mensajes hay que realizar tres pasos:

1⃣ Marcar la función en cuestión como una tarea.
2⃣ Invocar el «desacople» de la tarea.
3⃣ Levantar un «worker» RQ para atender peticiones.

Veamos un ejemplo en el proyecto del «blog». La idea es que cada vez que se almacene un nuevo «post» desacoplemos una tarea que calcula estadísticas:

posts/tasks.py
from django_rq import job#(1)!

import posts.models as pm#(2)!


@job#(3)!
def post_stats() -> None:#(4)!
    posts = pm.Post.objects.all()
    num_posts = posts.count()
    tot_content_length = sum(len(post.content) for post in posts)
    avg_content_length = tot_content_length / num_posts if num_posts > 0 else 0
    print(f'Total Posts: {num_posts}')
    print(f'Average Content Length: {avg_content_length:.2f} characters')

  1. Importamos el decorador.
    • Deberíamos importar con from .models import Post pero nos llevaría a un import circular.
    • Para resolverlo, realizamos la importación de esta manera.
    • El alias as es opcional.
  2. Marcamos la función como una tarea django-rq.
    • Definimos la función normalmente.
    • En este caso la función no tiene parámetros pero se podrían definir aquellos parámetros necesarios.
    • En el caso de pasar argumentos estos deben ser serializables. Por defecto se utiliza el módulo pickle como serializador, pero se podrían definir otros serializadores alternativos.

posts/models.py
from django.db import models
from django.utils.text import slugify

from . import tasks#(1)!


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)
        tasks.post_stats.delay()#(2)!

  1. Importamos el módulo de tareas.
  2. Invocamos Desacoplamos la tarea.

Ahora es necesario levantar el proceso que atiende las peticiones de tareas desatendidas:

$ ./manage.py rqworker
$ uv run manage.py rqworker

La salida debería ser similar a la siguiente:

18:31:45 Worker 06a7fa349ea0459aa9c38e0a2132a201: started with PID 89082, version 2.6.1
18:31:45 Worker 06a7fa349ea0459aa9c38e0a2132a201: subscribing to channel rq:pubsub:06a7fa349ea0459aa9c38e0a2132a201
18:31:45 *** Listening on default...
18:31:45 Worker 06a7fa349ea0459aa9c38e0a2132a201: cleaning registries for queue: default

En el momento de guardar nuevos «posts» podremos observar que la tarea se «encola» y se atiende por django-rq de la forma esperada:

>>> Post.objects.create(title='Django is awesome', content='Django makes it easier to build better web apps')
<Post: Django is awesome>
>>> Post.objects.create(title='Python is great', content='Python makes it funnier to write software')
<Post: Python is great>
10:42:35 default: posts.tasks.post_stats() (aa15b6b7-d369-46aa-a14a-58334f3f6740)
Total Posts: 1
Average Content Length: 47.00 characters
10:42:35 Successfully completed posts.tasks.post_stats() job in 0:00:00.006705s on worker 75f316d097dc425abfece73b18a0c702
10:42:35 default: Job OK (aa15b6b7-d369-46aa-a14a-58334f3f6740)
10:42:35 Result is kept for 500 seconds
10:44:18 default: posts.tasks.post_stats() (d57539ea-a169-4671-9ac4-fa892135f8ac)
Total Posts: 2
Average Content Length: 44.00 characters
10:44:18 Successfully completed posts.tasks.post_stats() job in 0:00:00.007005s on worker 75f316d097dc425abfece73b18a0c702
10:44:18 default: Job OK (d57539ea-a169-4671-9ac4-fa892135f8ac)
10:44:18 Result is kept for 500 seconds

Recargar tras cambios

El comando ./manage.py rqworker no recarga el proceso cuando hay cambios en el código.

Para solucionarlo, podemos hacer uso del paquete watchdog que se encarga de «escuchar» cambios en el código y recargar los procesos indicados. Su instalación es muy sencilla:

$ pip install watchdog

$ uv add --dev watchdog #(1)!

  1. Dado que es una utilidad exclusivamente para la fase de desarrollo, utilizamos el modificador --dev para indicar que sólo se instale en dicho contexto.

Suponiendo que las tareas RQ las estamos escribiendo en ficheros tasks.py se podría usar el siguiente comando watchdog para recargar los cambios:

$ watchmedo auto-restart --pattern=tasks.py --recursive -- ./manage.py rqworker #(1)!

  1. Si quisiéramos recargar tras un cambio en cualquier fichero Python tendríamos que modificar el argumento: --pattern=*.py

$ uv run watchmedo auto-restart --pattern=tasks.py --recursive -- ./manage.py rqworker #(1)!

  1. Si quisiéramos recargar tras un cambio en cualquier fichero Python tendríamos que modificar el argumento: --pattern=*.py
justfile

Consulta la receta rq para incluirla en tu justfile.

Enviar correo

Una tarea bastante habitual en cualquier aplicación web es notificar a los usuarios mediante correo electrónico. Es por ello que Django ofrece una serie de funcionalidades de envío de correo, que hacen esta tarea realmente sencilla.

Configuración

Es necesario definir —al menos— las siguientes variables en el fichero de configuración del proyecto:

main/settings.py
EMAIL_HOST = 'email-host'
EMAIL_PORT = 'email-port'
EMAIL_HOST_USER = 'email-host-user'
EMAIL_HOST_PASSWORD = 'email-host-password'
DEFAULT_FROM_EMAIL = 'default-from-email'#(1)!

    • Se trata del correo electrónico origen que verá el usuario notificado.
    • Aunque este dato no es oligatorio, resulta cómodo fijarlo aquí y poder usarlo en el resto de la aplicación.
    • Suele ser habitual algo del estilo 'noreply@example.com'

Brevo

Basándome en mi experiencia, y sin buscar ningún tipo de publicidad (no me llevo nada), me gustaría comentar aquí el caso de Brevo que proporciona credenciales «gratuitas» para poder hacer uso de sus servicios SMTP.

Una vez dados de alta en Brevo, tendremos que acceder a la sección de configuración SMTP y Generar una nueva clave SMTP. Con esto ya dispondremos de los datos necesarios para configurar el envío de correo:

Configuración Valor
EMAIL_HOST 'smtp-relay.brevo.com'
EMAIL_PORT 587
EMAIL_HOST_USER Correo de «Iniciar Sesión/Login» de tu configuración SMTP
Típicamente algo en la forma a7f45c86e@smtp-brevo.com
EMAIL_HOST_PASSWORD Valor de clave SMTP
⚠ Sólo aparecerá la primera vez (guárdala en sitio seguro)
DEFAULT_FROM_EMAIL El correo electrónico que usaste para crear la cuenta brevo.com

EMAIL_HOST_PASSWORD

Nunca expongas el contenido de EMAIL_HOST_PASSWORD ni lo incluyas en el control de versiones de tu proyecto. El paquete prettyconf puede ser de gran ayuda.

Modo de uso

Existen varias maneras de enviar correo a través de Django, pero aquí vamos a tratar el caso de uso mediante la clase EmailMessage, ya que es la que ofrece mayor flexibilidad.

>>> from django.core.mail import EmailMessage

>>> email = EmailMessage(
...     subject='Email test',
...     body='Hello there! This is the email body',
...     to=['recipient@example.com'],
... )

>>> email.send()#(1)!
1

  1. Esta función devuelve un 1 si todo ha ido bien y un valor distinto si ha habido algún error.

>>> from django.core.mail import EmailMessage

>>> email = EmailMessage(
...     subject='Email test',
...     body='<h3>Hello there!</h3> <p>This is the email body</p>',
...     to=['recipient@example.com'],
... )
>>> email.content_subtype = 'html'

>>> email.send()#(1)!
1

  1. Esta función devuelve un 1 si todo ha ido bien y un valor distinto si ha habido algún error.

>>> from django.core.mail import EmailMessage

>>> email = EmailMessage(
...     subject='Email test',
...     body='<h3>Hello there!</h3> <p>This is the email body</p>',
...     to=['recipient@example.com'],
... )
>>> email.content_subtype = 'html'
>>> email.attach_file('report.pdf')#(1)!

>>> email.send()#(2)!
1

  1. Puedes usar ruta relativa o ruta absoluta al fichero en cuestión.
  2. Esta función devuelve un 1 si todo ha ido bien y un valor distinto si ha habido algún error.
Múltiples destinatarios

En el caso de querer enviar el mismo correo a múltiples destinatarios, podemos usar el parámetro to (formato lista) del constructor sobre EmailMessage().

Pero una forma más «eficiente» de llevarlo a cabo es utilizando la función send_mass_mail() que sólo abre una única conexión con el servidor SMTP.

Plantillas de correo

Una estrategia bastante interesante es escribir la plantilla de correo (como una plantilla normal de Django) pero usando Markdown y luego renderizarla mediante Django Markdownify.

Supogamos el siguiente ejemplo en el que preparamos una plantilla de correo para informar de que un nuevo «post» se ha añadido al «blog» desde la vista correspondiente:

posts/templates/posts/emails/add.md
Hi there!

We are exited to announce that a new post has added to our blog:
**{{ post }}**

Keep in touch!
Best regards.

posts/views.py
from django.core.mail import EmailMessage
from django.shortcuts import redirect, render
from django.template.loader import render_to_string#(1)!

from markdown import markdown#(2)!


@login_required
def add_post(request):
    if request.method == 'POST':
        if (form := AddPostForm(request.POST)).is_valid():
            post = form.save()
            body = markdown(render_to_string(#(3)!
                'posts/emails/add.md',#(4)!
                {'post': post},#(5)!
            ))
            email = EmailMessage(
                subject='New post',
                body=body,
                to=['super@blog.com'],
            )
            email.send()#(6)!
            return redirect('posts:post-list')
    else:
        form = AddPostForm()
    return render(request, 'posts/post/add.html', {'form': form})

  1. Necesitamos la función render_to_string() que devuelve la plantilla renderizada como cadena de texto.
  2. El paquete markdown nos permite pasar de Markdown a HTML.
  3. Renderizamos la plantilla usando funcionalidades de Django y luego la convertimos desde Markdown a HTML.
  4. Pasamos la ruta a la plantilla de correo.
  5. El contexto vendrá definido por el «post» que acabamos de crear.
  6. Idealmente habría que desacoplar esta tarea.

Django ColorField

django-colorfield es un paquete Python que proporciona un «nuevo» campo para almacenar colores en los modelos de Django.

Además ofrece un «color picker» muy agradable para seleccionar el color de manera visual en la interfaz administrativa de Django.

Instalación

La instalación del paquete es muy sencilla:

$ pip install django-colorfield
$ uv add django-colorfield

Configuración

Para configurar django-colorfield debemos «instalar» la aplicación en settings.py:

main/settings.py
INSTALLED_APPS = (
    # ...
    'colorfield',
    # ...
)
Producción

Sólo en un escenario de producción, debes ejecutar también el siguiente comando para recopilar los ficheros estáticos y que el selector de color en la interfaz administrativa funcione correctamente:

$ ./manage.py collectstatic
$ uv run manage.py collectstatic

Modo de uso

Este paquete proporciona la clase ColorField para almacenar colores.

Veamos un ejemplo para almacenar el color de la categoría de un «post» en un proyecto de «blog»:

categories/models.py
from colorfield.fields import ColorField
from django.db import models


class Category(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()
    color = ColorField(default='#FF0000')#(1)!

    • Es posible definir un color por defecto.
    • En este caso se ha definido el rojo #FF0000.