# Documentation Django 3.1.9.dev

# 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 sont 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

    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 `SafeString`. Vous pouvez les identifier en utilisant du code comme :

      from django.utils.safestring import SafeString

      if isinstance(value, SafeString):
          # 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 court :

    <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 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 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 passe 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 méthode `render()` de votre balise de 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 balises de gabarit personnalisées 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 classe `Variable`, il faut l’instancier avec le nom de la variable à résoudre puis 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, attribuez 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 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`.