• 40 hours
  • Medium

Free online content available in this course.

Paperback available in this course

course.header.alt.is_certifying

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 10/11/19

Simplifions nos templates : filtres, tags et contextes

Log in or subscribe for free to enjoy all this course has to offer!

Comme nous l'avons vu rapidement dans le premier chapitre sur les templates, Django offre une panoplie de filtres et de tags. Cependant, il se peut que vous ayez un jour un besoin particulier impossible à combler avec les filtres et tags de base. Heureusement, Django permet également de créer nos propres filtres et tags, et même de générer des variables par défaut lors de la construction d'un template (ce que nous appelons le contexte du template). Nous aborderons ces différentes possibilités dans ce chapitre.

Préparation du terrain : architecture des filtres et tags

Pour construire nos propres filtres et tags, Django impose que ces derniers soient placés dans une application, tout comme les vues ou les modèles. À partir d'ici, nous retrouvons deux écoles dans la communauté de Django :

  • Soit votre fonctionnalité est propre à une application (par exemple un filtre utilisé uniquement lors de l'affichage d'articles), dans ce cas vous pouvez directement le(s) placer au sein de l'application concernée ; nous préférons cette méthode ;

  • Soit vous créez une application à part, qui regroupe tous vos filtres et tags personnalisés.

Une fois ce choix fait, la procédure est identique : l'application choisie doit contenir un dossier nommétemplatetags(attention ausfinal !), dans lequel il faut créer un fichier Python par groupe de filtres/tags (plus de détails sur l'organisation de ces fichiers viendront plus tard). Pour le moment, nous allons en créer un, appeléblog_extras.py.

La nouvelle structure de l'application « blog » est donc la suivante :

blog/
    __init__.py
    migrations/
    models.py
    templatetags/
        __init__.py
        blog_extras.py
    views.py

Une fois les fichiers créés, il est nécessaire de spécifier une instance de classe qui nous permettra d'enregistrer nos filtres et tags, de la même manière que dans nos fichiersadmin.pyavecadmin.site.register(). Pour ce faire, il faut déclarer les deux lignes suivantes au début du fichierblog_extras.py:

from django import template

register = template.Library()

L'application incluant lestemplatetagsdoit être incluse dans le fameuxINSTALLED_APPSde notresettings.py, si vous avez décidé d'ajouter vos tags et filtres personnalisés dans une application spécifique. Une fois les nouveaux tags et filtres codés, il sera possible de les intégrer dans n'importe quel template du projet via la ligne suivante :

{% load blog_extras %}

Le nomblog_extrasvient du nom de fichier que nous avons renseigné plus haut, à savoirblog_extras.py.

Sachez qu'il est possible de charger plusieurs modules de filtres et tags par{% load %}, qui comprend un nombre illimité d'arguments :

{% load blog_extras static i18n %}

Nous pouvons désormais entrer dans le vif du sujet, à savoir la création de filtres et de tags !

Personnaliser l'affichage de données avec nos propres filtres

Commençons par les filtres. En soi, un filtre est une fonction classique qui prend 1 ou 2 arguments :

  • La variable à afficher, qui peut être n'importe quel objet en Python ;

  • Et de façon facultative, un paramètre.

Comme petit rappel au cas où vous auriez la mémoire courte, voici deux filtres : l'un sans paramètre, le deuxième avec.

Filtre upper sur la variable "texte" :
{{ texte|upper }}            

Filtre truncatewords, avec comme argument "80" sur la variable "texte" :
{{ texte|truncatewords:80 }}

Les fonctions Python associées à ces filtres ne sont appelées qu'au sein du template. Pour cette raison, il faut éviter de lancer des exceptions, et toujours renvoyer un résultat. En cas d'erreur, il est plus prudent de renvoyer l'entrée de départ ou une chaîne vide, afin d'éviter des effets de bord lors du « chaînage » de filtres par exemple.

Un premier exemple de filtre sans argument

Attaquons la réalisation de notre premier filtre. Pour commencer, prenons comme exemple le modèle « Citation » de Wikipédia : nous allons encadrer la chaîne fournie par des guillemets français doubles.
Ainsi, si dans notre template nous avons{{ "Bonjour le monde !"|citation }}, le résultat dans notre page sera« Bonjour le monde ! » .

Pour ce faire, il faut ajouter une fonction nomméecitationdansblog_extras.py. Cette fonction n'a pas d'argument particulier et son écriture est assez intuitive :

def citation(texte):   
    """
    Affiche le texte passé en paramètre, encadré de guillemets 
    français doubles
    """
    return "« {} »".format(texte)

 «  et  »  sont les entités HTML permettant d'afficher respectivement les guillemets gauches et droits. Une fois la fonction écrite, il faut préciser au framework d'attacher cette méthode au filtre qui a pour nomcitation. Encore une fois, il y a deux façons différentes de procéder :

  • Soit en ajoutant la ligne@register.filtercomme décorateur de la fonction. L'argumentnamepeut être indiqué pour choisir le nom du filtre ;

  • Soit en appelant la méthoderegister.filter('citation', citation).

Notons qu'avec ces deux méthodes le nom du filtre n'est donc pas directement lié au nom de la fonction, et cette dernière aurait pu s'appelerfiltre_citationou autre, cela n'aurait posé aucun souci tant qu'elle est correctement renseignée par la suite.

Ainsi, ces trois fonctions sont équivalentes :

from django import template

register = template.Library()

@register.filter
def citation(texte):   
    return "« {} »".format(texte)


@register.filter(name='mon_filtre_citation')
def citation2(texte):
    return "« {} »".format(texte)

def citation3(texte):
    return "« {} »".format(texte)

register.filter('un_autre_filtre_citation', citation3)

Nous pouvons maintenant essayer le nouveau filtre dans un template. Il faut tout d'abord charger les filtres dans notre template, via le tagload, introduit récemment, puis appeler notre filtrecitationsur une chaîne de caractères quelconque :

{% load blog_extras %}
Un jour, une certaine personne m'a dit : {{ "Bonjour le monde !"|citation }}

Et là… c'est le drame ! En effet, voici le résultat à la figure suivante.

Le résultat incorrect de notre filtre
Le résultat incorrect de notre filtre

Mais pourquoi les guillemets sont-ils échappés ?‌

Par défaut, Django échappe automatiquement tous les caractères spéciaux des chaînes de caractères affichées dans un template, ainsi que le résultat des filtres. Nous allons donc devoir préciser au framework que le résultat de notre filtre est contrôlé et sécurisé, et qu'il n'est pas nécessaire de l'échapper. Pour cela, il est nécessaire de transformer un peu l'enregistrement de notre fonction avecregister. La méthodefilterpeut prendre comme argumentis_safe, qui permet de signaler au framework par la suite que notre chaîne est sûre :

@register.filter(is_safe=True)
def citation(texte):
    """
    Affiche le texte passé en paramètre, encadré de guillemets 
    français doubles et d'espaces insécables
    """
    return "« {} »".format(texte)

De cette façon, tout le HTML renvoyé par le filtre est correctement interprété et nous obtenons le résultat voulu (voir la figure suivante).

Le résultat correct de notre filtre
Le résultat correct de notre filtre

Cependant, un problème se pose avec cette méthode. En effet, si du HTML est présent dans la chaîne donnée en paramètre, faut-il l'échapper ou pas ? Ainsi, si dans le template nous remplaçons l'exemple précédent par{{ "<strong>Bonjour</strong> le monde !"|citation }}, alors le mot « Bonjour » sera en gras. En soi, ce n'est pas un problème si vous êtes sûrs de la provenance de la chaîne de caractères. Il se pourrait en revanche que, parfois, vous deviez afficher des données entrées par vos utilisateurs, et à ce moment-là n'importe quel visiteur mal intentionné pourrait y placer du code HTML dangereux, ce qui conduirait à des failles de sécurité.

Pour éviter cela, nous allons échapper les caractères spéciaux de notre argument de base et marquer notre résultat comme safe. Cela peut être fait via la fonctionespacedu moduledjango.utils.html. Au final, voici ce que nous obtenons :

from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe

register = template.Library()

@register.filter(is_safe=True)
def citation(texte):
    """
    Affiche le texte passé en paramètre, encadré de guillemets 
    français doubles et d'espaces insécables.
    """
    res = "&laquo; {} &raquo;".format(escape(texte))
    return mark_safe(res)

Finalement, notre chaîne est encadrée de guillemets corrects, mais l'intérieur du message est tout de même échappé.

Un filtre avec arguments

Nous avons pour le moment traité uniquement le cas des filtres sans paramètre. Cependant, il peut arriver que l'affichage doive être différent selon un paramètre spécifié, et ce indépendamment de la variable de base.
Un exemple parmi tant d'autres est la troncature de texte, il existe même déjà un filtre pour couper une chaîne à une certaine position. Nous allons ici plutôt réaliser un filtre qui va couper une chaîne après un certain nombre de caractères, mais sans couper en plein milieu d'un mot.

Comme nous l'avons précisé tout à l'heure, la forme d'un filtre avec un argument est la suivante :

{{ ma_chaine|smart_truncate:40 }}

Nous souhaitons ici appeler un nouveau filtresmart_truncatesur la variablema_chaine, tout en lui passant en argument le nombre 40. La structure du filtre sera similaire à l'exemple précédent. Il faudra cependant bien vérifier que le paramètre est bien un nombre et qu'il y a des caractères à tronquer. Voici un début de fonction :

def smart_truncate(texte, nb_caracteres):
    # Nous vérifions tout d'abord que l'argument passé est bien un nombre
    try:
        nb_caracteres = int(nb_caracteres)
    except ValueError:
        return texte  # Retour de la chaîne originale sinon

    # Si la chaîne est plus petite que le nombre de caractères maximum voulus,
    # nous renvoyons directement la chaîne telle quelle.
    if len(texte) <= nb_caracteres:
        return texte

    # […]

La suite de la fonction est tout aussi classique : nous coupons notre chaîne au nombre de caractères maximum voulu, et nous retirons la dernière suite de lettres, si jamais cette chaîne est coupée en plein milieu d'un mot :

def smart_truncate(texte, nb_caracteres):
    """
    Coupe la chaîne de caractères jusqu'au nombre de caractères souhaité,
    sans couper la nouvelle chaîne au milieu d'un mot.
    Si la chaîne est plus petite, elle est renvoyée sans points de suspension.
    ---
    Exemple d'utilisation :
    {{ "Bonjour tout le monde, c'est Diego"|smart_truncate:18 }} renvoie
    "Bonjour tout le..."
    """
    # Nous vérifions tout d'abord que l'argument passé est bien un nombre
    try:
        nb_caracteres = int(nb_caracteres)
    except ValueError:
        return texte  # Retour de la chaîne originale sinon

    # Si la chaîne est plus petite que le nombre de caractères maximum voulus,
    # nous renvoyons directement la chaîne telle quelle.
    if len(texte) <= nb_caracteres:
        return texte

    # Sinon, nous coupons au maximum, tout en gardant le caractère suivant
    # pour savoir si nous avons coupé à la fin d'un mot ou en plein milieu
    texte = texte[:nb_caracteres + 1]

    # Nous vérifions d'abord que le dernier caractère n'est pas une espace,
    # autrement, il est inutile d'enlever le dernier mot !
    if texte[-1:] != ' ':
        mots = texte.split(' ')[:-1]
        texte = ' '.join(mots)
    else:
        texte = texte[0:-1]

    return texte + '…'

Il ne reste plus qu'à enregistrer notre filtre (via le décorateur@register.filterau-dessus de la lignedef smart_truncate(texte, nb_caracteres):par exemple) et vous pouvez dès à présent tester ce tout nouveau filtre :

<p>
   {{ "Bonjour"|smart_truncate:14 }}<br />
   {{ "Bonjour tout le monde"|smart_truncate:15 }}<br />
   {{ "Bonjour tout le monde, c'est bientôt Noël"|smart_truncate:18 }}<br />
   {{ "To be or not to be, that's the question"|smart_truncate:16 }}<br />
</p>

Ce qui affiche le paragraphe suivant :

Citation

Bonjour
Bonjour tout le...
Bonjour tout le...
To be or not to...

Pour finir, il est possible de mixer les cas filtre sans argument et filtre avec un argument. Dans notre cas de troncature, nous pouvons par exemple vouloir par défaut tronquer à partir du 20e caractère, si aucun argument n'est passé. Dans ce cas, la méthode est classique : nous pouvons indiquer qu'un argument est facultatif et lui donner une valeur par défaut. Il suffit de changer la déclaration de la fonction par :

def smart_truncate(texte, nb_caracteres=20):

Désormais, la syntaxe suivante est acceptée :

{{ "To be or not to be, that's the question"|smart_truncate }}

et renvoie « To be or not to be,... ».

Les contextes de templates

Avant d'attaquer les tags, nous allons aborder un autre point essentiel qui est la création de template context processor (ou en français, des processeurs de contextes de templates). Le but des template context processor est de préremplir le contexte de la requête et ainsi de disposer de données dans tous les templates de notre projet. Le contexte est l'ensemble des variables disponibles dans votre template. Prenons l'exemple suivant :

return render(request, 'blog/archives.html', {'news': news, 'date': date_actuelle})

Ici, nous indiquons au template les variablesnewsetdate_actuellequi seront incorporées au contexte, avec les nomsnewsetdate. Cependant, par défaut notre contexte ne contiendra pas que ces variables, il est même possible d'en ajouter davantage, si le besoin se fait sentir.

Pour mieux comprendre l'utilité des contextes, démarrons par un petit exemple.

Un exemple maladroit : afficher la date sur toutes nos pages

Il arrive que vous ayez besoin d'accéder à certaines variables depuis tous vos templates, et que ceux-ci soient enregistrés dans votre base de données, un fichier, un cache, etc.
Imaginons que vous souhaitiez afficher dans tous vos templates la date du jour. Une première idée serait de récupérer la date sur chacune des vues :

from django.shortcuts import render
from datetime import datetime

def accueil(request):
    date_actuelle = datetime.now()
    # […] Récupération d'autres données (exemple : une liste de news)
    return render(request, 'accueil.html', locals())

def contact(request):
    return render(request, 'contact.html', {'date_actuelle': datetime.now()})

Une fois cela fait, il suffit après d'intégrer la date via{{ date_actuelle }}dans un template parent, à partir duquel tous les autres templates seront étendus. Néanmoins, cette méthode est lourde et répétitive, c'est ici que les processeurs de contextes entrent en jeu.

Factorisons encore et toujours

Pour résoudre ce problème, nous allons créer une fonction qui sera appelée à chaque page, et qui se chargera d'incorporer la date dans les données disponibles de façon automatique.

Tout d'abord, créez un fichier Python, que nous appelleronscontext_processors.py, par convention, dans une de vos applications. Vu que cela concerne tout le projet, il est même conseillé de le créer dans le sous-dossier ayant le même nom que votre projet (crepes_bretonnesdans le cas de ce cours).

Dans ce fichier, nous allons coder une ou plusieurs fonctions, qui renverront des dictionnaires de données que le framework intégrera à tous nos templates.
Tout d'abord, écrivons notre fonction qui va récupérer la date actuelle. La fonction ne prend qu'un paramètre, qui est notre déjà très connu objetrequest.
En retour, la fonction renvoie un dictionnaire, contenant les valeurs à intégrer dans les templates, assez similaire au dictionnaire passé à la fonctionrenderpour construire un template. Par exemple :

from datetime import datetime

def get_infos(request):
    date_actuelle = datetime.now()
    return {'date_actuelle': date_actuelle}

Sachez que Django exécute d'abord la vue et seulement après le contexte. Faites donc attention à prendre des noms de variables suffisamment explicites et qui ont peu de chances de se retrouver dans vos vues, et donc d'entrer en collision. Si jamais vous appelez une variabledate_actuelle, elle sera tout simplement écrasée par la fonction ci-dessus.

Il faut maintenant indiquer au framework d'exécuter cette fonction à chaque page. Pour cela, nous allons encore une fois nous plonger dans le fichiersettings.pyet y modifier la variable  TEMPLATES . À chaque page, Django exécute et récupère les dictionnaires de plusieurs fonctions, listées dans l'option  context_processors . Nous avons ici rajouté notre fonction fraichement créée  get_infos .

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'crepes_bretonnes.context_processors.get_infos',
            ],
        },
    },
]

Nous pouvons désormais utiliser notre variabledate_actuelledans tous nos templates et afficher fièrement la date sur notre blog :

<p>Bonjour à tous, nous sommes le {{ date_actuelle }} et il fait beau en Bretagne !</p>

Et peu importe le template où vous intégrez cette ligne, vous aurez forcément le résultat suivant (si vous n'avez pas de variabledate_actuelledans votre vue correspondante, bien sûr) :

Bonjour à tous, nous sommes le 9 septembre 2012 21:42:00 et il fait beau en Bretagne !
Petit point technique sur l'initialisation du contexte

Attention : dans ce cours nous avons toujours utilisérendercomme retour de nos vues (hormis quelques cas précis où nous avons utiliséHttpResponse). Comme nous l'avions précisé dans le premier chapitre sur les templates, la fonctionrenderest un « raccourci », effectuant plusieurs actions en interne, nous évitant la réécriture de plusieurs lignes de code. Cette méthode prend notamment en charge le fait de charger le contexte !

Cependant, toutes les fonctions de django.shortcutne le font pas, comme par exemplerender_to_response, dont nous n'avons pas parlé et qui fonctionne de la façon suivante pour le cas des archives de notre blog :

from django.shortcuts import render_to_response
[...]
return render_to_response('blog/archives.html', locals())

Si vous rechargez la page, vous remarquerez que la date actuelle a disparu, et que ceci apparaît : « Bonjour à tous, nous sommes le et il fait beau en Bretagne ! ». En effet, par défautrender_to_responsene prend pas en compte les fonctions de processeurs contenues dans TEMPLATES… Pour régler ce problème, il faut à chaque fois ajouter un argument :

return render_to_response('blog/archives.html', locals(), context_instance=RequestContext(request))

… ce qui est plus lourd à écrire ! Cependant, certains utilisateurs avancés peuvent préférer cette méthode afin de gérer de façon précise le contexte à utiliser.

Faites donc attention à vos contextes si jamais vous vous écartez de la fonctionrender.

Des structures plus complexes : les custom tags

Nous avons vu précédemment que les filtres nous permettent de faire de légères opérations sur nos variables, afin de factoriser un traitement qui pourra être souvent répété dans notre template (par exemple la mise en forme d'une citation). Nous allons maintenant aborder les tags, qui sont légèrement plus complexes à mettre en œuvre, mais bien plus puissants.

Alors que les filtres peuvent être comparés à des fonctions, les tags doivent être décomposés en deux parties : la structuration du tag et son rendu. Pour définir de façon précise un tag, nous devons préciser comment l'écrire et ce qu'il renvoie.

Pour mieux comprendre, regardons comment marche un template avec Django.

À la compilation du template, Django découpe votre fichier template en plusieurs nœuds de plusieurs types. Prenons le cas du template suivant :

Bonjour, nous sommes le {% now %}. Je suis {{ prenom }} {{ nom|upper }}

Ici, les nœuds détectés lors de la lecture du template seront :

  • TextNode:"Bonjour, nous sommes le ";

  • Now node(sans argument) ;

  • TextNode:". Je suis ";

  • VariableNode:prenom;

  • TextNode:" ";

  • VariableNode:nomet unFilterExpressionupper.

Lors de l'exécution de la fonctionrenderà la fin d'une vue, Django se charge d'appeler la méthoderenderde chaque nœud et concatène le tout.
Le schéma de la figure suivante récapitule tout cela.

Schéma d'exécution
Schéma d'exécution

Lorsque nous créons un nouveau tag, la fonction appelée à la compilation doit renvoyer un objet dont la classe hérite deNode, avec sa propre méthoderender.
C'est à partir de ce principe que nous obtenons les deux étapes de description d'un tag, à savoir :

  • Décrire comment il peut être écrit pour être reconnu (fonction de compilation) ;

  • Décrire ce qu'il rend, via une classe contenant au moins une fonctionrender(fonction de rendu).

Première étape : la fonction de compilation

À chaque fois que le parseur de template rencontre un tag, il appelle la méthode correspondant au nom du tag enregistré comme pour nos filtres. La fonction se charge ici de vérifier si les paramètres fournis sont corrects ou de renvoyer une erreur si jamais le tag est mal utilisé. Nous allons nous baser sur un exemple assez simple pour commencer : afficher un nombre aléatoire compris entre deux arguments. Cette opération est notamment impossible avec un filtre, ou du moins pas proprement.

Notre tag pourra être utilisé de la façon suivante :{% random 0 42 %}et renverra donc un nombre entier compris entre 0 et 42. Il faudra faire attention à ce que les paramètres soient bien des entiers, et que le premier soit inférieur au second.

Contrairement au filtre, Django requiert que notre méthode prenne deux arguments précis :parser, qui est l'objet en charge de parser le template actuel (que nous n'utiliserons pas ici), ettoken, qui contient les informations sur le tag actuel, comme les paramètres passés.tokencontient de plus quelques méthodes sympathiques qui vont nous simplifier le traitement des paramètres. Par exemple, la méthodesplit_contents()permet de séparer les arguments dans une liste. Il est extrêmement déconseillé d'utiliser la méthode classiquetoken.contents.split(' '), qui pourrait « casser » vos arguments si jamais il y a des chaînes de caractères avec des espaces.
Voici un bref exemple de fonction de compilation :

from django import template

def random(parser, token):
    """ Tag générant un nombre aléatoire, entre les bornes données en arguments """
    # Séparation des paramètres contenus dans l'objet token. Le premier  
    # élément du token est toujours le nom du tag en cours
    try:
        nom_tag, begin, end = token.split_contents()
    except ValueError:
        msg = 'Le tag %s doit prendre exactement deux arguments.' % token.split_contents()[0]
        raise template.TemplateSyntaxError(msg)

    # Nous vérifions ensuite que nos deux paramètres sont bien des entiers
    try:
        begin, end = int(begin), int(end)
    except ValueError:
        msg = 'Les arguments du tag %s sont obligatoirement des entiers.' % nom_tag
        raise template.TemplateSyntaxError(msg)

    # Nous vérifions si le premier est inférieur au second
    if begin > end:
        msg = 'L\'argument "begin" doit obligatoirement être inférieur à l\'argument "end" dans le tag %s.' % nom_tag
        raise template.TemplateSyntaxError(msg)

    return RandomNode(begin, end)

Jusqu'ici, il n'y a qu'une suite de conditions afin de vérifier que les arguments sont bien ceux attendus. Si jamais un tag est mal formé (nombre d'arguments incorrect, types des arguments invalides, etc.), alors le template ne se construira pas et une erreur HTTP 500 sera renvoyée au client, avec comme message d'erreur ce qui est précisé dans la variablemsg, si jamais vous êtes en mode « debug ».

Il ne nous reste plus qu'à écrire la classeRandomNode, qui est renvoyée par la méthode ci-dessus. Vu son appel, il semble évident que sa méthode__init__prend trois arguments :self,beginetend. Comme nous l'avons vu tout à l'heure, cette classe doit également définir une méthoderender(self, context), qui va renvoyer une chaîne de caractères, qui remplacera notre tag dans notre rendu HTML. Cette méthode prend en paramètre le contexte du template, auquel nous pouvons accéder et que nous pouvons éditer.

from random import randint

class RandomNode(template.Node):
    def __init__(self, begin, end):
           self.begin = begin
           self.end = end

    def render(self, context):
           return str(randint(self.begin, self.end))

Comme pour la fonction de structuration, le code en lui-même n'est pas complexe. Nous nous contentons ici de nous souvenir des arguments, et une fois que la fonctionrenderest appelée, nous générons un nombre aléatoire. Il ne faut cependant pas oublier de le transposer en chaîne de caractères, puisque Django fait après une simple concaténation des nœuds !

Il ne nous reste plus qu'à enregistrer notre tag désormais ! Comme pour les filtres, il existe plusieurs méthodes :

  • @register.tag()au début de notre fonction de compilation ;

  • @register.tag(name='nom_du_tag')si jamais nous prenons un nom différent ;

  • register.tag('nom_du_tag', random)pour l'enregistrer après la déclaration de la fonction.

Ici, nous allons garder la première méthode, comme pour les filtres. Au final, notre tag complet ressemble à ceci :

from django import template
from random import randint

register = template.Library()

@register.tag
def random(parser, token):
    """ Tag générant un nombre aléatoire, entre les bornes données en arguments """
    try:
        nom_tag, begin, end = token.split_contents()
    except ValueError:
        msg = 'Le tag %s doit prendre exactement deux arguments.' % token.split_contents()[0]
        raise template.TemplateSyntaxError(msg)

    # Nous vérifions que nos deux paramètres sont bien des entiers
    try:
        begin, end = int(begin), int(end)
    except ValueError:
        msg = 'Les arguments du tag %s sont obligatoirement des entiers.' % nom_tag
        raise template.TemplateSyntaxError(msg)

    # Nous vérifions si le premier est bien inférieur au second
    if begin > end:
        msg = 'L\'argument "begin" doit obligatoirement être inférieur à l\'argument "end" dans le tag %s.' % nom_tag
        raise template.TemplateSyntaxError(msg)

    return RandomNode(begin, end)


class RandomNode(template.Node):
    def __init__(self, begin, end):
        self.begin = begin
        self.end = end

    def render(self, context):
        return str(randint(self.begin, self.end))

Nous allons enfin pouvoir en profiter dans notre template ! En incorporant{% random 1 20 %}, vous allez afficher un nombre compris entre 1 et 20 à chaque appel de la page.
Vous pouvez d'ailleurs tester les cas incorrects cités dans la méthode de compilation. Par exemple,{% random "a" 10 %}affiche la page d'erreur 500 suivante :

Erreur 500 lorsque le tag est mal utilisé
Erreur 500 lorsque le tag est mal utilisé

Passage de variable dans notre tag

Avec le tag que nous venons d'écrire, il n'est possible que de passer des entiers en paramètres. Il est cependant parfois pratique de pouvoir donner des variables en arguments, comme nous avons pu le faire avec{% url %}dans le premier TP.
Pour ce faire, il va falloir revoir un peu l'architecture de notre tag. Une variable est par définition indéterminée, il y a donc plusieurs tests que nous ne pourrons faire qu'au rendu, et non plus à la compilation du tag. Nous allons continuer sur notre tag{% random %}, en lui passant en paramètres deux variables, qui seront définies dans notre vue comme ceci :

def ma_vue(request):
    return render(request, 'template.html', {'begin': 1, 'end': 42})
{% random begin end %}

Nous allons devoir changer notre tag pour interpréter les variables et faire attention au cas où une des variables entrées n'existe pas dans notre contexte (qui est l'ensemble des variables passées au template depuis la vue)… Le problème, comme nous l'avons dit plus haut, c'est que ce genre d'informations n'est disponible qu'au rendu. Il va donc falloir décaler la plupart de nos tests au rendu. Cela pouvait paraître logique de tester nos entrées dès leur réception, mais cela devient tout simplement impossible.

Tout d'abord, supprimons les tests sur le type et la comparaison entrebeginetendde la méthode de compilation, ce qui nous laisse uniquement :

@register.tag
def random(parser, token):
    """ Tag générant un nombre aléatoire, entre les bornes en arguments """
    try:
        nom_tag, begin, end = token.split_contents()
    except ValueError:
        msg = 'Le tag random doit prendre exactement deux arguments.'
        raise template.TemplateSyntaxError(msg)

    return RandomNode(begin, end)

Désormais, notre méthoderenderdans la classeRandomNodesera un peu plus complexe. Nous allons devoir vérifier dedans si la variable passée en paramètre existe et si oui, vérifier s'il s'agit bien d'un entier. Pour ce faire, il existe dans le moduletemplateune classeVariablequi permet de récupérer le contenu d'une variable à partir de son nom dans le contexte. Si jamais nous lui donnons une constante, nous obtiendrons cette même constante en retour, ce qui nous permet de rester compatibles avec notre ancien tag !

from django.template.base import VariableDoesNotExist

class RandomNode(template.Node):
    def __init__(self, begin, end):
        self.begin = begin
        self.end = end

    def render(self, context):
        not_exist = False

        try:
            begin = template.Variable(self.begin).resolve(context)
            self.begin = int(begin)
        except (VariableDoesNotExist, ValueError):
            not_exist = self.begin
        try:
            end = template.Variable(self.end).resolve(context)
            self.end = int(end)
        except (VariableDoesNotExist, ValueError):
            not_exist = self.end

        if not_exist:
            msg = 'L\'argument "%s" n\'existe pas, ou n\'est pas un entier.' % not_exist
            raise template.TemplateSyntaxError(msg)

        # Nous vérifions si le premier entier est bien inférieur au second
        if self.begin > self.end:
            msg = 'L\'argument "begin" doit obligatoirement être inférieur à l\'argument "end" dans le tag random.'
            raise template.TemplateSyntaxError(msg)

        return str(randint(self.begin, self.end))

Quelques explications s'imposent.

  • Notre méthode__init__n'a pas changé, elle ne fait que garder les paramètres passés dans des attributs de l'objet.

  • Au début derender(), nous vérifions les arguments passés. Via la classetemplate, nous récupérons le contenu de la variable ou les constantes 1 et 10 si jamais nous avons{% random 1 10 %}. Nous renvoyons une exception de base de Django,VariableDoesNotExist, si la variable n'existe pas.

  • En cas d'erreur, nous renvoyons les mêmes messages d'erreur qu'avant, comme si nous étions à la compilation.

  • Enfin nous vérifions toujours à la fin que la première borne est bien inférieure à la seconde, et nous retournons notre nombre aléatoire.

Vous pouvez désormais tester votre tag dans n'importe quel sens :

{% random 0 42 %}
{% random begin end %}
{% random begin 42 %}

Mais aussi avec des cas qui ne marchent pas :

{% random a 42 %} avec a = "Bonjour"
{% random begin fin %} où 'fin' n'existe pas

Les simple tags

Il ne nous reste plus qu'à voir comment coder des tags simples, qui prennent des arguments et dont la sortie ne dépend que de ces arguments. C'est le cas de notre tagrandompar exemple, qui renvoie un nombre en ne se basant que sur nos deux paramètres. Il est alors possible de simplifier tout notre tag par :

@register.simple_tag(name='random')  # L'argument name est encore une fois facultatif
def random(begin, end):
    try:
       return randint(int(begin), int(end))
    except ValueError:
       raise template.TemplateSyntaxError('Les arguments doivent nécessairement être des entiers')

Il est aussi possible d'accéder au contexte depuis ce genre de tags, en le précisant à son enregistrement :

@register.simple_tag(takes_context=True)
def random(context, begin, end):
    # …

Pourquoi avoir fait toute cette partie si au final nous pouvons faire un tag en moins de lignes, et plus simplement ?

D'une part, il n'est pas possible de tout faire avec des simple tags. Dès que vous avez besoin d'avoir un état interne par exemple (comme pourcycle), il est plus facile de passer via une classe (notre nœud) qui stockera cet état. De plus, les simple tags fonctionnent en réalité de la même façon que nos tags précédents : un objetSimpleNodeest instancié et sa fonctionrenderne fait qu'appeler notre fonctionrandom.

Finalement, sachez que nous n'avons pas présenté ici tous les types de tags possibles, cela serait beaucoup trop lourd et indigeste. Voici des cas spécifiques :

  • Les tags composés, par exemple{% if %} {% endif %};

  • Les tags incluant d'autres templates, et possédant leur propre contexte ;

  • Et enfin, les tags agissant sur le contexte plutôt que de renvoyer une valeur.

Ces types de tags assez spécifiques sont en revanche décrits dans la documentation officielle, n'hésitez pas à y jeter un coup d'œil si vous en avez besoin.

Quelques points à ne pas négliger

Pour finir, il est important de savoir que les tags renvoient toujours du texte considéré comme sécurisé, c'est-à-dire que le HTML y est interprété. Il est donc important de penser à échapper le HTML quand il est nécessaire, via la fonctionescape, telle que nous l'avons vue avec les filtres.

De plus, les développeurs de Django recommandent de rester vigilants lorsque nous souhaitons garder un état interne avec les tags. En effet, certains environnements fonctionnent de façon multithreadée, et donc un même nœud pourrait être exécuté à deux endroits différents, dans deux contextes différents, dans un ordre indéterminé. Ainsi son état interne est partagé entre les deux contextes et le résultat peut être inattendu.
Dans ce cas, il est conseillé de garder un état interne dans le contexte, via le paramètre disponible dans la fonctionrender, afin de savoir où en était l'exécution pour ce lieu, et non pour l'ensemble du template.
Ce point est assez complexe, pour plus d'informations à ce sujet, consultez la la documentation officielle.

En résumé

  • Django permet aux développeurs d'étendre les possibilités des templates en créant des filtres et des tags.

  • Les filtres et tags créés sont organisés par modules. Pour utiliser un filtre ou un tag il faut charger son module via{% load nom_module %}.

  • Les filtres sont de simples fonctions, prenant en entrée 1 ou 2 arguments et renvoyant toujours une chaîne de caractères.

  • Le contexte des templates est l'ensemble des variables disponibles et utilisables dans un template. Ce contexte est rempli par toutes les fonctions citées dans l'option context_processors de la variable TEMPLATES, puis par la vue appelée et enfin par les éventuels tags du template.

  • Les tags permettent des traitements plus complexes sur les données à afficher. Les tags peuvent avoir une « mémoire », plusieurs arguments, former des blocs…

Example of certificate of achievement
Example of certificate of achievement