El libro de Django 1.0

4.2. Usando el sistema de plantillas

Para usar el sistema de plantillas en el código Python, sólo sigue estos dos pasos:

  1. Crea un objeto Template brindando el código en crudo de la plantilla como una cadena. Django también ofrece un camino para crear objetos Template especificando la ruta al archivo de plantilla en el sistemas de archivos; vamos a examinar esto en un rato.

  2. Llama al método render() del objeto Template con un conjunto de variables (o sea, el contexto). Este retorna una plantilla totalmente renderizada como una cadena de caracteres, con todas las variables y etiquetas de bloques evaluadas de acuerdo al contexto.

Las siguientes secciones describen cada uno de los pasos con mayor detalle.

4.2.1. Creación de objetos Template

La manera fácil de crear objetos Template es instanciarlos directamente. La clase Template se encuentra en el módulo django.template, y el constructor toma un argumento, el código en crudo de la plantilla. Vamos a sumergirnos en el intérprete interactivo de Python para ver cómo funciona este código.

Desde dentro del directorio del proyecto creado por django-admin.py startproject (como se expuso en el Capítulo 2), escribe python manage.py shell para comenzar el intérprete interactivo. Aquí hay un ensayo básico:

>>> from django.template import Template
>>> t = Template("My name is {{ name }}.")
>>> print t

Si lo estás siguiendo interactivamente, verás algo como esto::

<django.template.Template object at 0xb7d5f24c>

Ese 0xb7d5f24c será distinto cada vez, y realmente no importa; es la forma simple en que Python "identifica" un objeto de Template.

Cuando creas un objeto Template, el sistema de plantillas compila el código en crudo a uno interno, de forma optimizada, listo para renderizar. Pero si tu código de plantilla incluye errores de sintaxis, la llamada a Template() causará una excepción TemplateSyntaxError:

>>> from django.template import Template
>>> t = Template('{% notatag %} ')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  ...
  django.template.TemplateSyntaxError: Invalid block tag: 'notatag'

El sistema lanza una excepción TemplateSyntaxError por alguno de los siguientes casos:

  • Bloques de etiquetas inválidos
  • Argumentos inválidos para una etiqueta válida
  • Filtros inválidos
  • Argumentos inválidos para filtros válidos
  • Sintaxis inválida de plantilla
  • Etiquetas de bloque sin cerrar (para etiquetas de bloque que requieran la etiqueta de cierre)

4.2.2. Renderizar una plantilla

Una vez que tienes un objeto Template, le puedes pasar datos brindando un contexto. Un contexto es simplemente un conjunto de variables y sus valores asociados. Una plantilla usa estas variables para llenar y evaluar estas etiquetas de bloque.

Un contexto es representado en Django por la clase Context, ésta se encuentra en el módulo django.template. Su constructor toma un argumento opcional: un diccionario que mapea nombres de variables con valores. Llama al método render() del objeto Template con el contexto para "llenar" la plantilla:

>>> from django.template import Context, Template
>>> t = Template("My name is {{ name }}.")
>>> c = Context({"name": "Stephane"})
>>> t.render(c)
'My name is Stephane.'

Los nombres de las variables deben comenzar con una letra (A-Z o a-z) y pueden contener dígitos, guiones bajos y puntos. (Los puntos son un caso especial al que llegaremos en un momento). Los nombres de variables son sensible a mayúsculas-minúsculas.

Este es un ejemplo de compilación y renderización de una plantilla, usando la plantilla de muestra del comienzo de este capítulo:

>>> from django.template import Template, Context
>>> raw_template = """<p>Dear {{ person_name }},</p>
...
... <p>Thanks for ordering {{ product }} from {{ company }}. It's scheduled
... to ship on {{ ship_date|date:"F j, Y" }}.</p>
...
... {% if ordered_warranty %}
... <p>Your warranty information will be included in the packaging.</p>
... {% endif %}
...
... <p>Sincerely,<br />{{ company }}</p>"""
>>> t = Template(raw_template)
>>> import datetime
>>> c = Context({'person_name': 'John Smith',
...     'product': 'Super Lawn Mower',
...     'company': 'Outdoor Equipment',
...     'ship_date': datetime.date(2009, 4, 2),
...     'ordered_warranty': True})
>>> t.render(c)
"<p>Dear John Smith,</p>\n\n<p>Thanks for ordering Super Lawn Mower from
Outdoor Equipment. It's scheduled \nto ship on April 2, 2009.</p>\n\n\n
<p>Your warranty information will be included in the packaging.</p>\n\n\n
<p>Sincerely,<br />Outdoor Equipment</p>"

Vamos paso a paso por este código, de a una sentencia a la vez:

  • Primero, importamos la clase Template y Context, ambas se encuentran en el módulo django.template.
  • Guardamos en texto crudo de nuestra plantilla en la variable raw_template. Note que usamos triple comillas para delimitar la cadena de caracteres, debido a que abarca varias líneas; en el código Python, las cadenas de caracteres delimitadas con una sola comilla indican que no puede abarcar varias líneas.
  • Luego, creamos un objeto plantilla, t, pasándole raw_template al constructor de la clase Template.
  • Importamos el módulo datetime desde la librería estándar de Python, porque lo vamos a necesitar en la próxima sentencia.
  • Entonces, creamos un objeto Context , c. El constructor de Context toma un diccionario de Python, el cual mapea nombres de variables con valores. Aquí, por ejemplo, especificamos que person_name es 'John Smith', product es 'Super Lawn Mower', y así sucesivamente.
  • Finalmente, llamamos al método render() sobre nuestro objeto de plantilla, pasando a éste el contexto. Este retorna la plantilla renderizada -- esto es, reemplaza las variables de la plantilla con los valores reales de las variables, y ejecuta cualquier bloque de etiquetas.
Nota que el párrafo de garantía fue mostrado porque la variable
`ordered_warranty` se evalúa como `True`. También nota que la fecha
`April 2, 2009`, es mostrada acorde al formato de cadena de caracteres
`F j, Y`. (Explicaremos los formatos de cadenas de caracteres para el
filtro `date` a la brevedad).
Si eres nuevo en Python, quizás te preguntes por qué la salida incluye los
caracteres de nueva línea (`'\n'`) en vez de mostrar los saltos de
línea.  Esto sucede porque es una sutileza del intérprete interactivo de
Python: la llamada a `t.render(c)` retorna una cadena de caracteres, y
el intérprete interactivo, por omisión, muestra una *representación* de
ésta, en vez de imprimir el valor de la cadena. Si quieres ver la cadena
de caracteres con los saltos de líneas como verdaderos saltos de líneas en
vez de caracteres `'\n'`, usa la sentencia `print`:
`print t.render(c)`.

Estos son los fundamentos del uso del sistema de plantillas de Django: sólo escribe una plantilla, crea un objeto Template, crea un Context, y llama al método render().

4.2.3. Múltiples contextos, mismas plantillas

Una vez que tengas un objeto Template, puedes renderizarlo con múltiples contextos, por ejemplo:

>>> from django.template import Template, Context
>>> t = Template('Hello, {{ name }}')
>>> print t.render(Context({'name': 'John'}))
Hello, John
>>> print t.render(Context({'name': 'Julie'}))
Hello, Julie
>>> print t.render(Context({'name': 'Pat'}))
Hello, Pat

Cuando estés usando la misma plantilla fuente para renderizar múltiples contextos como este, es más eficiente crear el objeto Template una sola vez y luego llamar a render() sobre éste muchas veces:

# Bad
for name in ('John', 'Julie', 'Pat'):
    t = Template('Hello, {{ name }}')
    print t.render(Context({'name': name}))

# Good
t = Template('Hello, {{ name }}')
for name in ('John', 'Julie', 'Pat'):
    print t.render(Context({'name': name}))

El análisis sintáctico de las plantillas de Django es bastante rápido. En segundo plano, la mayoría de los analizadores pasan con una simple llamada a una expresión regular corta. Esto es un claro contraste con el motor de plantillas de XML, que incurre en la excesiva actividad de un analizador XML, y tiende a ser órdenes de magnitud más lento que el motor de renderizado de Django.

4.2.4. Búsqueda del contexto de una variable

En los ejemplos dados hasta el momento, pasamos valores simples a los contextos — en su mayoría cadena de caracteres, más un datetime.date. Sin embargo, el sistema de plantillas maneja elegantemente estructuras de datos más complicadas, como listas, diccionarios y objetos personalizados.

La clave para recorrer estructuras de datos complejas en las plantillas de Django ese el carácter punto (.). Usa un punto para acceder a las claves de un diccionario, atributos, índices o métodos de un objeto.

Esto es mejor ilustrarlos con algunos ejemplos. Por ejemplo, imagina que pasas un diccionario de Python a una plantilla. Para acceder al valor de ese diccionario por su clave, usa el punto:

>>> from django.template import Template, Context
>>> person = {'name': 'Sally', 'age': '43'}
>>> t = Template('{{ person.name }} is {{ person.age }} years old.')
>>> c = Context({'person': person})
>>> t.render(c)
'Sally is 43 years old.'

De forma similar, los puntos te permiten acceder a los atributos de los objetos. Por ejemplo, un objeto de Python datetime.date tiene los atributos year, month y day, y puedes usar el punto para acceder a ellos en las plantillas de Django:

>>> from django.template import Template, Context
>>> import datetime
>>> d = datetime.date(1993, 5, 2)
>>> d.year
1993
>>> d.month
5
>>> d.day
2
>>> t = Template('The month is {{ date.month }} and the year is {{ date.year }}.')
>>> c = Context({'date': d})
>>> t.render(c)
'The month is 5 and the year is 1993.'

Este ejemplo usa una clase personalizada:

>>> from django.template import Template, Context
>>> class Person(object):
...     def __init__(self, first_name, last_name):
...         self.first_name, self.last_name = first_name, last_name
>>> t = Template('Hello, {{ person.first_name }} {{ person.last_name }}.')
>>> c = Context({'person': Person('John', 'Smith')})
>>> t.render(c)
'Hello, John Smith.'

Los puntos también son utilizados para llamar a métodos sobre los objetos. Por ejemplo, cada cadena de caracteres de Python tiene el métodos upper() y isdigit(), y puedes llamar a estos en las plantillas de Django usando la misma sintaxis de punto:

>>> from django.template import Template, Context
>>> t = Template('{{ var }} -- {{ var.upper }} -- {{ var.isdigit }}')
>>> t.render(Context({'var': 'hello'}))
'hello -- HELLO -- False'
>>> t.render(Context({'var': '123'}))
'123 -- 123 -- True'

Nota que no tienes que incluir los paréntesis en las llamadas a los métodos. Además, tampoco es posible pasar argumentos a los métodos; sólo puedes llamar los métodos que no requieran argumentos. (Explicaremos esta filosofía luego en este capítulo).

Finalmente, los puntos también son usados para acceder a los índices de las listas, por ejemplo:

>>> from django.template import Template, Context
>>> t = Template('Item 2 is {{ items.2 }}.')
>>> c = Context({'items': ['apples', 'bananas', 'carrots']})
>>> t.render(c)
'Item 2 is carrots.'

Los índices negativos de las listas no están permitidos. Por ejemplo, la variable {{ items.-1 }} causará una TemplateSyntaxError.

Nota Las listas de Python comienzan en cero, entonces el primer elemento es el 0, el segundo es el 1 y así sucesivamente.

La búsqueda del punto puede resumirse como esto: cuando un sistema de plantillas encuentra un punto en una variable, éste intenta la siguiente búsqueda, en este orden:

  • Diccionario (por ej. foo["bar"])
  • Atributo (por ej. foo.bar)
  • Llamada de método (por ej. foo.bar())
  • Índice de lista (por ej. foo[bar])

El sistema utiliza el primer tipo de búsqueda que funcione, lo que se denomina "la lógica de cortocircuito".

Los puntos pueden ser anidados a múltiples niveles de profundidad. El siguiente ejemplo usa {{ person.name.upper }}, el que se traduce en una búsqueda de diccionario (person['name']) y luego en una llamada a un método (upper()):

>>> from django.template import Template, Context
>>> person = {'name': 'Sally', 'age': '43'}
>>> t = Template('{{ person.name.upper }} is {{ person.age }} years old.')
>>> c = Context({'person': person})
>>> t.render(c)
'SALLY is 43 years old.'

4.2.4.1. Comportamiento de la llamada a los métodos

La llamada a los métodos es ligeramente más compleja que los otros tipos de búsqueda. Aquí hay algunas cosas a tener en cuenta:

  • Si, durante la búsqueda de método, un método provoca una excepción, la excepción será propagada, a menos que la excepción tenga un atributo silent_variable_failure cuyo valor sea True. Si la excepción tiene el atributo silent_variable_failure, la variable será renderizada como una cadena de texto vacía, por ejemplo:
>>> t = Template("My name is {{ person.first_name }}.")
>>> class PersonClass3:
...     def first_name(self):
...         raise AssertionError, "foo"
>>> p = PersonClass3()
>>> t.render(Context({"person": p}))
Traceback (most recent call last):
...
AssertionError: foo

>>> class SilentAssertionError(AssertionError):
...     silent_variable_failure = True
>>> class PersonClass4:
...     def first_name(self):
...         raise SilentAssertionError
>>> p = PersonClass4()
>>> t.render(Context({"person": p}))
"My name is ."
  • La llamada a un método funcionará sólo si el método no requiere argumentos. En otro caso, el sistema pasará a la siguiente búsqueda de tipo (índice de lista).
  • Evidentemente, algunos métodos tienen efectos secundarios, por lo que sería absurdo, en el mejor de los casos, y posiblemente un agujero de seguridad, permitir que el sistema de plantillas tenga acceso a ellos.

Digamos, por ejemplo, tienes objeto BankAccount que tiene un método delete(). Una plantilla no debería permitir incluir algo como {{ account.delete }}.

Para prevenir esto, asigna el atributo alters_data de la función en el método:

def delete(self):
    # Delete the account
delete.alters_data = True

El sistema de plantillas no debería ejecutar cualquier método marcado de este modo. En otras palabras, si una plantilla incluye {{ account.delete }}, esta etiqueta no ejecutará el método delete(). Este fallará silenciosamente.

4.2.4.2. ¿Cómo se manejan las variables inválidas?

Por omisión, si una variable no existe, el sistema de plantillas renderiza este como un string vacío, fallando silenciosamente, por ejemplo:

>>> from django.template import Template, Context
>>> t = Template('Your name is {{ name }}.')
>>> t.render(Context())
'Your name is .'
>>> t.render(Context({'var': 'hello'}))
'Your name is .'
>>> t.render(Context({'NAME': 'hello'}))
'Your name is .'
>>> t.render(Context({'Name': 'hello'}))
'Your name is .'

El sistema falla silenciosamente en vez de levantar una excepción porque intenta ser flexible a los errores humanos. En este caso, todas las búsquedas fallan porque los nombres de las variables, o su capitalización es incorrecta. En el mundo real, es inaceptable para un sitio web ser inaccesible debido a un error de sintaxis tan pequeño.

Ten en cuenta que es posible cambiar el comportamiento por omisión de Django en este sentido, ajustando la configuración de Django. Discutiremos esto más adelante en el Capítulo 10.

4.2.5. Jugando con objetos Context

La mayoría de la veces, instancias un objeto Context pasando un diccionario completamente poblado a Context. Pero puedes agregar y quitar elementos de un objeto Context una vez que éste está instanciado, también, usando la sintaxis estándar de los diccionarios de Python:

>>> from django.template import Context
>>> c = Context({"foo": "bar"})
>>> c['foo']
'bar'
>>> del c['foo']
>>> c['foo']
''
>>> c['newvariable'] = 'hello'
>>> c['newvariable']
'hello'