Inici de sessió d'usuari


Capítol 10: Estenent el motor de plantilles


La majoria de la teva interacció amb el llenguatge de plantilles de Django segurament hauran estat des del rol de l'autor de la plantilla. Aquest capítol aprofundeix dins el nucli del sistema de plantilles de Django; llegeix-lo si et cal estendre el sistema de plantilles, o si tens curiositat sobre com treballa internament.

Si penses en utilitzar el sistema de plantilles de Django com a part d'una altra aplicació -- p.e., sense la resta del marc-de-treball -- assegura't de llegir la secció de configuració al final del document.

La base

Una plantilla és un document de text, o una cadena normal de Python, que te un sistema de marques del llenguatge de plantilles de Django. Una plantilla pot contenir etiquetes de bloc o variables.

Una etiqueta de bloc és un símbol dins una plantilla que fa quelcom.

Aquesta definició és deliberadament vaga. Per exemple, una etiqueta de bloc pot fer sortir contingut, servir com una estructura de control (un sentència "if" o un bucle "for"), agafen el contingut des d'una base de dades o activen l'accés a altres etiquetes de plantilla.

Les etiquetes de blocs estan envoltats per {% i %}:

{% if is_logged_in %}
  Thanks for logging in!
{% else %}
  Please log in.
{% endif %}

Una variables és un símbol dins una plantilla que retorna un valor.

Les etiquetes de variable estan envoltades de {{ i }}:

My first name is {{ first_name }}. My last name is {{ last_name }}.

Un context és un assignació "nom" -> "valor" (semblant al diccionari de Python) que es passa a la plantilla.

Una plantilla imprimeix un context per substituir els "forats" de la variable amb els valors del context i executant totes les etiquetes de bloc.

Utilitzant l'objecte plantilla

A baix nivell, utilitzant el sistema de plantilles amb Python és un procés de dos passos:

  • Primer, compila el codi cru de la plantilla dins un objecte Template.
  • Després, crida al mètode render() de l'objecte Template amb un determinat context.

Compilant una cadena de text

La forma més senzilla per crear un objecte Template és instanciant-lo directament. El contructor agafa un argument -- el codi de la plantilla:

>>> from django.template import Template
>>> t = Template("My name is {{ my_name }}.")
>>> print t
<django.template.Template object at 0x1150c70>
Darrera l'escenari
El sistema només analitza el teu codi de plantilla un cop -- quan crees l'objecte Template. A part d'aquí, el guarda internament com una estructura de "node" per rendiment.
Fins i tot el propi anàlisi és molt ràpid. La majoria de l'anàlisi succeeix via una única crida a una única i curta expressió regular.

Imprimint un context

Un cop has compilat l'objecte Template, pots imprimir un context -- o molts contextos -- amb ella. El constructor Context agafa un argument (opcional): un diccionari d'assignacions de noms de variables a valors de variables.

Crida el mètode render() de l'objecte Template amb el context per "omplir" la plantilla:

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

>>> c = Context({"my_name": "Adrian"})
>>> t.render(c)
"My name is Adrian."

>>> c = Context({"my_name": "Dolores"})
>>> t.render(c)
"My name is Dolores."

Els noms de les variables han de consistir en qualsevol lletra (A-Z), qualsevol dígit (0-9), un guió baix o un punt.

Els punts tenen un significat especia en la impressió de la plantilla. Un punt en un nom de variable significa operació de cerca. Específicament, quan el sistema de plantilla trobar un punt en un nom de variable, intenta un varietat d'opcions possibles. Per exemple, la variables {{ foo.bar }} podria espandir-se a qualsevol de les següents:

  • Diccionari: foo['bar']
  • Atribut: foo.bar
  • Mètode: foo.bar()
  • Llista: foo[2]

El sistema de plantilles utilitza la primera operació de cerca que funcioni; és una lògica del camí més curt.

Aquí hi ha uns quants exemples:

>>> from django.template import Context, Template
>>> t = Template("My name is {{ person.first_name }}.")

>>> d = {"person": {"first_name": "Joe", "last_name": "Johnson"}}
>>> t.render(Context(d))
"My name is Joe."

>>> class Person:
...     def __init__(self, first_name, last_name):
...         self.first_name, self.last_name = first_name, last_name
...
>>> p = Person("Ron", "Nasty")
>>> t.render(Context({"person": p}))
"My name is Ron."

>>> class Person2:
...     def first_name(self):
...         return "Samantha"
...
>>> p = Person2()
>>> t.render(Context({"person": p}))
"My name is Samantha."

>>> t = Template("The first stooge in the list is {{ stooges.0 }}.")
>>> c = Context({"stooges": ["Larry", "Curly", "Moe"]})
>>> t.render(c)
"The first stooge in the list is Larry."

Els mètodes de les operacions de cerca són lleugerament més complexes que els altres tipus d'operacions de cerca. Aquí hi ha algunes coses que cal tenir present:

  • Si, durant el mètode de cerca, un mètode envia una excepció, l'excepció seria propagar-la excepte que tingui un atribut silent_variable_failure amb el valor a True.

    Si l'excepció no té aquest atribut, la variable s'imprimirà com una cadena buida.

    Per exemple:

    >>> t = Template("My name is {{ person.first_name }}.")
    
    >>> class Person3:
    ...     def first_name(self):
    ...         raise AssertionError("foo")
    ...
    >>> p = Person3()
    >>> t.render(Context({"person": p}))
    Traceback (most recent call last):
    ...
    AssertionError: foo
    
    >>> class SilentAssertionError(AssertionError):
    ...     silent_variable_failure = True
    ...
    >>> class Person4:
    ...     def first_name(self):
    ...         raise SilentAssertionError("foo")
    ...
    >>> p = PersonClass4()
    >>> t.render(Context({"person": p}))
    "My name is ."
    

    Fixa't que django.core.exceptions.ObjectDoesNotExist, que és la classe base de totes les excepcions DoesNotExist de la API Django, té silent_variable_failure = True. Així si utilitzes les plantilles Django amb els objectes model de Django, qualsevol excepció DoesNotExist serà silenciosa.

  • Una crida a mètode només funciona si el mètode no li calen arguments. Si no, el sistema anirà al pròxim tipus d'operació (la llista anterior).
  • Obviament, algunes mètodes tenen efectes col·laterals, i seria una ximpleria o un forat de seguretat permetre al sistema de plantilles accedir-hi.

    Un bon exemple és el mètode delete() de cada objecte model de Django. El sistema de plantilles no tindria permisos per fer alguna cosa com aquesta:
    I will now delete this valuable data. {{ data.delete }}
    

    Per evitar això, posa un atribut de funció alters_data en el mètode. El sistema de plantilles no podrà executar un mètode si el mètode té activat el alters_data=True:

    def sensitive_function(self):
        self.database_record.delete()
    sensitive_function.alters_data = True
    

    Els mètodes generats dinàmicament delete() i save() dels objectes del model Django tenen el alters_data=True automàticament, per exemple.

Com es tracten les variables invàlides

Generalment, si una variable no existeix, el sistema de plantilles inserta el valor del paràmetre de configuració TEMPLATE_STRING_IF_INVALID, que per defecte té el valor d'una cadena buida.

Els filtres que s'apliquen a una variable invàlida només seran aplicats si TEMPLATE_STRING_IF_INVALID té el seu valor per defecte. Si TEMPLATE_STRING_IF_INVALID val qualsevol altre valor, els filtres de les variables seran ignorats.

Aquesta característiques són lleugerament diferents per les etiquetes de plantilla if, for i regroup. Si una variable invàlida és proporcionada a una d'aquestes etiquetes de plantilla, la variable s'interpretarà com a None. Els filtres sempre són per variables invàlides dins d'aquestes etiquetes plantilles.

Jugant amb els objectes Context

La majoria de les vegades, instanciaràs objectes Context passant-lo com un diccionari complet a Context(). Però pots afegir i esborrar elements de l'objecte Context un cop ha estat instanciat, també, utilitzant la sintaxi del diccionari estàndard:

>>> c = Context({"foo": "bar"})
>>> c['foo']
'bar'
>>> del c['foo']
>>> c['foo']
''
>>> c['newvariable'] = 'hello'
>>> c['newvariable']
'hello'

A més, un objecte Context actua com una pila. Això és, pots afegit (push()) i extreure (pop()) contextes addicionals dins de la pila. Totes les operacions succeeixen a la part alta de la pila, però les operacions de cerca a la pila es fan des de dalt a baix fins que es trobi el valor.

Si extreus (pop()) masses, aconseguiràs una django.template.ContextPopException.

Aquí hi ha un exemple de com aquests múltiples nivells treballen:

# Create a new blank context and set a simple value:
>>> c = Context()
>>> c['foo'] = 'first level'

# Push a new context onto the stack:
>>> c.push()
>>> c['foo'] = 'second level'

# The value of "foo" is now what we set at the second level:
>>> c['foo']
'second level'

# After popping a layer off, the old value is still there:
>>> c.pop()
>>> c['foo']
'first level'

# If we don't push() again, we'll overwrite existing values:
>>> c['foo'] = 'overwritten'
>>> c['foo']
'overwritten'

# There's only one context on the stack, so pop()ing will fail:
>>> c.pop()
Traceback (most recent call last):
...
django.template.ContextPopException

Utilitzant un Context com a pila pot ser interesant en algunes etiquetes de plantilla personalitzada, com veuràs posteriorment.

RequestContext i processadors de context

Django ve amb una classe Context especial, django.template.RequestContext, que actua lleugerament diferents que el django.template.Context normal. La primera diferència és que pren un objecte HttpRequest (mireu el Capítol XXX) com a primer argument:

c = RequestContext(request, {
    'foo': 'bar',
}

La segona diferència és que aquest automàticament publica el context amb uns quantes variables, d'acord amb el paràmetre de configuració TEMPLATE_CONTEXT_PROCESSORS.

El paràmetre TEMPLATE_CONTEXT_PROCESSORS és una tupla de crides anomenades processadors de context que agafen una objecte petició com a argument i retornen un diccionari d'elements que poden ser combinat dins del context. Per defecte, aquest paràmetre de configuració TEMPLATE_CONTEXT_PROCESSORS val:

("django.core.context_processors.auth",
 "django.core.context_processors.debug",
 "django.core.context_processors.i18n")

Cada processador s'aplica en ordre. És a dir, si un processador afegeix una variable en el context i el segon processador afegeix una variable amb el mateix nom, el segon sobre-escriurà al primer. Els processadors que venen per defecte s'expliquen després.

També, pots donar al RequestContext una llista de processadors addicionals, utilitzant el tercer argument opcional, processors. En aquest exemple, la instància RequestContext obté una variable ip_address:

def ip_address_processor(request):
    return {'ip_address': request.META['REMOTE_ADDR']}

def some_view(request):
    # ...
    return RequestContext(request, {
        'foo': 'bar',
    }, processors=[ip_address_processor])

Aquí s'explica el que fa cada processador:

django.core.context_processors.auth

Si TEMPLATE_CONTEXT_PROCESSORS conté aquest processador, cada RequestContext contindrà aquestes tres variables

user
Una instància django.contrib.auth.models.User representant l'usuari actualment identificat (o una instància de AnonymousUser, si el client no està identificat).

messages
Una llista de missatges (com a cadenes) per l'usuari actualment identificat. Darrera de les escenes, aquest crida request.user.get_and_delete_messages() per cada petició. Aquest mètode recol·lecta els missatges de l'usuari i els esborra de la base de dades.

Fixa't que els missatges es creen amb user.add_message().

perms
Una instància de django.core.context_processors.PermWrapper, representant els permisos que l'usuari té.

Mira el Capítol per saber-ne més sobre els usuaris, permisos, i missatges.

django.core.context_processors.debug

Aquest processador posa informació de depuració sota la capa de la plantilla. Si està activar, només funcionarà si:

  • El paràmetre DEBUG és True, i
  • la petició ve des d'una adreça IP que sigui dins del paràmetre INTERNAL_IPS.

Si aquestes condicions són certes, els següents variables estaràn activades:

debug
Posat a True; pots utilitzar això en les plantilles per comprovar si estàs en mode DEBUG.

sql_queries
Una llista de diccionaris {'sql': ..., 'time': ...}, representant cada consulta SQL que s'ha realitzat durant la petició i quant de temps ha transcorregut. La llista està en l'ordre de les consultes.

django.core.context_processors_i18n

Si aquest processador està activat, cada RequestContext contindrà aquestes dues variables:

LANGUAGES
El valor del paràmetres LANGUAGES

LANGUAGE_CODE
request.LANGUAGE_CODE, si existeix. Altrament, el valor del paràmetres LANGUAGE_CODE.

L'Apèndix XXX té més informació sobre aquests dos paràmetres.

django.core.context_processors.request

Si està activat, cada RequestContext contindrà una variable request, que és l'objecte HttpRequest actual. Fixa't que aquest processador no està activat per defecte; l'hauràs d'activar.

Carregant plantilles

Normalment, guardes plantilles en fitxers del sistema de fitxers (o en altres llocs si has escrit carregadors de plantilles personalitzats) en comptes d'usar la pròpia API de baix nivell Template.

Django cerca pels directoris de plantilles en un nombre de llocs, depenent del teu paràmetre de configuració template-loader (mira més avall "Tipus de Carrgadors"), però la forma més bàsica d'especificar els directoris de plantilles és utilitzant el paràmetre TEMPLATE_DIRS.

Aquest podria ser una llista o tupla de cadenes de caràcters que conté els camins complerts dels teus directoris de plantilles:

TEMPLATE_DIRS = (
    "/home/html/templates/lawrence.com",
    "/home/html/templates/default",
)

Les plantilles poden ser allí on vulguis, mentre siguin directoris que puguin llegir-se pel servidor Web. Poden tenir qualsevol extensió, com .html o .txt o poden no tenir cap extensió.

Fixa't que aquests camins haurien s'usar-se les barres de l'style Unix, fins i tot dins a Windows.

La API Python

Django té dos formes de carregar les plantilles des dels fitxers:

django.template.loader.get_template(template_name)
get_template retorna la plantilla compilada (un objecte Template) per la plantilla amb el nom indicat. Si la plantilla no existeix, salta una excepció django.template.TemplateDoesNotExist.

django.template.loader.select_template(template_name_list)
select_template és com get_template, excepte que pren una llista de noms de plantilles. De la llista, retorna la primera plantilla que existeixi.

Per exemple, si crides a get_template('story_detail.html') i té el paràmetre TEMPLATE_DIRS anterior, aquests seran els fitxers que Django buscarà, per ordre:

  • /home/html/templates/lawrence.com/story_detail.html
  • /home/html/templates/default/story_detail.html

Si crides a select_template(['story_253_detail.html', 'story_detail.html']), així és com Django la buscarà:

  • /home/html/templates/lawrence.com/story_253_detail.html
  • /home/html/templates/default/story_253_detail.html
  • /home/html/templates/lawrence.com/story_detail.html
  • /home/html/templates/default/story_detail.html

Quan Django troba una plantilla que existeix, atura la cerca.

extrem
Pots utilitzar select_template() per la super-flexible" "plantillabilitat". Per exemple, si estàs escrivint unes noticies i vols que algunes històries tinguin plantilles personalitzades, utilitza quelcom com això: select_template(['story_%s_detail.html' % story.id, 'story_detail.html']). Que et permetrà utilitzar una plantilla personalitzada per a una història individual, amb una plantilla per defecte per a històries que no tenen plantilles personalitzades.

Utilitzant subdirectoris

És possible -- i preferible -- organitzar plantilles en subdirectoris del directori plantilla. La convenció és crear un subdirectori de cada aplicació Django, amb subdirectoris dins aquests subdirectoris que es necessitin.

Fer això per la teva pròpia salut. Guardar totes les plantilles en el nivell arrel d'un únic directori és brut.

Per carregar una plantilla que és dins un subdirectori, només has d'usar una barra, així:

get_template('news/story_detail.html')

Utilitzant el mateix paràmetre TEMPLATE_DIRS anterior, aquest exemple de crida get_template() intentaria carregar les següents plantilles:

  • /home/html/templates/lawrence.com/news/story_detail.html
  • /home/html/templates/default/news/story_detail.html

Carregadors de Plantilles

Per defecte, Django carrega les plantilles del sistema de fitxers, però Django ve amb uns quants carregadors de plantilles que saben com carregar plantilles d'altres orígens.

Alguns d'aquests altres carregadors estan desactivats per defecte, però pots activar-los editant el teu paràmetre de configuració TEMPLATE_LOADERS. TEMPLATE_LOADERS ha de ser una tupla de cadenes, on cada cadena representa un carregador de plantilles. Els carregadors de plantilles que van amb Django són:

django.template.loaders.filesystem.load_template_source
Carrega les plantilles des del sistema de fitxers, d'acord amb el paràmetre TEMPLATE_DIRS.
Aquest carregador ve activar per defecte.

django.template.loaders.app_directories.load_template_source
Carrega plantilles des de les aplicacions de Django en el sistema de fitxers. Per cada aplicació a INSTALLED_APPS, el carregador busca en el subdirectori templates. Si el directori existeix, Django hi busca les plantilles.

Això vol dir que pots guardar plantilles dins de les aplicacions individual. Això fa que sigui fàcil distribuir aplicacions Django amb plantilles per defecte.

Per exemple, if INSTALLED_APPS conté ('myproject.polls', 'myproject.music'), llavors get_template('foo.html') buscarà les plantilles en aquest ordre:

  • /path/to/myproject/polls/templates/foo.html
  • /path/to/myproject/music/templates/foo.html

Fixa't que el carregador processa una optimització quan és el primer importat: Aquest guarda en memòria una llista de quins paquets INTALLED_APPS tenen el subdirectori templates.

Aquest carregador ve activat per defecte.

django.template.loaders.eggs.load_template_source
Com l'anterior app_directories, però aquest carrega plantilles de les ous de Python en comtes de des del sistema de fitxers.

Aquest carregador ve desactivat per defecte; necessitaràs activar si estàs utilitzant ous per distribuir la teva aplicació.

Django utilitza els carregadors de plantilla en l'odre del paràmetre TEMPLATE_LOADERS. Aquest utilitza cada carregador fins que es troba la plantilla.

Estendre el sistema de plantilles

Encara que el llenguatge de plantilles Django ve amb moltes etiquetes i filtres per defecte, pots voler escriure les teves pròpies, i és fàcil de fer-ho.

Primer, crea un paquet templatetags en el paquet apropiat de l'aplicació Django. Aquest hauria d'estar en el mateix nivell que moldes.py, views.py, etc. Per exemple:

polls/
    models.py
    templatetags/
    views.py

Afegeix dos fitxers al paquet templatetags: un fitxer __init__.py (per indicar a Python que aquest és un mòdul que conté codi Python) i un fitxer que contindrà les teves pròpies etiquetes i filtres personalitzats.

El nom de l'últim fitxer és el nom que utilitzarà per carregar després les etiquetes. Per exemple, si les teves etiquetes i filtres personalitzats estan en el fitxer anomenat poll_extras.py, hauràs de fer el següent en la plantilla:

{% load poll_extras %}

L'etiqueta {% load %} mira si el paràmetre INSTALLED_APPS i només permet la càrrega de les llibreries de plantilla dins de les aplicacions instal·lades a Django. Això és una característica de seguretat: Això et permet hostatjar codi Python de moltes llibreries de plantilles en un únic ordinador sense activar l'accés des de totes les instal·lacions Django.

Si escrius una llibreria de plantilles que no està lligada a cap vista o model en particular, és perfectament correcta tenir un paquet d'aplicació Django que només contingui un paquet templatetags.

No hi ha cap límit en quants mòduls pots posar en el paquet templatetags. Només tingues present que una sentència carregarà els filtres i etiquetes del nom del mòdul Python indicat, no del nom de l'aplicació.

Un cop has creat aquest mòdul Python, només hauràs d'escriure un mica de codi Python, depenent de si has escrit filtres o etiquetes.

Per tenir una llibreria d'etiquetes vàlides, el mòdul conté una variable de nivell de mòdul anomenada register que és una instància template.Library, en que les etiquetes i filtres estan registrats. Així, prop de l'inici del mòdul, poseu el següent:

from django import template

register = template.Library()
Darrera l'escenari
Per a un munt d'exemples, llegeix el codi font dels filtres i etiquetes per defecte de Django. Són a django/template/defaultfilters.py i django/template/defaulttags.py, respectivament.

Les aplicacions de django.contrib també contenen numerosos exemples.

Escrivint filtres de plantilla personalitzats

Els filtres personalitzats són només funcions Python que agafen un o dos arguments:

  • El valor de la variable (entrada)
  • El valor de l'argument, que aquest pot ser un valor per defecte.

Per exemple, en el filtre {{ var|foo:"bar" }}, el filtre foo li seria passada la variable var i l'argument "bar".

Les funcions filtres sempre haurien de retornar alguna cosa. No haurien de generar excepcions i si passa alguna cosa, haurien de ser silenciones. Si hi ha un error, haurien de retornar l'entrada original o una cadena buida -- o alguna cosa amb més sentit.

Aquí hi ha un exemple de definició de filtre:

def cut(value, arg):
    "Removes all values of arg from the given string"
    return value.replace(arg, '')

I aquí hi ha un exemple de com aquest filtre s'ha d'usar:

{{ somevariable|cut:"0" }}

La majoria dels filtres no agafen arguments. En aquest cas, només cal que deixis l'argument fora de la funció:

def lower(value): # Only one argument.
    "Converts a string into all lowercase"
    return value.lower()

Quan has escrit la teva funció de filtre, necessites registrar-la amb la teva instància Library, per fer-la accessible al llenguatge de plantilles de Django:

register.filter('cut', cut)
register.filter('lower', lower)

El mètode Library.filter() pren dos arguments:

  1. El nom del filtre (una cadena de text)
  2. La funció compilada (una funció Python, no el nom de la funció)

Si estàs usant Python 2.4 o major, pots usar register.filter() com un decorador:

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()

Si no poses l'argument name, com en el segon exemple, Django usarà el nom de la funció com a nom de filtre.

Escrivint etiquetes de plantilla personalitzades

Les etiquetes són més complexes que els filtres, perquè les etiquetes poden qualsevol cosa.

Una petita introducció

Abans, aquest capítol descrivia com les sistema de plantilles funciona en dos passos: compilant i imprimint. Per definir una etiqueta de plantilla personalitzada, necessites dir-li a Django com gestionar ambdós passos quan agafa la teva etiqueta.

Quan Django compila una plantilla, separa el text de la plantilla dins de "nodes". Cada node és una instància de django.template.Node i té un mètode render(). Així, una plantilla compilada és simplement un llistat d'objecte Node

Quan crides render() en una plantilla compilada, la plantilla crida el render() de cada Node del llistat de nodes, que té el context. Els resultats són tots els concatenats junts de al formulari de sortida de la plantilla.

Així, per definir una etiqueta de plantilla personalitzada, especifica com l'etiqueta de la plantilla es converteix dins un Node (la funció de compilació), i el mètode render() què fa.

Escrivint la funció de compilació

Per cada etiqueta de plantilla que l'analitzador de plantilla troba, crida una funció Python amb el contingut de l'etiqueta i el propi objecte analitzador. Aquest funció és la responsable de retornar una instància Node basada en els continguts de l'etiqueta.

Per exemple, escriu l'etiqueta {% current_time %}, que mostra la data i hora actual, formatada d'acord a un paràmetres donat per l'etiqueta, amb la sintaxi strftime (mira http://www.python.org/doc/current/lib/module-time.html#l2h-1941). És bona idea decidir la sintaxi de l'etiqueta abans de qualsevol altra cosa. En el nostre cas, diguem que l'etiqueta s'utilitzarà així:

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>
Nota
Si, aquesta etiqueta de plantilla és redundant; l'etiqueta {% now %} per defecte de Django fa la mateixa tasca amb una sintaxi més simple. Només és un exemple.

L'analitzador d'aquesta funció hauria de gravar el paràmetre i crear un objecte 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:
        raise template.TemplateSyntaxError("%r tag requires a single argument" % token.contents[0])
    return CurrentTimeNode(format_string[1:-1])

Realment, aquí hi ha molta cosa:

  • El parser és l'objecte analitzador de la plantilla. No el necessitem en aquest exemple.
  • token.contents és una cadena del contingut de l'etiqueta. En el nostre exemple seria 'current_time "%Y-%m-%d %I:%M %p"'.
  • El mètode token.split_contents() separa els arguments en espais mantenint junts les cadenes entre cometes. Un token.contents.split() més directe no seria tant robust, ja que separaria tots els espais, inclosos els que hi ha dins de les comentes. És bona idea utilitzar sempre token.split_contents().
  • Aquesta funció es responsabilitza d'aixecar un django.template.TemplateSyntaxError, amb missatges d'ajuda, si hi ha un error de sintaxi.
  • No codifiquis el nom de l'etiqueta en els teus missatges d'error, perquè això ja ho potser fer amb la funció token.contents.split[0] que sempre serà el nom de la teva etiqueta -- fins hi tot quan no hi hagi arguments.
  • La funció retorna un CurrentTimeNode (el qual crearem després) que conté tot el que el node necessita saber sobre aquesta etiqueta. En aquest cas, només se li passa un argument -- "%Y-%m-%d %I:%M %p". La capçalera i les cometes de la plantilla s'han eliminat amb format_string[1:-1].
  • Les funcions de compilació d'etiquetes de plantilla han de retornar una subclasse de Node; qualsevol altre valors serà un error
  • L'analitzador és de molt baix nivell. Hem experimentat escrivint unes petites frameworks a sobre d'aquest sistema d'anàlisi però aquests experiments fan el motor de plantilla massa lent. El baix nivell és més ràpid.

Escrivint el node plantilla

El segon pas en la creació d'etiquetes personalitzades és definir una subclasse de Node que té un mètode render(). Continuant amb l'exemple anterior, necessitem definir CurrentTimeNode:

import datetime

class CurrentTimeNode(template.Node):

    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        return datetime.datetime.now().strftime(self.format_string)

Les dues funcions (__init__ i render) equivalen directament als dos passos del processat de plantilla (compilació i impressió). Així, la funció d'inicialització només cal per guardar la cadena de format per usar més tard, i la funció render() fa la feina real.

Com en els filtres de plantilla, aquestes funció d'impressió haurien de fallar de forma silenciosa si hi ha algun error. L'únic moment que es poden generar errors és durant la compilació.

Registrant l'etiqueta

Finalment, necessites registrar l'etiqueta amb la tema instància Library del mode, com s'ha explicat anteriorment a "Escrivint filtres de plantilla":

register.tag('current_time', do_current_time)

El mètode tag() pren dos arguments:

  1. El nom de l'etiqueta de la plantilla (cadena de text). Si s'omet, s'utilitzarà el nom de la funció de compilació.
  2. La funció de compilació.

Com amb el registre del filtre, també és possible utilitzar aquesta com un decorador a Python 2.4 en endavant:

@register.tag(name="current_time")
def do_current_time(parser, token):
    # ...

@register.tag
def shout(parser, token):
    # ...

Si deixes l'argument name, com en el segon exemple anterior, Django utilitzarà el nom de la funció com a nom de la etiqueta.

Establint una variable en el context

Els exemples anteriors simplement treuen un valor. Sovint és útil establir variables de plantilla en comptes de valors de sortida. D'aquesta forma, els autors de les plantilles poden simplement usar els valors que han creat les teves etiquetes de plantilla.

Per establir una variable en el context, només cal que usis l'assignament de diccionari sobre l'objecte del context en el mètode render(). Aquí hi ha una versió actualitzada de CurrentTimeNode que activa una variable de plantilla current_time en comptes de treure el resultat:

class CurrentTimeNode2(template.Node):

    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        context['current_time'] = datetime.datetime.now().strftime(self.format_string)
        return ''

Fixa't que render() retornar una cadena buida; render() sempre ha de retornar una cadena, si totes les etiquetes de plantilla només activen una variable, render() hauria de retornar un cadena buida.

Aquí es veu com s'utilitza la nova versió de l'etiqueta:

{% current_time "%Y-%M-%d %I:%M %p" %}
<p>The time is {{ current_time }}.</p>

Però, hi ha un problema amb CurrentTimeNode2: el nom de variable current_time està codificat en el codi. Això significa que necessites estar segur que la teva plantilla no usa {{ current_time }} enlloc més, perquè el {% current_time %} sobre-escriure el valor de la variable.

Una solució més neta és fer una etiqueta de plantilla específica el nom de la variable de sortida, així:

{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>

Per fer això, necessitaràs recodificar tant la funció de compilació i la classe Node, així:

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):
        context[self.var_name] = datetime.datetime.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:
        raise template.TemplateSyntaxError("%r tag requires arguments" % token.contents[0])

    m = re.search(r'(.*?) as (\w+)', arg)
    if m:
        format_string, var_name = m.groups()
    else:
        raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)

    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name)

    return CurrentTimeNode3(format_string[1:-1], var_name)

Ara, do_current_time() grava la cadena de format i el nom de la variable, passant ambdós paràmetres a CurrentTimeNode3.

Analitzant fin a una altra etiqueta de bloc

Les etiquetes de plantilla funcionen com blocs que contenen altres etiquetes. Per exemple, l'etiqueta estàndard {% comment %} amaga tot el que troba fins {% endcomment %}.

Per crear una etiqueta de plantilla com aquesta, utilitza parser.parse() en la teva funció de compilació.

Aquí es mostra com està implementada l'etiqueta estàndard {% comment %}:

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() agafa una tupla de noms d'etiquetes de bloc per analitzar-les fins allí. Retorna una instància de django.template.NodeList, que és una llista de tots els objectes Node que l'analitzador ha trobat abans de qualsevol de les etiquetes indicades en la tupla.

Així en l'exemple anterior, nodelist és una llista de tots els nodes entre les etiquetes {% comment %} i {% endcomment %}, sense incloure els propis {% comment %} i {% endcomment %}.

Després que parser.parser() sigui cridat, l'analitzador encara no ha "consumit" l'etiqueta {% endcomment %}, així que s'ha de cridar a parser.delete_first_token() per evitar que aquesta etiqueta es torni a processar.

Després, CommentNode.render() simplement retornar una cadena buida. Qualsevol cosa entre {% comment %} i {% endcomment %} s'ignora.

Analitzant fins una altra etiqueta de bloc i guardant els continguts

En l'exemple previ, do_comment() descarta tot el que hi ha entre {% comment %} i {% endcomment %}. En comptes de fer això es possible fer quelcom amb el codi entre les etiquetes.

Per exemple, aquí hi ha una etiqueta de plantilla personalitzada {% upper %}, que passa a majúscules tot el que troba entre ella i {% endupper %}:

{% upper %}
    This will appear in uppercase, {{ your_name }}.
{% endupper %}

Com en l'exemple anterior, utilitzarem parser.upper(). Aquesta vegada, passarem el nodelist resultant a el 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()

L'únic concepte nou que hi ha és el self.nodelist.render(context) a UpperNode.render().

Per més exemples de l'impressió complexa, mireu el codi font de {% if %}, {% for %}, {% ifequal %} i {% ifchanged %}. Aquestes són a django/template/defaulttags.py.

Dreceres per a etiquetes simples

Moltes etiquetes de plantilla només agafen un únic argument -- una cadena o una referència a una variable de plantilla -- i retornen una cadena després de fer algun processat basat només en l'argument d'entrada i algunes informació externa. Per exemple, l'etiqueta current_time estaria escrita en aquesta varietat: nosaltres li donarem un format de cadena, retornant l'hora en una cadena.

Per facilitar la creació d'aquests tipus d'etiquetes, Django proporciona una funció d'ajuda, simple_tag. Aquesta funció, que és un mètode de django.template.Library, agafa una funció que accepta un argument i l'embolcalla en una funció render i en altres elements necessaris mencionats anteriorment i registrar-lo en el sistema de plantilles.

La nostra anterior funció current_time hauria d'escriure's així:

def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

register.simple_tag(current_time)

A Python 2.4, la sintaxi decorador també funciona:

@register.simple_tag
def current_time(token):
    ...

Unes quantes coses que cal fixar-se sobre la funció d'ajuda simple_tag:

  • Només un únic argument pot passar-se en la nostra funció.
  • No cal fer la comprovació del nombre d'arguments.
  • Les cometes de l'argument (si n'hi ha) ha seran eliminades, només rebrem text pla.

Inclussió d'etiquetes

Un altre tipus d'etiqueta comuna és la que mostra algunes dades imprimint una altra plantilla.

Per exemple, la interfície d'administració Django utilitza etiquetes personalitzades per mostrar els botons al final de les pàgines de formulari "afegit/canviar". Aquestes botons sempre són els mateixos, però els enllaços de destí canvien depenent de l'operació que es realitzi. És un cas perfecte per utilitzar una petita plantilla que s'omplirà amb els detalls del nostre objecte actual.

Aquests conjunt d'etiquetes s'anomenen etiquetes d'inclussió.

Escriure etiquetes d'inclussió és demostra millor amb un exemple. Escriu una etiqueta que tregui una llista de les opcions de un objecte Poll de múltiple selecció. Utilitzarem l'etiqueta així:

{% show_results poll %}

... i la sortida serà quelcom així:

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

Primer, definim la funció que agafarà l'argument i produirà un diccionari de dades com a resultat. Fixa't que només necessitem retornar un diccionari, no té cap més complexitat. Aquest s'utilitzarà com el context per al fragment de plantilla:

def show_results(poll):
    choices = poll.choice_set.all()
    return {'choices': choices}

Després, crearem la plantilla utilitzada per imprimir la sortida de l'etiqueta. seguint el nostre exemple, la plantilla és molt simple:

<ul>
{% for choice in choices %}
    <li> {{ choice }} </li>
{% endfor %}
</ul>

Finalment, crearem i registrarem l'etiqueta d'inclussió cridant el mètode inclusion_tag() de l'objecte Library.

Seguint amb el nostre objecte, si la plantilla anteriors és en un fitxer anomenat polls/result_snippet.html, registrarem l'etiqueta així:

register.inclusion_tag('polls/result_snippet.html')(show_results)

Com sempre, la sintaxi del decorador de Python 2.4 també funciona, podríem fer-ho així:

@register.inclusion_tag('results.html')
def show_results(poll):
    ...

Algunes vegades, les teves etiquetes incloses necessiten accedir al context de la plantilla pare.

Per solucionar això, Django proporciona una opció takes_context per les etiquetes d'inclusió. si especifiques takes_context en la creació de l'etiqueta de plantilla, l'etiqueta no necessitarà arguments, i la funció de sota de Python tindrà un argument -- el context de la plantilla com quan l'etiqueta s'ha cridat.

Per exemple, diguem que estàs escrivint una etiqueta d'inclusió que sempre s'usarà en un context que conté les variables home_link i home_title que apunten a la pàgina principal. Aquí veiem el que la funció de Python ha de fer:

@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àmetre de la funció ha d'anomenar-se context.

La plantilla link.html ha de contindre:

Jump directly to <a href="{{ link }}">{{ title }}</a>.

Llavors, cada cop que tu utilitzis l'etiqueta personalitzada, carregarà aquesta llibreria i la cridarà sense cap argument, així:

{% jump_link %}

Fixa't que quan utilitzes takes_context=True, no cal passar arguments a l'etiqueta de plantilla. Automàticament accedirà al context.

Escrivint carregadors de plantilla personalitzats

Els carregadors de plantilles dins de Django normalment cobreixen les necessitats de càrrega de plantilles, però és molt fàcil escriure'n el teu propi si necessites una lògica de càrrega especial.

Un carregador de plantilla -- que é, cada entrada en el paràmetre TEMPLATE_LOADERS -- s'espera que sigui un tipus cridable amb aquesta interfície:

load_template_source(template_name, template_dirs=None)

L'argument template_name és el nom de la plantilla a carregar (com passa a loader.get_template() o loader.select_template()), i template_dirs és una llista opcional de directoris per cercar en comptes de TEMPLATE_DIRS.

Si el carregador es capaç de carregar correctament una plantilla, aquest hauria de retornar una tupla: (template_source, template_path). Aquí template_source és la cadena de la plantilla que serà compilada per el motor de la plantilla, i template_path és el camí des d'on s'ha carregat la plantilla. Aquest camí podria ser mostrat a l'usuari per supòsits de depuració, així podries identificar ràpidament d'on s'ha carregat la plantilla.

Si el carregador no es capaç de carregar una plantilla, enviarà una excepció django.template.TemplateDoesNotExists.

Cada funció del carregador ha de tenir un atribut de funció is_usable en l'actual instal·lació Python.

Per exemple, el carregador d'ous (que és capaç de carregar plantilles des dels ous) posa is_usable a False si el mòdul pkg_resources no està instal·lat, perquè pck_resources és necessari per llegir informació des dels ous.

Un exemple podria ajudar a clarificar totes aquestes coses. Aquí hi ha una funció de carregador de plantilla que pot carregar plantilles des d'un fitxer ZIP. Utilitza un paràmetre personalitzat, TEMPLATE_ZIPS_FILES com un cerca de fitxers en comptes de TEMPLATE_DIRS, i espera que cada element de la llista sigui un fitxer ZIP que contingui plantilles:

import zipfile
from django.conf import settings
from django.template import TemplateDoesNotExist

def load_template_source(template_name, template_dirs=None):
    """Template loader that loads templates from a ZIP file."""

    # Lookup ZIP file list from settings if it's not already given.
    if template_zipfiles is None:
        template_zipfiles = getattr(settings, "TEMPLATE_ZIP_FILES", [])

    # Try each ZIP file in TEMPLATE_ZIP_FILES.
    for fname in template_zipfiles:
        try:
            z = zipfile.ZipFile(fname)
            source = z.read(template_name)
        except (IOError, KeyError):
            continue

        # We found a template, so return the source.
        template_path = "%s:%s" % (fname, template_name)
        return (source, template_path)

    # If we reach here, the template couldn't be loaded
    raise TemplateDoesNotExist(template_name)

# This loader is always usable (since zipfile is a Python standard library function)
load_template_source.is_usable = True

L'únic pas que falta si volem utilitzar aquest carregador és afegir-lo al paràmetre TEMPLATE_LOADERS. Si posem aquest codi en un mòdul anomenat myproject.zip_loader, llavors haurem d'afegir myproject.zip_loader.load_template_source a TEMPLATE_LOADERS.

Utilitzant els plantilles internes

La interfície d'administrador Django inclou una referència completa de totes les etiquetes de plantilla i filtres disponibles per un lloc. Està dissenyat per ser una eina que els programadors Django donen als desenvolupadors de plantilles. Per veure'ls, ves a la teva interfície d'administració i fes clic a l'enllaç de "Documentació" en la cantonada superior dreta de la pàgina.

La referència està dividida en 4 seccions: etiquetes, filtres, models i vistes.

Les seccions etiquetes i filtres descriuen totes les etiquetes internes (de fet, les referències d'etiquetes i filtres de sota venen directament d'aquestes pàgines) igual que qualsevol etiqueta i filtre personalitzat que estigui disponible.

La pàgina views és la més valuosa. Cada URL del teu lloc hi té una entrada a part, i fent clic en la URL et mostra:

  • El nom de la funció vista que genera aquesta vista.
  • Una descripció curta de que fa la vista.
  • El context, o una llista de les variables disponibles en la plantilla de la vista.
  • El nom de la plantilla o plantilles que s'usen per aquesta vista.

Cada pàgina de documentació de vista també té un preferit que pots usar per saltar des de qualsevol pàgina a la pàgina de documentació per aquesta vista.

Ja que els llocs fets amb Django normalment usen objecte de bases de dades, la secció moidels de la pàgina de documentació descriu cada tipus d'objecte del sistema amb totes els camps disponibles d'aquest objecte.

Ajuntant-ho tot, les pàgines de la documentació han de dir-te quines etiquetes, filtres, variables i objectes hi ha disponibles en una plantilla concreta.

Configurant el sistema de plantilles mode independent

Nota
Aquesta secció només és d'interès per gent que vulgui utilitzar el sistema de plantilles com a component de sortida d'una altra aplicació. si estàs usant el sistema de plantilles com a part d'una aplicació Django, res d'això no t'afecta.

Normalment, Django carregarà tota la informació de configuració que necessita des del propi fitxer de configuració, combinat amb els paràmetres del mòdul donats en la variable d'entorn DJANGO_SETTINGS_MODULE. Però si estàs usant el sistema de plantilles independentment de la resta de Django, la variable d'entorn no és massa convenient, perquè probablement voldràs configurar el sistema de plantilla en línia amb la resta de les teves aplicacions en comptes d'utilitzar fitxers de configuració i apuntar-hi via variables d'entorn.

Per solucionar aquest problema, necessites usar el manual de configuració descrit en l'Apèndix XXX.

Simplement importa les peces apropiades del sistema de plantilles i llavors, abans de cridar a les funcions de plantilla, crida a django.conf.settings.configure() amb qualsevol paràmetres que hi vulguis especificar.

Podries voler considerar els paràmetres TEMPLATE_DIRS (si vas a utilitzar els carregadors de plantilles), DEFAULT_CHARSET (encara que el valor per defecte utf-8 et funcioni) i TEMPLATE_DEBUG. Tots els paràmetres disponibles estan descrits en el Capítol XXX, i qualsevol paràmetre que comenci amb TEMPLATE_ segurament et serà d'interès.