Tutos Django

Tag et Filtres personnalisés

Bien que Django vous fournisse un grand nombre de tags et de filtres par défaut : Built-in template tags and filters, il vous est possible de créer les vôtres.

Pour rendre cela possible, il va d'abord nous falloir créer un répertoire nommé 'templatetags' dans le dossier de notre application. Ce répertoire sera donc au même niveau que les fichiers models.py et views.py. De plus, il faudra rendre ce répertoire atteignable par django et donc créer un fichier __init__.py vide.
Si votre application s'appelle my_app, vous obtenez une arborescence de ce genre :

my_app/
    models.py
    templatetags/
        __init__.py
    views.py

Cela peut être réalisé avec cette ligne de code shell :

$ python manage.py startapp my_app && cd my_app && mkdir templatetags && cd templatetags && touch __init__.py

Afin de rendre l'application disponible à notre projet, n'oubliez pas de la renseigner dans le paramètre INSTALLED_APPS de votre fichier de configuration settings.py, sans quoi les tags et filtres personnalisés créés ne seront pas disponibles.

Filtres

Commençons par créer un filtre personnalisé.
remove_s : un filtre nous permettant d'enlever à une chaine de caractère un ou plusieurs caractères.
 Exemple : considérons la chaine str valant "hello world", je veux enlever tous les "o",  ce qui donnera "hell wrld".  Mon filtre s'utilisera de cette manière :

{{ str|remove_s:"o" }} 

Placez-vous dans le répertoire templatetags et créez un fichier python du nom de votre tag.

$ touch remove_s.py

Maintenant il va falloir réaliser la logique du code de ce filtre. Éditez votre fichier en ajoutant ceci :

from django import template

register = template.Library()

Ces lignes permettent d'identifier le fichier python comme un filtre ou un tag pour django. Nous pourrons ensuite ajouter les tags et filtres créés à l'environnement de l'application.
Ajoutons maintenant la fonction de remplacement :

def remove_s(value, arg):
    return value.replace(arg, '')

le paramètre value est l'objet sur lequel s'applique le filtre et le paramètre arg est le principal argument du filtre. Ici value vaut "hello world" et arg vaut "o". La chaine retournée par ce filtre est donc "hell wrld"
Il ne nous reste plus qu'à enregistrer cette fonction en tant que filtre. Pour cela deux manières de faire, soit en ajoutant a la fin de notre fichier :

register.filter('remove_s', remove_s)

Ou bien en utilisant un décorateur :

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

On spécifie le nom du filtre en association avec la méthode définie.
 Si le nom de votre filtre est le même que le nom de la méthode utilisée comme dans notre cas, vous pouvez omettre le paramètre name et simplement écrire :

@register.filter
def remove_s(value, arg):
    return value.replace(arg, '')

De plus, avec ce type de filtre nous ne voulons faire des opérations uniquement sur des chaines de caractères. Nous pouvons donc rajouter le décorateur @stringfilter :

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

register = template.Library()

@register.filter
@stringfilter
def remove_s(value, arg):
    return value.replace(arg, '')

Ce décorateur permet de transformer la valeur passée value en son type 'string', de ce fait si un entier par exemple est associé à ce filtre, il sera d'abord converti en string et l'exception 'AttributeError' ne sera pas levée (car les entiers n'ont pas de méthode 'replace').

Par ailleurs, vous devez également gérer correctement l'échappement des caractères. Si votre filtre n'ajoute pas de caractères dits "unsafe" (<, >, ', " ou &) dans le résultat de retour, vous pouvez utiliser @register.filter(is_safe=True). Cela signifie que si une chaine de caractère dite 'safe' est passée au filtre alors django retournera une chaine de caractère dite 'safe', au contraire si elle est 'unsafe', django s'occupera de l'échappement si nécessaire. C'est pourquoi il faut bien gérer les cas ou vos filtres sont utiliser dans des tag 'autoescape' par exemple.
Par défaut 'is_safe' a une valeur fausse et vous n'avez pas besoin de l'utiliser sur les filtres ne traitant pas de caractères html. Mais faites bien attention au cas ou votre filtre supprime des caractères, si '<a>' devient '<a', alors il devra être échappé pour éviter que des problèmes apparaissent dans le rendu de votre template.
Enfin, si votre filtre rajoute des balises html, il vous faudra considérer la méthode 'mark_safe' conjuguée au flag 'needs_autoescape'.
Retrouvez plus d'informations sur ce cas particulier dans la documentation officielle : Filters and auto-escaping.

Il nous reste une dernière chose à faire pour utiliser tout filtre ou tag !
Ajouter au début de votre template le tag load prenant en paramètre le nom de votre fichier python contenant vos filtres et tags. Ce qui donnera :

{% load remove_s %}

Et voilà ! Vous savez comment créer et utiliser des filtres personnalisés !
Bien sûr vous pouvez définir plusieurs filtres dans un même fichier python, ce qui vous permettra de ne charger qu'un seul fichier depuis votre template.
Il en va de même pour les tags.

Tags

Nous allons maintenant passer au tag personnalisés. Les tags sont plus complexes car ils permettent de tout faire !
Ils comprennent deux parties, la partie de compilation et la partie de rendu.

Créons un tag nous permettant de connaitre la date et l'heure du moment :

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

Écrivons la fonction de compilation :

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])
  • parser est l'objet parser du template, nous ne nous en servons pas dans notre exemple.
  • token.contents est une chaine de caractère du contenu brut du tag. Ici, c'est 'current_time "%Y-%m-%d %I:%M %p"'.
  • La méthode token.split_contents() divisent les arguments séparés par des espaces sans pour autant s'exécuter sur les chaines de caractères entre double guillemet. Ce qui permet d'obtenir un tableau a deux éléments 'current_time' et la chaine de caractère "%Y-%m-%d %I:%M %p".
  • Une vérification est ensuite faite afin de savoir si le nombre d'arguments est respecté ainsi que leurs formats.
  • Cette fonction retourne un objet CurrentTimeNode contenant tout ce que le 'Node' doit connaitre a propos du tag. Dans ce cas, L'argument "%Y-%m-%d %I:%M %p" est passé. Les guillemets de début et de fin sont enlevés a l'aide de format_string[1:-1].

Écrivons maintenant le rendu :

from django import template
import datetime
class CurrentTimeNode(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string
    def render(self, context):
        return datetime.datetime.now().strftime(self.format_string)
  • __init__() récupère format_string depuis la méthode do_current_time(). Il faut toujours passer les options/paramètres/arguments a un objet Node via la méthode __init__().
  • La méthode render() définit la logique du code. Équivalente a une fonction utilisée pour un filtre.
  • A la manière d'un filtre render() ne devrait jamais lever d'exception TemplateSyntaxError ou toute autre exception. Mais échouer silencieusement.

Et voilà ! Ce n'était pas plus compliqué !
De la même manière que les filtres, il vous faudra gérer les cas d'échappement : Auto-escaping considerations

Aussi, souvenez-vous du tag cycle que nous avons vu ensemble permettant d'alterner des arguments au cours d'une boucle. Si vous désirez créer des tags du même genre, vous aurez besoin de vous penchez sur la notion de thread en Django : Thread-safety considerations

Enfin il ne nous reste plus qu'à enregistrer notre tag. Cela se fait de la même façon qu'avec un filtre, mais en utilisant register.tag et non register.filter. Exemple ici :

register.tag('current_time', do_current_time)

ou

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

Tags avec paramètres

Imaginons maintenant que nous passions une variable à notre tag :

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

Il faudra modifier notre code de cette manière :

Au niveau compilation.

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

La variable date_to_be_formatted est créée et passée au rendu.

Au niveau rendu.

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 nouveauté ici est que nous utilisons la classe Variable() afin de récupérer l'objet par lequel est défini date_to_be_formatted.
Ensuite pour récupérer sa valeur dans la méthode render(), il suffit d'utiliser : 
self.date_to_be_formatted.resolve(context)

Simple tag

Beaucoup de tags prennent quelques arguments, effectuent des opérations et retournent une chaine de caractère.
Par exemple le tag current_time que nous avons écrit en fait partie. Il prend un format et retourne la date et l'heure du moment sous la forme d'une chaine de caractère.
Et bien Django nous fournit une aide bien précieuse nous permettant d'écrire plus facilement nos tags, sans avoir a définir une méthode de compilation ni une méthode de rendu.

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

register.simple_tag(current_time)

Ou avec un décorateur :

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

Aussi simple que cela ! Les vérifications du nombre d'arguments ont déjà été réalisées, les arguments possédant des guillemets de début et de fin ont été correctement formatés et si vous passez une variable à votre tag, sa valeur actuelle est directement envoyée (pas la variable en elle-même).

De plus si votre tag a besoin d'accéder au contexte, vous pouvez utiliser takes_context :

# The first argument *must* be called "context" here.
def current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

register.simple_tag(takes_context=True)(current_time)

Ou en utilisant le décorateur :

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

Aller plus loin avec les tags

Il existe encore un autre type de tags, inclusion_tag qui permet au tag d'utiliser un template.
Exemple :

{% jump_link %}

utilisant le template :

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

ou bien :

{% show_results poll %}

avec le template 'results.html' :

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

produisant :

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

Le code le permettant étant :

@register.inclusion_tag('results.html')
def show_results(poll):
    choices = poll.choice_set.all()
    return {'choices': choices}

Un coup d'œil a la documentation vous en apprendra plus : Inclusion tags

Pour finir, il est également possible d'utiliser des tags ouvrant et fermant à la manière de {% autoescape on/off%}{% endautoescape %}.
Ce sera l'objet parser que nous avons délaissé plus haut qui sera utilisé. Voici comment est implémenté le tag {% comment %}{% endcomment %} :

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

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

Plus d'informations sur la documentation officielle : Parsing until another block tag

Voilà ! Vous savez à présent comment créer des filtres et des tags afin de faciliter toujours plus vos développements de template en django !

Article suivant

Article précédent

Articles associés

Articles similaires

Commentaires

Les commentaires sont fermés.

Pingbacks

Les pingbacks sont fermés.