Accueil / Blog / Métier / 2020 / Créer un tag d'inclusion avec paramètres dans Django

Créer un tag d'inclusion avec paramètres dans Django

Par Sébastien Corbin publié 22/12/2020
La bibliothèque de tags interne permet d'enregistrer des tags avec paramètres ou des tags d'inclusion de template, voici comment faire les deux en même temps.

Les possibilités de base de Django

Selon la documentation sur les tags de template personnalisés de Django, on peut déclarer un tag d'inclusion, qui peut prendre des paramètres

@register.inclusion_tag('link.html')
def jump_link(link, title):
    return {
        'link': link,
        'title': title,
   }

Avec un template link.html définit comme ceci :

Aller directement à <a href="{{ link }}">{{ title }}</a>.

Qui s'utiliserait de cette façon

{% jump_link "https://makina-corpus.com" "Makina Corpus" %}

Mais si on veut un paramètre qui contienne du HTML, il deviendrait vite fastidieux de faire :

{% jump_link "https://makina-corpus.com" '<span class"icon icon-goto">Aller sur notre site</span>' %}

Remarquez d'ailleurs que les guillemets commencent à poser problème, et que cette méthode ne permet pas une coloration syntaxique ou une lecture facilitée.

Ce qu'on aimerait faire

L'idée serait ici de créer une balise fermable, dont le contenu serait lui-même un paramètre, le tout serait passé ensuite dans un template pour notre composant. Imaginons donc un composant d'accordéon, typiquement une FAQ, basé sur Bootstrap.

Notre template de composant d'accordéon serait

{# faq_question.html #}
<div class="panel panel-default">
  <div class="panel-heading" role="tab" id="question{{ faq_counter }}">
    <h4 class="panel-title">
      <a role="button" data-toggle="collapse" data-parent="#accordion"
         href="#answer{{ faq_counter }}" aria-expanded="true"
         aria-controls="answer{{ faq_counter }}">
        {{ question }}
      </a>
    </h4>
  </div>
  <div id="answer{{ faq_counter }}" class="panel-collapse collapse{% if  active %} in{% endif %}"
       role="tabpanel" aria-labelledby="question{{ faq_counter }}">
    <div class="panel-body">
      {{ answer }}
    </div>
  </div>
</div>

Puis dans un template de votre site, on pourrait utiliser un tag comme ceci :

<h3>Foire aux questions</h3>
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">

  {% if user.is_anonymous %}

    {% faq "J'ai oublié mon mot de passe" active=True %}
      <p>
        Si vous avez oublié votre mot de passe de votre compte, utilisez le
        <a href="{% url 'account:password' %}">
          formulaire de réinitialisation de mot de passe
        </a>.
      </p>
      <p>Note : nous ne stockons pas les mots de passe, inutile de nous les demander !</p>
    {% endfaq %}

  {% else %}

    {% faq "Je souhaite changer mon mot de passe" %}
      <p>
        Si vous souhaitez choisir votre propre mot de passe, déconnectez-vous et utilisez le
        <a href="{% url 'account:password' %}">
          formulaire de réinitialisation de mot de passe
        </a>
      </p>
    {% endfaq %}

  {% endif %}
</div>

C'est un cas d'usage qu'on pourrait retrouver assez souvent : il permet de mixer paramètres normaux, inclusion de template et rendu du contenu du tag.

L'implémentation

Un tag complexe se base sur une classe de type TagHelperNode qui s'occupe de résoudre les arguments passés au tag. Pour ne pas réinventer la roue, nous allons sous-classer template.library.InclusionNode qui prend en paramètre un template et gère les arguments. Il faut juste rajouter la mécanique de rendu du contenu du tag.

Une fois le traitement du template défini dans la classe, il faut déclarer le décorateur comme peut le faire Django avec @register.inclusion_tag. Celui-ci décorera la fonction déclarée comme tag (dans le dossier templatetags) et analysera le contenu et instanciera la classe de traitement.

En regardant le code du décorator, vous vous direz peut-être "Mais comment il a sorti ça ?!", eh bien là encore, je n'ai pas inventé la roue : il s'agit juste du corps de django.template.library.Library.inclusion_tag adapté pour analyser le contenu.

import functools
from inspect import getfullargspec

from django import template

class InclusionWithBodyNode(template.library.InclusionNode):
    def __init__(self, nodelist, func, takes_context, args, kwargs, filename, context_name):
        super().__init__(func, takes_context, args, kwargs, filename)
        self.nodelist = nodelist
        self.context_name = context_name

    def render(self, context):
        context[self.context_name] = self.nodelist.render(context)
        return super().render(context)


def register_inclusion_with_body(filename, takes_context=None, name=None, context_name='body'):
    def dec(func):
        params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec(func)
        function_name = name or getattr(func, '_decorated_function', func).__name__

        @functools.wraps(func)
        def compile_func(parser, token):
            bits = token.split_contents()[1:]
            args, kwargs = template.library.parse_bits(
                parser, bits, params, varargs, varkw, defaults,
                kwonly, kwonly_defaults, takes_context, function_name,
            )
            nodelist = parser.parse((f'end{function_name}',))
            parser.delete_first_token()
            return InclusionWithBodyNode(
                nodelist, func, takes_context, args, kwargs, filename, context_name
            )

        register.tag(function_name, compile_func)
        return func
    return dec

Voyons maintenant comment utiliser ce décorateur dans vos fichiers templatetags :

@register_inclusion_with_body('faq_question.html', takes_context=True, context_name='answer')
def faq(context, question, active=False):
    """
    Template tag for faq question
    :param question: the question title
    :param active: If the question should be un-collapsed by default
    :type context: django.template.context.RequestContext
    """
    context.setdefault('faq_counter', 0)
    context['faq_counter'] += 1
    context['active'] = active
    context['question'] = question
    return context
ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
Présentation de django-admin-watchdog Présentation de django-admin-watchdog 12/11/2020

Comment garder une trace des erreurs Django en toute simplicité.

Présentation de django-tracking-fields Présentation de django-tracking-fields 03/11/2020

Suivi de modification d'objets Django

Présentation de Django-Safedelete Présentation de Django-Safedelete 09/07/2013

Masquage d'objets en base de données une alternative à la suppression définitive.

Wagtail : Comment écrire les templates (partie 3) Wagtail : Comment écrire les templates (partie 3) 18/07/2016

Il n'y a pas de vue à proprement parlé dans Wagtail. Tout est en fait géré dans le modèle. ...

Wagtail : Utiliser le modèle Page ainsi que son Manager (partie 2) Wagtail : Utiliser le modèle Page ainsi que son Manager (partie 2) 12/07/2016

Le modèle Page contient plusieurs méthodes spécifiques à l'outil Wagtail. C'est également le ...