Balises et filtres de gabarit personnalisés

Le langage de gabarits de Django offre une large palette de balises et filtres intégrés conçus pour répondre aux besoins de la logique de présentation de votre application. Néanmoins, il se peut que vous rencontriez des besoins non couverts par cet ensemble de fonctions de gabarits. Il est possible d’étendre le moteur de gabarit en définissant des balises et des filtres personnalisés en Python, puis en les rendant disponibles dans vos gabarits par la balise {% load %}.

Disposition du code

L’endroit le plus souvent utilisé pour placer des balises et filtres de gabarit personnalisés est à l’intérieur d’une application Django. S’ils sont liés à une application existante, il est logique de les y intégrer ; sinon, ils peuvent être ajoutés à une nouvelle application. Lorsqu’une application Django est ajoutée à INSTALLED_APPS, toute balise qu’elle définit à l’endroit conventionnel présenté ci-dessous est automatiquement mise à disposition du chargement depuis les gabarits.

L’application doit contenir un répertoire templatetags au même niveau que models.py, views.py, etc. S’il n’existe pas encore, créez-le, sans oublier le fichier __init__.py pour que le répertoire soit bien considéré comme un paquet Python.

Le serveur de développement ne va pas redémarrer automatiquement

Après avoir ajouté le module templatetags, vous devez redémarrer le serveur avant de pouvoir utiliser les balises ou les filtres dans les gabarits.

Vos balises et filtres personnalisés se trouveront dans un module à l’intérieur du répertoire templatetags. Le nom du fichier de module est le nom que vous utiliserez plus tard pour charger les balises, prenez donc soin de choisir un nom qui n’est pas déjà utilisé pour des balises et filtres d’une autre application.

Par exemple, si vos balises/filtres personnalisés se trouvent dans un fichier nommé poll_extras.py, la disposition des fichiers de votre application pourrait ressembler à ceci :

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

Et dans votre gabarit, voici ce qu’il faudrait écrire :

{% load poll_extras %}

L’application qui contient les balises personnalisées doit apparaître dans INSTALLED_APPS pour que la balise {% load %} fonctionne. C’est un élément de sécurité : cela permet d’accueillir du code Python de beaucoup de bibliothèques de gabarits sur une même machine sans devoir permettre l’accès à toutes ces bibliothèques pour chaque installation Django.

Il n’y a aucune limite sur le nombre de modules que l’on peut placer dans le paquet templatetags. Gardez simplement à l’esprit qu’une commande {% load %} va charger les balises/filtres du nom de module Python indiqué, et non pas du nom de l’application.

Pour être valide, le module de la bibliothèque de balises doit contenir une variable au niveau module nommée register et qui doit être une instance de template.Library, dans laquelle toutes les balises et les filtres sont inscrits. Ainsi, dans la partie supérieure de votre module, écrivez ceci :

from django import template

register = template.Library()

Il est aussi possible d’inscrire les modules de balises de gabarit au moyen du paramètre 'libraries' de DjangoTemplates. Cette méthode est utile si vous souhaitez utiliser un nom autre que celui du module de balises de gabarit au moment de charger ces balises. Cela permet aussi d’inscrire des balises sans devoir installer une application.

En coulisses

Pour de multiples exemples, lisez le code source des filtres et balises par défaut de Django. Vous les trouverez respectivement dans django/template/defaultfilters.py et django/template/defaulttags.py.

Pour plus d’informations sur la balise load, lisez sa documentation.

Écriture de filtres de gabarits personnalisés

Les filtres personnalisés ne sont que des fonctions Python qui acceptent un ou deux paramètres :

  • La valeur de la variable (en entrée), pas forcément une chaîne.
  • La valeur du paramètre ; il peut avoir une valeur par défaut ou être simplement absent.

Par exemple, dans le filtre {{ var|foo:"bar" }}, le filtre foo reçoit la variable var et le paramètre "bar".

Dans la mesure où le langage de gabarit ne s’occupe pas de la gestion des exceptions, toute exception générée dans un filtre de gabarit apparaît comme erreur de serveur. C’est pourquoi les fonctions de filtres devraient éviter de générer des exceptions quand il existe une valeur de repli raisonnable à renvoyer. Dans le cas d’une valeur d’entrée représentant clairement un bogue dans un gabarit, il se peut que la génération d’une exception soit toujours la meilleure solution plutôt que d’échouer silencieusement en masquant l’erreur.

Voici un exemple de définition de filtre :

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

Et voici une exemple de la manière dont ce filtre pourrait être utilisé :

{{ somevariable|cut:"0" }}

La plupart des filtres n’acceptent pas de paramètre. Dans ce cas, ne rajoutez pas de paramètre à votre fonction. Exemple :

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

Inscription de filtres personnalisés

django.template.Library.filter()

Après avoir écrit la définition d’un filtre, il est nécessaire de l’inscrire avec votre instance de Library pour qu’il soit disponible dans le langage de gabarit de Django :

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

La méthode Library.filter() accepte deux paramètres :

  1. Le nom du filtre, une chaîne.
  2. La fonction de compilation, une fonction Python (et non pas le nom de la fonction sous forme de chaîne).

Vous pouvez aussi utiliser register.filter() sous forme de décorateur :

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

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

Si vous omettez le paramètre name comme dans le second exemple ci-dessus, Django utilisera le nom de la fonction comme nom de filtre.

Pour terminer, register.filter() accepte aussi trois paramètres nommés : is_safe, needs_autoescape et expects_localtime. Ces paramètres sont décrits dans filtres et échappement automatique et filtres et fuseaux horaires ci-dessous.

Filtres de gabarits agissant sur des chaînes

django.template.defaultfilters.stringfilter()

Si vous écrivez un filtre de gabarit qui s’attend uniquement à une chaîne comme premier paramètre, vous devriez utiliser le décorateur stringfilter. Celui-ci se chargera de convertir un objet dans son équivalent chaîne de caractères avant de le passer à votre fonction :

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()

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

De cette façon, vous pourrez par exemple passer un nombre entier à ce filtre et il ne générera pas d’erreur AttributeError (les nombres entiers n’ont pas de méthode lower()).

Les filtres et l’échappement automatique

Quand vous écrivez un filtre personnalisé, réfléchissez sur la façon dont celui-ci va interagir avec le comportement d’échappement automatique de Django. Notez que deux types de chaînes peuvent circuler dans le code des gabarits :

  • Des chaînes brutes sont les chaînes Python natives. En sortie, et si l’échappement automatique est en vigueur, ces chaînes seront échappées, sinon elles sont laissées telles quelles.

  • Des chaînes sûres sont des chaînes qui ont été marquées comme sûres et par là-même qu’elles n’ont plus besoin d’être échappées au moment de l’affichage. Tout échappement nécessaire aura déjà été effectué. Elles sont habituellement utilisées pour des chaînes qui contiennent du HTML brut devant être interprété tel quel au niveau du client.

    En interne, ces chaînes sont de type SafeText. Vous pouvez les identifier en utilisant du code comme :

    from django.utils.safestring import SafeText
    
    if isinstance(value, SafeText):
        # Do something with the "safe" string.
        ...
    

Le code des filtres de gabarits entre dans l’une des deux situations suivantes :

  1. Votre filtre n’introduit aucun nouveau caractère HTML non sûr (<, >, ', " ou &) dans le résultat. Dans ce cas, vous pouvez laisser Django s’occuper de toute la gestion de l’échappement automatique. Tout ce que vous avez à faire est de définir le drapeau is_safe à True lorsque vous inscrivez votre fonction de filtre, comme ceci :

    @register.filter(is_safe=True)
    def myfilter(value):
        return value
    

    Ce drapeau indique à Django que si une chaîne « sûre » (safe) est passée à votre filtre, le résultat sera toujours « sûr » et que si une chaîne non sûre est passée, Django l’échappera automatiquement si nécessaire.

    Vous pouvez considérer cela comme signifiant que « ce filtre est sûr, il n’introduit aucune éventualité de code HTML non sûr ».

    La raison d’être de is_safe est qu’il existe un grand nombre d’opérations normales sur les chaînes qui transforment un objet SafeData en un objet str normal, et qu’au lieu d’essayer d’identifier tous ces cas de figure, ce qui serait très difficile, Django répare les dommages après que le filtre ait fait son travail.

    Par exemple, imaginons un filtre qui ajoute la chaîne xx à la fin de n’importe quelle variable d’entrée. Comme cela n’introduit aucun caractère HTML dangereux dans le résultat (sauf s’il y en avait déjà), vous devriez marquer ce filtre avec is_safe:

    @register.filter(is_safe=True)
    def add_xx(value):
        return '%sxx' % value
    

    Lorsque ce filtre est utilisé dans un gabarit dans lequel l’échappement automatique est en vigueur, Django va échapper le contenu chaque fois que la variable dde départ n’est pas déjà marquée comme « sûre ».

    Par défaut, is_safe vaut False, et vous pouvez simplement l’omettre quand il n’est pas nécessaire.

    Soyez prudent quand vous décidez si votre filtre conserve réellement des chaînes sûres. Si vous enlevez des caractères, il se peut que vous laissiez par mégarde des balises ou entités HTML mal équilibrées dans le résultat. Par exemple, en enlevant un > du contenu initial, cela pourrait transformer <a> en <a qui devrait alors être échappé à l’affichage pour éviter de poser des problèmes. De même, enlever un point-virgule (;) peut transformer &amp; en &amp, ce qui n’est plus une entité valide et devrait donc de nouveau être échappé. Ce ne sera pas si subtil dans la plupart des cas, mais gardez à l’esprit ce genre de problème potentiel lorsque vous relisez votre code.

    Le marquage d’un filtre avec is_safe force la valeur de retour du filtre en chaîne de caractères. Si votre filtre doit renvoyer un booléen ou une autre valeur non textuelle, le marquage avec is_safe va probablement générer des conséquences inattendues (comme la conversion du booléen False en chaîne “False”).

  2. Une autre option possible est de s’occuper manuellement dans le code du filtre d’effectuer d’éventuels échappements. C’est nécessaire quand vous introduisez de nouvelles balises HTML dans le résultat. Il faut alors marquer le contenu comme sûr et exempt de besoin d’échappement afin que le code HTML introduit ne soit pas échappé plus tard, ce qui implique donc que vous gériez vous-même ce contenu.

    Pour marquer le résultat comme une chaîne sûre, utilisez django.utils.safestring.mark_safe().

    Mais restez prudent. Vous devez faire plus que simplement marquer le résultat comme sûr. Vous devez vous assurer qu’il soit réellement sûr et votre action dépend de l’activité de l’échappement automatique. L’idée est d’écrire des filtres qui peuvent agir dans des gabarits où l’échappement automatique est activé ou non, afin de faciliter la tâche aux rédacteurs de gabarits.

    Pour que vos filtres sachent si l’échappement automatique est actuellement actif, définissez le drapeau needs_autoescape à True lorsque vous inscrivez votre fonction de filtre (il vaut False par défaut). Ce drapeau indique à Django que votre fonction de filtre souhaite recevoir un paramètre mot-clé supplémentaire, autoescape. Ce paramètre vaudra True quand l’échappement automatique est en vigueur et False dans le cas contraire. Il est recommandé de définir la valeur par défaut du paramètre autoescape à True, pour que si la fonction est appelée depuis le code Python, l’échappement soit activé par défaut.

    Par exemple, écrivons un filtre qui met en évidence le premier caractère d’une chaîne :

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=True):
        first, other = text[0], text[1:]
        if autoescape:
            esc = conditional_escape
        else:
            esc = lambda x: x
        result = '<strong>%s</strong>%s' % (esc(first), esc(other))
        return mark_safe(result)
    

    Le drapeau needs_autoescape et le paramètre nommé autoescape signifient que notre fonction saura si l’échappement automatique est en vigueur au moment où le filtre est appelé. Nous utilisons autoescape pour savoir si nous devons faire passer les données reçues par la fonction django.utils.html.conditional_escape (sinon, nous utilisons la fonction neutre comme fonction d’échappement). La fonction conditional_escape() est semblable à escape(), sauf que l’échappement n’a lieu que si la valeur d’entrée n’est pas déjà une instance de SafeData. Si une instance de SafeData est transmise à conditional_escape(), les données sont renvoyées sans être modifiées.

    Finalement, dans l’exemple ci-dessus, nous n’oublions pas de marquer le résultat comme sûr afin que notre code HTML soit inséré directement dans le gabarit sans échappement supplémentaire.

    Il n’y a pas besoin de se préoccuper du drapeau is_safe dans ce cas (même si on pourrait le définir sans dommages). Chaque fois que vous gérez manuellement la question de l’échappement automatique et que vous renvoyez une chaîne sûre, le drapeau is_safe ne change rien, quelle que soit sa valeur.

Avertissement

Risque de vulnérabilités XSS lors de la réutilisation de filtres intégrés

Les filtres intégrés de Django contiennent autoescape=True par défaut afin d’obtenir le bon comportement d’échappement automatique et d’éviter ainsi une éventuelle vulnérabilité de script inter-site.

Dans les versions plus anciennes de Django, réutilisez prudemment les filtres intégrés de Django car la valeur par défaut de autoescape est None. Vous devrez passer explicitement autoescape=True pour que le résultat soit automatiquement échappé.

Par exemple, si vous voulez écrire un filtre personnalisé nommé urlize_and_linebreaks qui combine les filtres urlize et linebreaksbr, le filtre pourrait ressembler à ceci :

from django.template.defaultfilters import linebreaksbr, urlize

@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
    return linebreaksbr(
        urlize(text, autoescape=autoescape),
        autoescape=autoescape
    )

Puis :

{{ comment|urlize_and_linebreaks }}

serait équivalent à :

{{ comment|urlize|linebreaksbr }}

Filtres et fuseaux horaires

Si vous écrivez un filtre personnalisé qui opère sur des objets datetime, vous allez généralement l’inscrire avec le drapeau expects_localtime défini à True:

@register.filter(expects_localtime=True)
def businesshours(value):
    try:
        return 9 <= value.hour < 17
    except AttributeError:
        return ''

Lorsque ce drapeau est défini et si le premier paramètre du filtre est un objet date/heure sensible aux fuseaux horaires, Django le convertit au besoin dans le fuseau horaire actuel avant de le passer à votre filtre, selon les règles de conversion de fuseaux horaires dans les gabarits.

Écriture de balises de gabarits personnalisées

Les balises sont plus complexes que les filtres, car les balises peuvent tout faire. Django fournit un certain nombre de raccourcis qui facilitent l’écriture de la plupart des types de balises. Nous allons commencer par explorer ces raccourcis, puis expliquer comment écrire une balise à partir de rien pour les cas où les raccourcis ne conviennent pas au besoin.

Balises simples

django.template.Library.simple_tag()

Bien des balises de gabarit acceptent des paramètres (chaînes ou variables de gabarit) et renvoient un résultat après avoir effectué quelques opérations sur la base des paramètres initiaux et de certaines informations externes. Par exemple, une balise current_time pourrait accepter une chaîne de formatage et renvoyer l’heure sous forme de chaîne dans le bon format.

Pour faciliter la création de ce type de balise, Django fournit une fonction utilitaire, simple_tag. Cette fonction, qui est une méthode de django.template.Library, accepte elle-même une fonction qui accepte autant de paramètres que nécessaire, l’enveloppe dans une fonction render ainsi que les autres éléments nécessaires tels que mentionnés ci-dessus et l’inscrit dans le système de gabarits.

Notre fonction current_time pourrait donc être écrite comme ceci :

import datetime
from django import template

register = template.Library()

@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

Quelques petites choses à signaler au sujet de la fonction utilitaire simple_tag:

  • Le contrôle du nombre de paramètres requis, etc., a déjà été effectué au moment où notre fonction est appelée, nous n’avons donc pas à le faire.
  • Les guillemets autour du paramètre (le cas échéant) ont déjà été enlevés, nous recevons donc une chaîne normale.
  • Si le paramètre était une variable de gabarit, notre fonction reçoit la valeur réelle de la variable et non pas la variable elle-même.

Au contraire des autres utilitaires de balises, simple_tag passe son résultat par conditional_escape() si le contexte de gabarit est en mode échappement automatique, pour garantir du HTML correct et vous protéger d’éventuelles vulnérabilités XSS.

Si l’échappement complémentaire n’est pas souhaité, il est nécessaire d’utiliser mark_safe() pour autant que vous soyez absolument sûr que votre code ne contienne pas de vulnérabilité XSS. Pour la construction de petits bouts de HTML, il est fortement recommandé d’utiliser format_html() au lieu de mark_safe().

Si votre balise de gabarit a besoin d’accéder au contexte en cours, vous pouvez utiliser le paramètre takes_context lors de l’inscription de la balise :

@register.simple_tag(takes_context=True)
def current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

Notez que le premier paramètre doit être nommé context.

Pour plus d’informations sur le fonctionnement de l’option takes_context, consultez la section sur les balises d’inclusion.

Si vous avez besoin de renommer votre balise, vous pouvez lui donner un nom personnalisé :

register.simple_tag(lambda x: x - 1, name='minusone')

@register.simple_tag(name='minustwo')
def some_function(value):
    return value - 2

Les fonctions simple_tag peuvent accepter n’importe quel nombre de paramètres positionnels ou nommés. Par exemple :

@register.simple_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

Puis, dans le gabarit, n’importe quel nombre de paramètres séparés par des espaces peuvent être transmis à la balise de gabarit. Comme en Python, les valeurs des paramètres nommés sont définis par le signe égal (=) et doivent être placés après les paramètres positionnels. Par exemple :

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

Il est possible de stocker le résultat de la balise dans une variable de gabarit au lieu de l’afficher directement. Cela se fait en utilisant le paramètre as suivi du nom de variable. Ce faisant, vous pouvez ensuite afficher ce contenu à l’endroit où vous le souhaitez :

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

Balises d’inclusion

django.template.Library.inclusion_tag()

Un autre type de balises de gabarit fréquent est le type qui affiche certaines données en effectuant le rendu d’un autre gabarit. Par exemple, l’interface d’administration de Django utilise des balises de gabarit personnalisées pour afficher les boutons au bas des pages de formulaires d’ajout/édition. Ces boutons ont toujours une même apparence, mais les cibles de leurs liens changent en fonction de l’objet en cours d’édition, ils représentent donc un cas typique d’utilisation d’un petit gabarit complété par certains détails sur l’objet en cours (dans le cas précis de l’interface d’administration, il s’agit de la balise submit_row).

Ces types de balises sont appelées des « balises d’inclusion ».

Un exemple sera le meilleur moyen d’illustrer l’écriture de balises d’inclusion. Écrivons une balise qui affiche une liste de choix pour un objet Poll donné, comme celui qui a été créé dans les tutoriels. Nous utiliserons la balise comme ceci :

{% show_results poll %}

… et cela donnera le résultat suivant :

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

Pour commencer, définissez la fonction qui accepte le paramètre et qui produit un dictionnaire de données comme résultat. Le point important ici est que nous devons uniquement renvoyer un dictionnaire, rien d’autre de plus complexe. Il sera utilisé comme contexte de gabarit pour le fragment de gabarit. Exemple :

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

Ensuite, créez le gabarit utilisé pour faire le rendu du résultat de la balise. Ce gabarit est une fonctionnalité figée de la balise : c’est le rédacteur de la balise qui le définit, pas le concepteur des gabarits. Suivant notre exemple, le gabarit est très simple :

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

Maintenant, créez et inscrivez la balise d’inclusion en appelant la méthode inclusion_tag() d’un objet Library. Suivant notre exemple, si le gabarit ci-dessus se trouve dans un fichier nommé results.html dans un répertoire parcouru par le chargeur de gabarits, voici comment nous inscrivons la balise :

# Here, register is a django.template.Library instance, as before
@register.inclusion_tag('results.html')
def show_results(poll):
    ...

Il est également possible d’inscrire la balise d’inclusion en utilisant une instance de django.template.Template:

from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

… lorsque nous avons initialement créé la fonction.

Il peut arriver que vos balises d’inclusion nécessitent un grand nombre de paramètres, ce qui rend pénible pour les auteurs des gabarits la transmission de tous les paramètres en se souvenant de l’ordre exigé. Pour résoudre ce problème, Django fournit l’option takes_context pour les balises d’inclusion. Si vous définissez takes_context lors de la création d’une balise de gabarit, celle-ci n’aura aucun paramètre obligatoire et la fonction Python sous-jacente recevra un paramètre, le contexte du gabarit au moment où la balise a été appelée.

Par exemple, disons que vous écrivez une balise d’inclusion qui sera toujours utilisée dans un contexte contenant les variables home_link et home_title pointant sur la page principale. Voici à quoi la fonction Python ressemblerait :

@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }

Notez que le premier paramètre de la fonction doit être nommé context.

Dans la ligne register.inclusion_tag(), nous précisons takes_context=True ainsi que le nom du gabarit. Voici à quoi pourrait ressembler le gabarit link.html:

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

Puis, chaque fois que vous souhaitez utiliser cette balise personnalisée, chargez sa bibliothèque et appelez-la sans paramètre, comme ceci :

{% jump_link %}

Notez que lorsque vous utilisez takes_context=True, il n’y a pas besoin de transmettre de paramètres à la balise de gabarit. Cette dernière a automatiquement accès au contexte.

Le paramètre takes_context vaut False par défaut. Lorsqu’il est défini à True, la balise reçoit l’objet contexte, comme dans cet exemple. C’est la seule différence entre ce cas et l’exemple inclusion_tag précédent.

Les fonctions inclusion_tag acceptent n’importe quel nombre de paramètres positionnels ou nommés. Par exemple :

@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

Puis, dans le gabarit, n’importe quel nombre de paramètres séparés par des espaces peuvent être transmis à la balise de gabarit. Comme en Python, les valeurs des paramètres nommés sont définis par le signe égal (=) et doivent être placés après les paramètres positionnels. Par exemple :

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

Balises de gabarits personnalisées (niveau avancé)

Il peut arriver que les fonctionnalités de base de la création d’une balise de gabarit personnalisée ne suffisent pas. Ne vous en faites pas. Django vous offre un accès complet à l’interface interne nécessaire pour construire une balise de gabarit à partir de rien.

Aperçu rapide

Le système de gabarits fonctionne par un processus en deux étapes : la compilation et le rendu. Pour créer une balise de gabarit personnalisée, il s’agit de définir à la fois le comportement de la compilation et celui du rendu.

Quand Django compile un gabarit, il divise le texte brut du gabarit en « nœuds ». Chaque nœud est une instance de django.template.Node et possède une méthode render(). Un gabarit compilé est simplement une liste d’objets Node. Lorsque vous appelez render() sur un objet gabarit compilé, le gabarit appelle render() pour chaque Node de sa liste de nœuds en leur passant le contexte. Les résultats sont tous concaténés pour former le rendu du gabarit.

Ainsi, pour créer une balise de gabarit personnalisée, vous définissez la manière de convertir la balise de gabarit brute en un Node (fonction de compilation) et ce que fait la méthode render() du nœud.

Écriture de la fonction de compilation

Pour chaque balise de gabarit que l’analyseur de gabarit rencontre, il appelle une fonction Python avec le contenu de la balise et l’objet analyseur lui-même. Cette fonction est responsable de renvoyer une instance de Node en fonction du contenu de la balise.

Par exemple, écrivons une implémentation complète de notre simple balise de gabarit, {% current_time %}, qui affiche l’heure et la date courantes formatées en fonction d’un paramètre donné à la balise, en respectant la syntaxe strftime(). Il est conseillé de définir la syntaxe de la balise avant toute autre chose. Dans notre cas, disons que la balise devrait être utilisée comme ceci :

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

L’analyseur de cette fonction doit capturer le paramètre et créer un objet 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.split()[0]
        )
    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 CurrentTimeNode(format_string[1:-1])

Notes :

  • parser est l’objet analyseur de gabarit. Nous n’en avons pas besoin dans cet exemple.
  • token.contents est une chaîne formée du contenu brut de la balise. Dans notre exemple, il s’agit de 'current_time "%Y-%m-%d %I:%M %p"'.
  • La méthode token.split_contents() sépare les paramètres en fonction des espaces tout en gardant groupées les chaînes entre guillemets. La méthode plus directe token.contents.split() ne serait pas aussi robuste, car elle couperait naïvement à chaque espace, y compris ceux à l’intérieur des chaînes entre guillemets. Il est conseillé de toujours utiliser token.split_contents().
  • Cette fonction est responsable de lever l’exception django.template.TemplateSyntaxError à chaque erreur de syntaxe, avec des messages utiles.
  • Les exceptions TemplateSyntaxError utilisent la variable tag_name. Ne codez pas en dur le nom de la balise dans les messages d’erreur, car cela lie le nom de la balise à votre fonction. token.contents.split()[0] sera toujours le nom de la balise, même lorsque celle-ci n’accepte aucun paramètre.
  • La fonction renvoie un CurrentTimeNode avec tout ce que le nœud a besoin de connaître au sujet de la balise. Dans ce cas, il ne fait que passer le paramètre ("%Y-%m-%d %I:%M %p"). Les guillemets ouvrant et fermant de la balise de gabarit sont enlevés par format_string[1:-1].
  • L’analyse est de très bas niveau. Les développeurs Django ont expérimenté l’écriture de petites infrastructures au-dessus de ce système d’analyse, en utilisant des techniques comme les grammaires EBNF, mais ces expériences ont rendu le moteur de gabarit trop lent. L’analyse est de bas niveau car c’est la solution la plus rapide.

Écriture de la fonction de rendu

La seconde étape dans l’écriture de balises de gabarit est définir une sous-classe de Node possédant une méthode render().

Pour continuer l’exemple ci-dessus, nous devons définir CurrentTimeNode:

import datetime
from django import template

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)

Notes :

  • __init__() reçoit la chaîne format_string de do_current_time(). Passez toujours toute option ou paramètre à un Node par l’intermédiaire de sa méthode __init__().
  • C’est dans la méthode render() que se passe réellement le travail.
  • render() doit généralement échouer silencieusement, particulièrement en environnement de production. Cependant, dans certains cas, spécialement lorsque context.template.engine.debug vaut True, cette méthode peut générer une exception pour faciliter le débogage. Par exemple, plusieurs balises intégrées génèrent django.template.TemplateSyntaxError si elles reçoivent le mauvais nombre de paramètres ou des paramètres du mauvais type.

Au final, cette distinction entre compilation et rendu aboutit à un système de gabarit efficace, car un gabarit peut produire un rendu avec plusieurs contextes différents sans devoir être analysé plusieurs fois.

Considérations sur l’échappement automatique

Le résultat des balises de gabarit ne passe pas automatiquement par les filtres d’échappement automatique (à l’exception de simple_tag() comme expliqué plus haut) . Toutefois, il y a quand même deux ou trois choses à garder à l’esprit en écrivant des balises de gabarit.

Si la fonction render() de votre gabarit conserve le résultat dans une variable de contexte (plutôt que de renvoyer le résultat dans une chaîne), elle devrait alors s’occuper d’appeler mark_safe() si nécessaire. Lorsque la variable sera finalement affichée, elle sera affectée par le réglage d’échappement automatique en vigueur à ce moment, il faut donc que les contenus qui ne doivent plus être échappés soient marqués comme tels.

De même, si votre balise de gabarit crée un nouveau contexte pour procéder à certains sous-rendus, définissez l’attribut d’échappement automatique pour la valeur du contexte en cours. La méthode __init__ de la classe Context accepte un paramètre nommé autoescape et qui est destiné à cet effet. Par exemple :

from django.template import Context

def render(self, context):
    # ...
    new_context = Context({'var': obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

Ce n’est pas une situation très courante, mais c’est utile si vous faites vous-même le rendu d’un gabarit. Par exemple :

def render(self, context):
    t = context.template.engine.get_template('small_fragment.html')
    return t.render(Context({'var': obj}, autoescape=context.autoescape))

Si nous avions négligé de passer la valeur actuelle de context.autoescape au nouveau Context de cet exemple, les résultats auraient toujours subi un échappement automatique, ce qui pourrait ne pas correspondre au comportement attendu si la balise de gabarit est utilisée à l’intérieur d’un bloc {% autoescape off %}.

Considérations sur la concurrence entre « threads »

Dès qu’un nœud est analysé, sa méthode render peut être appelée indéfiniment. Comme Django est parfois lancé dans un environnement avec plusieurs fils d’exécution parallèles (multi-thread), un même nœud peut être rendu plusieurs fois simultanément avec différents contextes en réponse à des requêtes séparées. Il est donc important de s’assurer que vos balises de gabarit sont robustes à la concurrence.

Pour que vos balises de gabarit sachent gérer la concurrence, il ne faut jamais stocker des informations d’état dans le nœud lui-même. Par exemple, Django contient une balise de gabarit cycle qui parcourt une liste de chaînes données à chaque rendu :

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}">
        ...
    </tr>
{% endfor %}

Une implémentation naïve de CycleNode pourrait ressembler à ceci :

import itertools
from django import template

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cycle_iter = itertools.cycle(cyclevars)

    def render(self, context):
        return next(self.cycle_iter)

Mais supposons que deux gabarits soient rendus en parallèle et qu’ils contiennent l’extrait de gabarit ci-dessus :

  1. Le fil d’exécution 1 effectue sa première itération de boucle, CycleNode.render() renvoie « row1 »
  2. Le fil d’exécution 2 effectue sa première itération de boucle, CycleNode.render() renvoie « row2 »
  3. Le fil d’exécution 1 effectue sa seconde itération de boucle, CycleNode.render() renvoie « row1 »
  4. Le fil d’exécution 2 effectue sa seconde itération de boucle, CycleNode.render() renvoie « row2 »

Le nœud CycleNode effectue son itération, mais globalement. En ce qui concerne les fils d’exécution 1 et 2, ils renvoient toujours la même valeur. Ce n’est évidemment pas ce que nous voulons !

Pour résoudre ce problème, Django met à disposition un render_context qui est associé au context du gabarit qui est en cours de rendu. render_context se comporte comme un dictionnaire Python et doit être utilisé pour stocker l’état de Node entre les invocations de la méthode render.

Révisons notre implémentation de CycleNode en utilisant render_context:

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cyclevars = cyclevars

    def render(self, context):
        if self not in context.render_context:
            context.render_context[self] = itertools.cycle(self.cyclevars)
        cycle_iter = context.render_context[self]
        return next(cycle_iter)

Notez qu’il est totalement correct de stocker sous forme d’attribut de l’information globale qui ne changera pas durant le cycle de vie de Node. Dans le cas de CycleNode, le paramètre cyclevars ne change plus après que Node a été instancié, nous n’avons donc pas besoin de le placer dans render_context. Mais l’information d’état qui est spécifique au gabarit en cours de rendu, comme l’état d’itération de CycleNode, doit être stockée dans render_context.

Note

Remarquez la manière dont nous avons utilisé self pour délimiter l’information spécifique de CycleNode dans render_context. Il peut y avoir plusieurs CycleNodes dans un gabarit donné, il faut donc être prudent de ne pas empiéter sur l’information d’état d’un autre nœud. La façon la plus simple de faire cela est de toujours utiliser self comme clé dans render_context. Si vous conservez la trace de plusieurs variables d’état, faites de render_context[self] un dictionnaire.

Inscription de la balise

Pour terminer, inscrivez la balise dans l’instance Library de votre module, comme expliqué dans la section sur l”écriture des filtres de gabarit personnalisés ci-dessus. Exemple :

register.tag('current_time', do_current_time)

La méthode tag() accepte deux paramètres :

  1. Le nom de la balise de gabarit (une chaîne). En cas d’omission, c’est le nom de la fonction de compilation qui sera utilisé.
  2. La fonction de compilation, une fonction Python (et non pas le nom de la fonction sous forme de chaîne).

Comme pour l’inscription de filtre, il est aussi possible d’utiliser une syntaxe de décorateur :

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

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

Si vous omettez le paramètre name comme dans le second exemple ci-dessus, Django utilise le nom de la fonction comme nom de balise.

Transmission de variables de gabarit à la balise

Même si vous pouvez transmettre autant de paramètres que souhaité à la balise de gabarit en utilisant token.split_contents(), les paramètres sont alors tous identifiés comme chaînes de caractères. Pour pouvoir passer du contenu dynamique (une variable de gabarit) comme paramètre d’une balise de gabarit, cela demande un petit peu plus d’effort.

Dans les exemples précédents, l’heure actuelle a été formatée en chaîne et celle-ci a été renvoyée en tant que chaîne. Supposons que nous souhaitions passer un DateTimeField d’un objet pour que la balise de gabarit mette en forme cette date :

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

Initialement, token.split_contents() renverra trois valeurs :

  1. Le nom de la balise
  2. La chaîne 'blog_entry.date_updated' (sans les guillemets).
  3. La chaîne de formatage '"%Y-%m-%d %I:%M %p"'. La valeur de retour de split_contents() contiendra les guillemets ouvrant et fermant pour de telles chaînes constantes.

Votre balise devrait maintenant commencer à ressembler à ceci :

from django import template

def do_format_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, date_to_be_formatted, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires exactly two arguments" % token.contents.split()[0]
        )
    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 FormatTimeNode(date_to_be_formatted, format_string[1:-1])

Vous devez aussi modifier la fonction de rendu pour récupérer le contenu réel de la propriété date_updated de l’objet blog_entry. Cela peut se faire en utilisant la classe Variable() de django.template.

Pour utiliser la clase Variable, il suffit de l’instancier avec le nom de la variable à résoudre puis d’appeler variable.resolve(context). Ainsi, par exemple :

class FormatTimeNode(template.Node):
    def __init__(self, date_to_be_formatted, format_string):
        self.date_to_be_formatted = template.Variable(date_to_be_formatted)
        self.format_string = format_string

    def render(self, context):
        try:
            actual_date = self.date_to_be_formatted.resolve(context)
            return actual_date.strftime(self.format_string)
        except template.VariableDoesNotExist:
            return ''

La résolution de variable lèvera une exception VariableDoesNotExist si elle ne peut pas résoudre la chaîne qui lui a été passée dans le contexte actuel de la page.

Définition d’une variable dans le contexte

Les exemples ci-dessus ne font qu’afficher une valeur. Il est généralement plus souple de définir des variables de gabarit dans les balises de gabarit que d’afficher des valeurs. De cette façon, les auteurs de gabarits peuvent réutiliser les valeurs créées par vos balises de gabarit.

Pour définir une variable dans le contexte, il suffit d’attribuer des valeurs à l’objet dictionnaire du contexte dans la méthode render(). Voici une version mise à jour de CurrentTimeNode qui définit une variable de gabarit current_time au lieu de l’afficher :

import datetime
from django import template

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 ''

Notez que render() renvoie la chaîne vide. render() doit toujours renvoyer une chaîne. Si la balise de gabarit ne fait que définir une variable, render() doit renvoyer la chaîne vide.

Voici comment on pourrait utiliser cette nouvelle version de la balise :

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

Portées des variables dans le contexte

Toute variable définie dans le contexte ne sera disponible que dans le même « bloc » de gabarit dans lequel elle a été définie. Ce comportement est voulu ; il définit la portée des variables afin qu’elles n’entrent pas en conflit avec le contexte d’autres blocs.

Mais il reste un problème avec CurrentTimeNode2: le nom de variable current_time est codé en dur. Cela veut dire que vous devrez vous assurer que votre gabarit n’utilise pas {{ current_time }} ailleurs, parce que {% current_time %} va écraser de manière aveugle la valeur de cette éventuelle variable. Une solution plus propre est de permettre à la balise de gabarit de définir le nom de la variable produite, comme ceci :

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

Pour faire cela, il faudra modifier à la fois la fonction de compilation et la classe Node, comme ceci :

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.split()[0]
        )
    m = re.search(r'(.*?) as (\w+)', arg)
    if not m:
        raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
    format_string, var_name = m.groups()
    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)

La différence ici est que do_current_time() capture la chaîne de format et le nom de la variable, les transmettant les deux à CurrentTimeNode3.

Finalement, si vous n’avez besoin que d’une syntaxe simple pour votre balise de gabarit de mise à jour du contexte, envisagez l’utilisation du raccourci simple_tag() qui permet d’attribuer le résultat de la balise à une variable de gabarit.

Analyse jusqu’à la prochaine balise de bloc

Les balises de gabarit peuvent travailler en tandem. Par exemple, la balise {% comment %} standard masque tout jusqu’à la prochaine occurrence de {% endcomment %}. Pour créer une balise de gabarit semblable, utilisez parser.parse() dans votre fonction de compilation.

Voici comment une balise {% comment %} simplifiée pourrait être écrite :

def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''

Note

L’implémentation effective de {% comment %} est légèrement différente dans la mesure où elle autorise des balises incorrectes à apparaître entre {% comment %} et {% endcomment %}. Elle fait cela en appelant parser.skip_past('endcomment') au lieu de parser.parse(('endcomment',)), suivi par parser.delete_first_token(), ce qui évite de générer une liste de nœuds.

parser.parse() accepte un tuple de noms de balises bloc définissant les limites d’analyse. Elle renvoie une instance de django.template.NodeList contenant une liste de tous les objets Node que l’analyseur a rencontré avant que n’apparaisse l’une des balises indiquées dans le tuple.

Dans nodelist = parser.parse(('endcomment',)) de l’exemple ci-dessus, nodelist est une liste de tous les nœuds entre {% comment %} et {% endcomment %}, à l’exclusion de {% comment %} et {% endcomment %}.

Après l’appel à parser.parse(), l’analyseur n’a pas encore « consommé » la balise {% endcomment %}, c’est pourquoi le code doit explicitement appeler parser.delete_first_token().

CommentNode.render() renvoie simplement une chaîne vide. Tout ce qui se trouve entre {% comment %} et {% endcomment %} est ignoré.

Analyse jusqu’à la prochaine balise de bloc et enregistrement du contenu

Dans l’exemple prédédent, do_comment() a ignoré tout ce qui apparaissait entre {% comment %} et {% endcomment %}. Au lieu de faire cela, il est possible de faire quelque chose avec le code situé entre les balises de bloc.

Par exemple, voici une balise de gabarit personnalisée, {% upper %}, qui met en majuscules tout ce qui se trouve entre elle et {% endupper %}.

Utilisation :

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

Comme dans l’exemple précédent, nous utilisons parser.parse(). Mais cette fois-ci, nous passons le contenu de nodelist à Node:

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()

Le seul nouveau concept ici est l’appel à self.nodelist.render(context) dans UpperNode.render().

Pour plus d’exemples de rendus complexes, consultez le code source des balises {% for %} dans django/template/defaulttags.py et {% if %} dans django/template/smartif.py.

Back to Top