Ver índice de contenidos del libro

10.4. Extender el sistema de plantillas

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 utilizar token.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 con format_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 %}