Ahora que entiendes un poco más acerca del funcionamiento interno del sistema de plantillas, echemos una mirada a cómo extender el sistema con código propio.
La mayor parte de la personalización de plantillas se da en forma de etiquetas y/o filtros. Aunque el lenguaje de plantillas de Django incluye muchos, probablemente ensamblarás tus propias librerías de etiquetas y filtros que se adapten a tus propias necesidades. Afortunadamente, es muy fácil definir tu propia funcionalidad.
10.4.1. Crear una librería para plantillas
Ya sea que estés escribiendo etiquetas o filtros personalizados, la primera tarea a realizar es crear una librería para plantillas — un pequeño fragmento de infraestructura con el cual Django puede interactuar.
La creación de una librería para plantillas es un proceso de dos pasos.
Primero, decidir qué aplicación Django alojará la librería. Si has
creado una aplicación vía manage.py startapp
puedes colocarla allí, o
puedes crear otra aplicación con el solo fin de alojar la librería.
Sin importar cual de las dos rutas tomes, asegúrate de agregar la
aplicación a tu variable de configuración INSTALLED_APPS
.
Explicaremos esto un poco más adelante.
Segundo, crear un directorio templatetags
en el paquete de aplicación
Django apropiado. Debe encontrarse en el mismo nivel que models.py
,
views.py
, etc. Por ejemplo:
books/ __init__.py models.py templatetags/ views.py
Crea dos archivos vacíos en el directorio templatetags
: un archivo
__init__.py
(para indicarle a Python que se trata de un paquete que
contiene código Python) y un archivo que contendrá tus definiciones
personalizadas de etiquetas/filtros. El nombre del segundo archivo es el
que usarás para cargar las etiquetas más tarde. Por ejemplo, si tus
etiquetas/filtros personalizadas están en un archivo llamado
poll_extras.py
, entonces deberás escribir lo siguiente en una
plantilla:
{% load poll_extras %}
La etiqueta {% load %}
examina tu variable de configuración
INSTALLED_APPS
y sólo permite la carga de librerías para plantillas
desde aplicaciones Django que estén instaladas. Se trata de una
característica de seguridad; te permite tener en cierto equipo el código
Python de varias librerías para plantillas sin tener que activar el
acceso a todas ellas para cada instalación de Django.
Si escribes una librería para plantillas que no se encuentra atada a ningún
modelo/vista particular es válido y normal el tener un paquete de aplicación
Django que sólo contiene un paquete templatetags
. No existen límites en lo
referente a cuántos módulos puedes poner en el paquete templatetags
. Sólo
ten presente que una sentencia {% load %}
cargará etiquetas/filtros para el
nombre del módulo Python provisto, no el nombre de la aplicación.
Una vez que has creado ese módulo Python, sólo tendrás que escribir un poquito de código Python, dependiendo de si estás escribiendo filtros o etiquetas.
Para ser una librería de etiquetas válida, el módulo debe contener una
variable a nivel del módulo llamada register
que sea una instancia de
template.Library
. Esta instancia de template.Library
es la estructura de
datos en la cual son registradas todas las etiquetas y filtros. Así que inserta
en la zona superior de tu módulo, lo siguiente:
from django import template
register = template.Library()
Nota Para ver un buen número de ejemplos, examina el código fuente de los filtros
y etiquetas incluidos con Django. Puedes encontrarlos en
django/template/defaultfilters.py
y django/template/defaulttags.py
,
respectivamente. Algunas aplicaciones en django.contrib
también
contienen librerías para plantillas.
Una vez que hayas creado esta variable register
, usarás la misma para crear
filtros y etiquetas para plantillas.
10.4.2. Escribir filtros de plantilla personalizados
Los filtros personalizados son sólo funciones Python que reciben uno o dos argumentos:
- El valor de la variable (entrada)
- El valor del argumento, el cual puede tener un valor por omisión o puede ser obviado.
Por ejemplo, en el filtro {{ var|foo:"bar" }}
el filtro foo
recibiría el
contenido de la variable var
y el argumento "bar"
.
Las funciones filtro deben siempre retornar algo. No deben arrojar excepciones, y deben fallar silenciosamente. Si existe un error, las mismas deben retornar la entrada original o una cadena vacía, dependiendo de qué sea más apropiado.
Esta es un ejemplo de definición de un filtro:
def cut(value, arg):
"Removes all values of arg from the given string"
return value.replace(arg, '')
Y este es un ejemplo de cómo se usaría:
{{ somevariable|cut:"0" }}
La mayoría de los filtros no reciben argumentos. En ese caso, basta con que no incluyas el argumento en tu función:
def lower(value): # Only one argument.
"Converts a string into all lowercase"
return value.lower()
Una vez que has escrito tu definición de filtro, necesitas registrarlo en tu
instancia de Library
, para que esté disponible para el lenguaje de
plantillas de Django:
register.filter('cut', cut)
register.filter('lower', lower)
El método Library.filter()
tiene dos argumentos:
- El nombre del filtro (una cadena)
- La función filtro propiamente dicha
Si estás usando Python 2.4 o más reciente, puedes usar register.filter()
como un decorador:
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@register.filter
def lower(value):
return value.lower()
Si no provees el argumento name
, como en el segundo ejemplo, Django usará el
nombre de la función como nombre del filtro.
Veamos entonces el ejemplo completo de una librería para plantillas, que
provee el filtro cut
:
from django import template
register = template.Library()
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
10.4.3. Escribir etiquetas de plantilla personalizadas
Las etiquetas son más complejas que los filtros porque las etiquetas pueden implementar prácticamente cualquier funcionalidad.
El Capítulo 4 describe cómo el sistema de plantillas funciona como un proceso de dos etapas: compilación y renderizado. Para definir una etiqueta de plantilla personalizada, necesitas indicarle a Django cómo manejar ambas etapas cuando llega a tu etiqueta.
Cuando Django compila una plantilla, divide el texto crudo de la plantilla en
nodos. Cada nodo es una instancia de django.template.Node
y tiene un
método render()
. Por lo tanto, una plantilla compilada es simplemente una
lista de objetos Node
.
Cuando llamas a render()
en una plantilla compilada, la plantilla llama a
render()
en cada Node()
de su lista de nodos, con el contexto
proporcionado. Los resultados son todos concatenados juntos para formar la
salida de la plantilla. Por ende, para definir una etiqueta de plantilla
personalizada debes especificar cómo se debe convertir la etiqueta en crudo en
un Node
(la función de compilación) y qué hace el método render()
del
nodo.
En las secciones que siguen, explicaremos todos los pasos necesarios para escribir una etiqueta propia.
10.4.3.1. Escribir la función de compilación
Para cada etiqueta de plantilla que encuentra, el intérprete (parser) de
plantillas llama a una función de Python pasándole el contenido de la etiqueta
y el objeto parser en sí mismo. Esta función tiene la responsabilidad de
retornar una instancia de Node
basada en el contenido de la etiqueta.
Por ejemplo, escribamos una etiqueta {% current_time %}
que visualice la
fecha/hora actuales con un formato determinado por un parámetro pasado a la
etiqueta, usando la sintaxis de strftime
(ver
http://www.djangoproject.com/r/python/strftime/
). Es una buena idea definir
la sintaxis de la etiqueta previamente. En nuestro caso, supongamos que la
etiqueta deberá ser usada de la siguiente manera:
<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>
Nota Si, esta etiqueta de plantilla es redundante — La etiqueta {% now %}
incluida en Django por defecto hace exactamente lo mismo con una sintaxis
más simple. Sólo mostramos esta etiqueta a modo de ejemplo.
Para evaluar esta función, se deberá obtener el parámetro y crear el objeto
Node
:
from django import template
def do_current_time(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, format_string = token.split_contents()
except ValueError:
msg = '%r tag requires a single argument' % token.split_contents()[0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[1:-1])
Hay muchas cosas en juego aquí:
parser
es la instancia del parser. No lo necesitamos en este ejemplo.token.contents
es un string con los contenidos crudos de la etiqueta, en nuestro ejemplo sería:'current_time "%Y-%m-%d %I:%M %p"'
.- El método
token.split_contents()
separa los argumentos en sus espacios, mientras deja unidas a los strings. Evite utilizartoken.contents.split()
(el cual usa la semántica natural de Python para dividir strings, y por esto no es tan robusto, ya que divide en todos los espacios, incluyendo aquellos dentro de cadenas entre comillas. - Esta función es la responsable de generar la excepción
django.template.TemplateSyntaxError
con mensajes útiles, ante cualquier caso de error de sintaxis. - No escribas el nombre de la etiqueta en el mensaje de error, ya que eso
acoplaría innecesariamente el nombre de la etiqueta a la función. En
cambio,
token.split_contents()[0]
siempre contendrá el nombre de tu etiqueta — aún cuando la etiqueta no lleve argumentos. - La función devuelve
CurrentTimeNode
(el cual mostraremos en un momento) conteniendo todo lo que el nodo necesita saber sobre esta etiqueta. En este caso, sólo pasa el argumento"%Y-%m-%d %I:%M %p"
. Las comillas son removidas conformat_string[1:-1]
. - Las funciones de compilación de etiquetas de plantilla deben devolver
una subclase de
Nodo
; cualquier otro valor es un error.
10.4.3.2. Escribir el nodo de plantilla
El segundo paso para escribir etiquetas propias, es definir una subclase de
Node
que posea un método render()
. Continuando con el ejemplo previo,
debemos definir CurrentTimeNode
:
import datetime
class CurrentTimeNode(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
now = datetime.datetime.now()
return now.strftime(self.format_string)
Estas dos funciones (__init__
y render
) se relacionan directamente
con los dos pasos para el proceso de la plantilla (compilación y renderizado).
La función de inicialización sólo necesitará almacenar el string con el formato
deseado, el trabajo real sucede dentro de la función render()
Del mismo modo que los filtros de plantilla, estas funciones de renderización deberían fallar silenciosamente en lugar de generar errores. En el único momento en el cual se le es permitido a las etiquetas de plantilla generar errores es en tiempo de compilación.
10.4.3.3. Registrar la etiqueta
Finalmente, deberás registrar la etiqueta con tu objeto Library
dentro del
módulo. Registrar nuevas etiquetas es muy similar a registrar nuevos filtros
(como explicamos previamente). Sólo deberás instanciar un objeto
template.Library
y llamar a su método tag()
. Por ejemplo:
register.tag('current_time', do_current_time)
El método tag()
toma dos argumentos:
- El nombre de la etiqueta de plantilla (string). Si esto se omite, se utilizará el nombre de la función de compilación.
- La función de compilación.
De manera similar a como sucede con el registro de filtros, también es posible
utilizar register.tag
como un decorador en Python 2.4 o posterior:
@register.tag(name="current_time")
def do_current_time(parser, token):
# ...
@register.tag
def shout(parser, token):
# ...
Si omitimos el argumento name
, así como en el segundo ejemplo, Django
usará el nombre de la función como nombre de la etiqueta.
10.4.3.4. Definir una variable en el contexto
El ejemplo en la sección anterior simplemente devuelve un valor. Muchas veces es útil definir variables de plantilla en vez de simplemente devolver valores. De esta manera, los autores de plantillas podrán directamente utilizar las variables que esta etiqueta defina.
Para definir una variable en el contexto, asignaremos a nuestro objeto
context
disponible en el método render()
nuestras variables, como si de
un diccionario se tratase. Aquí mostramos la versión actualizada de
CurrentTimeNode
que define una variable de plantilla, current_time
, en
lugar de devolverla:
class CurrentTimeNode2(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
now = datetime.datetime.now()
context['current_time'] = now.strftime(self.format_string)
return ''
Devolvemos un string vacío, debido a que render()
siempre debe devolver
un string. Entonces, si todo lo que la etiqueta hace es definir una variable,
render()
debe al menos devolver un string vacío.
De esta manera usaríamos esta nueva versión de nuestra etiqueta:
{% current_time2 "%Y-%M-%d %I:%M %p" %}
<p>The time is {{ current_time }}.</p>
Pero hay un problema con CurrentTimeNode2
: el nombre de la variable
current_time
está definido dentro del código. Esto significa que tendrás que
asegurar que {{ current_time }}
no sea utilizado en otro lugar dentro de la
plantilla, ya que {% current_time %}
sobreescribirá el valor de esa otra
variable.
Una solución más limpia, es poder recibir el nombre de la variable en la etiqueta de plantilla así:
{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>
Para hacer esto, necesitaremos modificar tanto la función de compilación
como la clase Node
de esta manera:
import re
class CurrentTimeNode3(template.Node):
def __init__(self, format_string, var_name):
self.format_string = format_string
self.var_name = var_name
def render(self, context):
now = datetime.datetime.now()
context[self.var_name] = now.strftime(self.format_string)
return ''
def do_current_time(parser, token):
# This version uses a regular expression to parse tag contents.
try:
# Splitting by None == splitting by spaces.
tag_name, arg = token.contents.split(None, 1)
except ValueError:
msg = '%r tag requires arguments' % token.contents[0]
raise template.TemplateSyntaxError(msg)
m = re.search(r'(.*?) as (\w+)', arg)
if m:
fmt, var_name = m.groups()
else:
msg = '%r tag had invalid arguments' % tag_name
raise template.TemplateSyntaxError(msg)
if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
msg = "%r tag's argument should be in quotes" % tag_name
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode3(fmt[1:-1], var_name)
Ahora, do_current_time()
pasa el string de formato junto al nombre de la
variable a CurrentTimeNode3
.
10.4.3.5. Evaluar hasta otra etiqueta de bloque
Las etiquetas de plantilla pueden funcionar como bloques que contienen otras
etiquetas (piensa en {% if %}
, {% for %}
, etc.). Para crear una
etiqueta como esta, usa parser.parse()
en tu función de compilación.
Aquí vemos como está implementada la etiqueta estándar {% coment %}
:
def do_comment(parser, token):
nodelist = parser.parse(('endcomment',))
parser.delete_first_token()
return CommentNode()
class CommentNode(template.Node):
def render(self, context):
return ''
parser.parse()
toma una tupla de nombres de etiquetas de bloque para
evaluar y devuelve una instancia de django.template.NodeList
, la cual es una
lista de todos los objetos Nodo
que el parser encontró antes de haber
encontrado alguna de las etiquetas nombradas en la tupla.
Entonces, en el ejemplo previo, nodelist
es una lista con todos los nodos
entre {% comment %}
y {% endcomment %}
, excluyendo a los mismos {%
comment %}
y {% endcomment %}
.
Luego de que parser.parse()
es llamado el parser aún no ha "consumido" la
etiqueta {% endcomment %}
, es por eso que en el código se necesita llamar
explícitamente a parser.delete_first_token()
para prevenir que esta
etiqueta sea procesada nuevamente.
Luego, CommentNode.render()
simplemente devuelve un string vacío.
Cualquier cosa entre {% comment %}
y {% endcomment %}
es ignorada.
En el ejemplo anterior, do_comment()
desechó todo entre {% comment %}
y
{% endcomment %}
, pero también es posible hacer algo con el código entre
estas etiquetas.
Por ejemplo, presentamos una etiqueta de plantilla, {% upper %}
, que
convertirá a mayúsculas todo hasta la etiqueta {% endupper %}
:
{% upper %}
This will appear in uppercase, {{ your_name }}.
{% endupper %}
Como en el ejemplo previo, utilizaremos parser.parse()
pero esta vez
pasamos el resultado en nodelist
a Node
:
@register.tag
def do_upper(parser, token):
nodelist = parser.parse(('endupper',))
parser.delete_first_token()
return UpperNode(nodelist)
class UpperNode(template.Node):
def __init__(self, nodelist):
self.nodelist = nodelist
def render(self, context):
output = self.nodelist.render(context)
return output.upper()
El único concepto nuevo aquí es self.nodelist.render(context)
en
UpperNode.render()
. El mismo simplemente llama a render()
en cada
Node
en la lista de nodos.
Para más ejemplos de renderizado complejo, examina el código fuente para las
etiquetas {% if %}
, {% for %}
, {% ifequal %}
y {% ifchanged %}
.
Puedes encontrarlas en django/template/defaulttags.py
.
10.4.4. Un atajo para etiquetas simples
Muchas etiquetas de plantilla reciben un único argumento — una cadena o una
referencia a una variable de plantilla — y retornan una cadena luego de hacer
algún procesamiento basado solamente en el argumento de entrada e información
externa. Por ejemplo la etiqueta current_time
que escribimos antes es de
este tipo. Le pasamos una cadena de formato, y retorna la hora como una cadena.
Para facilitar la creación de esos tipos de etiquetas, Django provee una
función auxiliar: simple_tag
. Esta función, que es un método de
django.template.Library
, recibe una función que acepta un argumento, lo
encapsula en una función render
y el resto de las piezas necesarias que
mencionamos previamente y lo registra con el sistema de plantillas.
Nuestra función current_time
podría entonces ser escrita de la siguiente
manera:
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
register.simple_tag(current_time)
En Python 2.4 la sintaxis de decorador también funciona:
@register.simple_tag
def current_time(token):
...
Un par de cosas a tener en cuenta acerca de la función auxiliar simple_tag
:
- Sólo se pasa un argumento a nuestra función.
- La verificación de la cantidad requerida de argumentos ya ha sido realizada para el momento en el que nuestra función es llamada, de manera que no es necesario que lo hagamos nosotros.
- Las comillas alrededor del argumento (si existieran) ya han sido quitadas, de manera que recibimos una cadena común.
10.4.5. Etiquetas de inclusión
Otro tipo de etiquetas de plantilla común es aquel que visualiza ciertos datos renderizando otra plantilla. Por ejemplo la interfaz de administración de Django usa etiquetas de plantillas personalizadas (custom) para visualizar los botones en la parte inferior de la páginas de formularios "agregar/cambiar". Dichos botones siempre se ven igual, pero el destino del enlace cambia dependiendo del objeto que se está modificando. Se trata de un caso perfecto para el uso de una pequeña plantilla que es llenada con detalles del objeto actual.
Ese tipo de etiquetas reciben el nombre de etiquetas de inclusión. Es
probablemente mejor demostrar cómo escribir una usando un ejemplo. Escribamos
una etiqueta que produzca una lista de opciones para un simple objeto Poll
con múltiples opciones. Usaremos una etiqueta como esta:
{% show_results poll %}
El resultado será algo como esto:
<ul>
<li>First choice</li>
<li>Second choice</li>
<li>Third choice</li>
</ul>
Primero definimos la función que toma el argumento y produce un diccionario de datos con los resultados. Nota que nos basta un diccionario y no necesitamos retornar nada más complejo. Esto será usado como el contexto para el fragmento de plantilla:
def show_books_for_author(author):
books = author.book_set.all()
return {'books': books}
Luego creamos la plantilla usada para renderizar la salida de la etiqueta. Siguiendo con nuestro ejemplo, la plantilla es muy simple:
<ul>
{% for book in books %}
<li> {{ book }} </li>
{% endfor %}
</ul>
Finalmente creamos y registramos la etiqueta de inclusión invocando el método
inclusion_tag()
sobre un objeto Library
.
Continuando con nuestro ejemplo, si la plantilla se encuentra en un archivo
llamado polls/result_snippet.html
, registraremos la plantilla de la
siguiente manera:
register.inclusion_tag('books/books_for_author.html')(show_books_for_author)
Como siempre, la sintaxis de decoradores de Python 2.4 también funciona, de manera que en cambio podríamos haber escrito:
@register.inclusion_tag('books/books_for_author.html')
def show_books_for_author(show_books_for_author):
...
A veces tus etiquetas de inclusión necesitan tener acceso a valores del
contexto de la plantilla padre. Para resolver esto Django provee una opción
takes_context
para las etiquetas de inclusión. Si especificas
takes_context
cuando creas una etiqueta de plantilla, la misma no tendrá
argumentos obligatorios y la función Python subyacente tendrá un argumento: el
contexto de la plantilla en el estado en el que se encontraba cuando la
etiqueta fue invocada.
Por ejemplo supongamos que estás escribiendo una etiqueta de inclusión que será
siempre usada en un contexto que contiene variables home_link
y
home_title
que apuntan a la página principal. Así es como se vería la
función Python:
@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
return {
'link': context['home_link'],
'title': context['home_title'],
}
Nota El primer parámetro de la función debe llamarse context
obligatoriamente.
La plantilla link.html
podría contener lo siguiente:
Jump directly to <a href="{{ link }}">{{ title }}</a>.
Entonces, cada vez que desees usar esa etiqueta personalizada, carga su librería y ejecútala sin argumentos, de la siguiente manera:
{% jump_link %}