Makina Blog

Le blog Makina-corpus

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


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

Actualités en lien

Image
Django PostgreSQL
07/11/2023

Utiliser des fonctions PostgreSQL dans des contraintes Django

Cet article vous présente comment utiliser les fonctions et les check constraints PostgreSQL en tant que contrainte sur vos modèles Django.

Voir l'article
Image
Encart Django
06/11/2023

Comment migrer vers une version récente de Django ?

Que ce soit pour avoir les dernières fonctionnalités ou les correctifs de sécurité, rester sur une version récente de Django est important pour la pérennité de son projet.

Voir l'article
Image
Agrégateur Geotrek
08/06/2023

Le projet Agrégateur : fusionner des bases de données Geotrek

Le partage et la diffusion des données font partie des problématiques historiques au cœur du projet Geotrek.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus