Módulo 4 · Lección 19 de 45
Filtros y etiquetas propias
Cuando el lenguaje de plantillas se queda corto, se amplía: un módulo, un decorador y una función normal de Python.
La lección 17 dejó una lista de cosas que las plantillas no saben hacer, y la respuesta general era calcularlas en la vista o en el modelo. Esa respuesta vale casi siempre y falla en dos casos concretos.
El primero es cuando lo que hace falta es de presentación y de todas las páginas: quitar las tildes para comparar, dar formato a un teléfono, pintar una insignia de color. Eso no es del modelo, porque no es verdad sobre el objeto, y ponerlo en cada vista es repetirlo.
El segundo es cuando el trozo necesita datos propios: una barra lateral con los últimos contactos, que sale en todas las páginas. Con include habría que pasar esa lista desde todas las vistas.
Para los dos, Django deja ampliar el lenguaje con funciones de Python normales.
Dónde van#
La estructura es obligatoria y no admite variaciones:
contactos/
templatetags/
__init__.py Vacío, y tiene que existir
agenda_extras.py El nombre lo eliges túTres cosas que no se pueden cambiar. La carpeta se llama templatetags, en inglés y sin guion bajo. Necesita el __init__.py para ser un paquete, como se vio en la lección 4. Y la aplicación tiene que estar en INSTALLED_APPS, que es la lección 7 otra vez: sin registrar, Django no mira ahí.
El nombre del fichero sí lo eliges, y es el que se escribe en el {% load %}, así que conviene que sea único en el proyecto entero.
from django import template
register = template.Library()Esas dos líneas van en todos. register tiene que llamarse así: es el nombre que Django busca en el módulo.
Filtros#
Un filtro es una función que recibe el valor y devuelve otro:
import unicodedata
from django import template
register = template.Library()
@register.filter
def sin_tildes(texto):
"""Quita las tildes: útil para comparar y para ordenar."""
descompuesto = unicodedata.normalize('NFKD', str(texto))
return ''.join(c for c in descompuesto if not unicodedata.combining(c))
@register.filter
def clave(diccionario, nombre):
"""Busca una clave que el punto no puede escribir, como 'mi-clave'."""
return diccionario.get(nombre, '')En la plantilla, con el {% load %} delante:
{% load agenda_extras %}
{{ contacto.nombre|sin_tildes }}
{{ datos|clave:"mi-clave" }}Zoe Alvarez
encontradaEl primer parámetro es lo que va delante del |; el segundo, si lo hay, es el argumento de detrás de los dos puntos. Un filtro admite como mucho un argumento, y eso no se puede ampliar; cuando hacen falta dos, es una etiqueta.
Fíjate en el segundo: resuelve el problema de la lección 17 con las claves que llevan guion. Es el ejemplo típico de filtro que merece la pena.
El str() del primero tampoco es casual. Un filtro puede recibir cualquier cosa, incluida la cadena vacía que Django pone cuando una variable no existe, y no puede reventar: si lo hace, se lleva la página por delante.
Etiquetas#
simple_tag#
Cuando hace falta más de un argumento, o cuando lo que se devuelve es un trozo de HTML:
from django.utils.html import format_html
@register.simple_tag
def etiqueta_grupo(contacto):
color = 'verde' if contacto.grupo else 'gris'
nombre = contacto.grupo.nombre if contacto.grupo else 'Sin grupo'
return format_html('<span class="chip {}">{}</span>', color, nombre){% etiqueta_grupo contacto %}<span class="chip verde">Trabajo</span>Sin |, sin límite de argumentos, y admite argumentos con nombre. Además se puede guardar el resultado en lugar de escribirlo:
{% etiqueta_grupo contacto as chip %}Eso mete chip en el contexto y no imprime nada, que es cómodo cuando el valor se usa dos veces.
inclusion_tag#
Para el trozo que necesita sus propios datos. La función devuelve un contexto y Django renderiza con él la plantilla que se indica:
@register.inclusion_tag('contactos/_ultimos.html')
def ultimos_contactos(cuantos=3):
return {'contactos': Contacto.objects.order_by('-creado')[:cuantos]}<ul class="ultimos">{% for c in contactos %}<li>{{ c.nombre }}</li>{% endfor %}</ul>{% ultimos_contactos 2 %}<ul class="ultimos"><li>Por id</li><li>Sin grupo</li></ul>Eso es lo que la lección anterior dejó pendiente: la barra lateral se pinta sola, sin que ninguna vista tenga que pasarle nada.
Generar HTML sin abrir un agujero#
Esto es lo más importante de la lección, porque una etiqueta propia es el sitio donde es fácil saltarse el escapado sin querer.
Compara las dos formas, medidas:
@register.simple_tag
def etiqueta_grupo(contacto):
return format_html('<span class="chip {}">{}</span>', color, nombre) # bien
@register.filter
def chip_inseguro(contacto):
return f'<span>{contacto.nombre}</span>' # malB seguro : <span class="chip verde">Trabajo</span>
C inseguro: <span>Zoe Alvarez</span>
D marcado : <span>Zoe Alvarez</span>El segundo devuelve una cadena normal, así que Django la escapa y sale el HTML literal en la página. Lo que casi todo el mundo hace entonces es añadirle |safe, o marcar la función con is_safe=True, y eso es el caso D: funciona, y a la vez acaba de abrir el agujero, porque si el nombre del contacto tuviera un <script> dentro saldría ejecutándose.
La forma correcta es format_html, que ya se mencionó en la lección 17:
format_html('<span class="chip {}">{}</span>', color, nombre)Escapa los argumentos y deja pasar el HTML de la plantilla, que lo has escrito tú. Es exactamente el reparto que se quiere, y hace lo mismo que las plantillas por defecto.
Sus dos compañeras, para cuando hay listas:
from django.utils.html import format_html, format_html_joinformat_html_join aplica format_html a cada elemento de una lista y los une, y es lo que se usa para construir una serie de etiquetas sin concatenar a mano.
Cuándo merece la pena#
Ampliar el lenguaje es fácil y por eso conviene un criterio, o se acaba con un módulo de cuarenta etiquetas que nadie recuerda:
| Lo que necesitas | Dónde va |
|---|---|
| Un dato del objeto | Propiedad del modelo |
| Un dato de esta página | La vista, en el contexto |
| Una transformación de presentación, en muchas páginas | Un filtro |
| Un trozo de HTML con lógica, en muchas páginas | simple_tag |
| Un trozo con sus propios datos, en muchas páginas | inclusion_tag |
| Algo que sale en todas las páginas y ya existe en el contexto | Un procesador de contexto |
Esa última fila es la alternativa que no se ha mencionado. Un procesador de contexto es una función que añade variables al contexto de todas las plantillas, y se registra en TEMPLATES. Los tres que ya tienes puestos son los que hacen que {{ user }} y {{ messages }} funcionen en cualquier sitio sin que ninguna vista los pase, y se ven en el bloque de usuarios.
La diferencia con una inclusion_tag es cuándo se ejecuta: un procesador de contexto corre en todas las páginas, la use la plantilla o no; una etiqueta solo corre donde se escribe. Para algo caro, la etiqueta es más prudente.