Pelican : maîtrisez le plugin subcategory!

28 août 2014

Pelican est donc le nouveau moteur du site, mais de base il n'offrait pas toutes les fonctions que je souhaitais. Heureusement il est pouvru d'une liste assez conséquente de plugins, et j'ai pu trouver ce qui me manquait.

Le plugin auquel je vais m'intéresser aujourd'hui est probablement celui qui m'a donné le plus de fil à retordre : le plugin subcategory. Sa fonction est très simple : de base pelican ne permet qu'un niveau de catégorie, par exemple Développement. Subcategory permet quand à lui d'avoir une hiérarchie plus détaillée, par exemple Développement / C# et Développement / Python.

Le problème c'est que la documentation fournie dans le README du plugin indique :

  • Qu'il est possible de créer une page dédiée à une sous-catégorie pour en lister tous les articles.
  • Comment afficher un fil d'ariane (breadcrumb en anglais) détaillant les différents niveaux de la sous-catégorie dans le template article.html.

Or ce n'était à mon avis pas suffisant pour profiter pleinement du plugin. Voici à mon avis ce qui manquait :

  • Pouvoir afficher le même fil d'ariane sur la page dédiée d'une sous catégorie.
  • Pouvoir lister les sous catégories dans le menu principal.

Ne connaissant ni le langage python ni la structure de pelican et de ses plugins, j'ai dû avancer à taton pour arriver au résultat espéré. Mais avec un peu de persévérence je suis finalement arrivé au résultat désiré.

Voici donc la démarche à suivre :

Template subcategory.html

Ce template est utilisé pour lister tous les articles d'une sous-catégorie. Pour respecter le thème que j'avais mis en place, je devais pouvoir afficher la sous-catégorie dans le titre de la fenêtre, comme sous-titre de la page, et dans le fil d'ariane, avec un lien pour chaque "étage" de la hiérarchie, comme dans l'exemple suivant :

La seule variable que nous ayons à disposition dans subcategory.html s'appelle subcategory. Après un peu décryptage il s'avère que cette variable possède plusieurs propriétés :

subcategory.name        # Le nom complet de la sous-catégorie, incluant celui de ses parents
subcategory.shortname   # Le nom de la sous-catégorie seul
subcategory.url         # L'url de la page listant les articles de cette sous-catégorie
subcategory.parent      # Pointe vers la sous-catégorie ou la catégorie parente. Si subcategory.parent est une catégorie, alors subcategory.parent.parent n'est pas définie.

C'est un bon début, et grace au moteur de template utilisé par pelican (Jinja2) nous allons pouvoir arriver à nos fins, plus particulièrement en utilisant une macro récursive. Voici déjà le code, j'essairai de l'expliquer au mieux après :

{% macro breadcrumb_recurs(scat) -%}
    {%- if scat.parent is defined -%}
        {{ breadcrumb_recurs(scat.parent) }}
    {%- endif %}

    {% if scat.shortname is defined %}
        <span>/</span> <a href="{{ SITEURL }}/{{ scat.url }}">{{ scat.shortname }}</a>
    {% else %}
        <span>/</span> <a href="{{ SITEURL }}/{{ scat.url }}">{{ scat.name }}</a>
    {% endif %}
{%- endmacro %}

La marco prend donc un paramètre scat qui pourra être de type subcategory ou category (dans le cas où l'appel est effectué depuis le premier niveau de sous-catégorie).

Etant donné que l'on va parcourir les sous-catégorie à contre-sens (en partant du plus bas niveau et en remontant par l'intermédiaire de scat.parent, alors qu'à l'affichage on souhaite commencer par le plus haut niveau puis descendre dans la hiérarchie), on va d'abord effectuer la récursivité avant d'afficher les infos liées au niveau actuel.

Donc si scat.parent est définie, on appel de nouveau la marco en passant le parent en paramètre.

On affiche alors le lien vers le niveau actuel, en effectue d'abord un test pour voir si scat.shortname est défini. S'il ne l'est pas, c'est que nous affichons la catégorie, il faut alors utiliser scat.name à la place.

Il ne reste plus qu'à appeler la macro à l'endroit souhaité, en ajoutant éventuellement la racine avant :

<a href="{{ SITEURL }}">Racine</a>
{%- if subcategory %}
    {{ breadcrumb_recurs(subcategory) }}
{%- endif %}

Menu principal

Pour pouvoir lister les sous-catégories au même titre que les catégories dans le menu principal, c'est un peu plus compliqué. En effet, là (on se trouve dans le template base.html) on n'as plus accès à rien de spécial concernant les sous catégories.

Heureusement, lorsque l'on parcourt les articles, on a accès à la variable article.subcategory. Cela va permettre de faire une petite pirouette pour arriver à nos fins : créer une macro qui va parcourir tous les articles à la recherche de sous-catégories dont le parent est une catégorie/sous-catégorie passée en paramètre. Cette macro sera également récursive pour parcourir tous les niveau de la hiérarchie de sous-catégories.

{% macro Search_scat(parent, site_url, articles) %}
    {% set children = [] %}
    {% for article in articles %}
        {% for subcategory in article.subcategories %}
            {% if subcategory.parent.name == parent.name and subcategory not in children %}
                {% do children.append(subcategory) %}
            {% endif %}
        {% endfor %}
    {% endfor %}

    {% if children|length > 0 %}
        <ul>
            {% for subcategory in children %}
                <li><a href="{{ site_url }}/{{ subcategory.url }}">{{ subcategory.shortname }}</a>
                    {{- Search_scat(subcategory, site_url, articles) -}}
                </li>
            {% endfor %}
        </ul>
    {% endif %}
{% endmacro %}

Etant donné que j'ai stocké mes macros dans un template à part, je suis obligé de passer en paramètre les variables site_url et articles, car celles-ci ne sont pas définies dans mon template macro.html.

On commence par déclarer un tableau vide children qui contiendra toutes les sous-catégories ayant pour parent la catégorie/sous-catégorie parent passée en paramètre.

On parcourt ensuite toutes les sous-catégories de tous les articles, et si le parent correspond au paramètre parent et n'a pas encore été détecté, on l'ajoute dans la liste children.

A la fin, si la liste n'est pas vide, on affiche donc sous forme de liste le résultat. Pour chaque sous catégorie, on appelle de nouveau (récursivité) la marco pour éventuellement détecter des enfants.

Il ne reste plus qu'à appeler la macro depuis le template base.html :

{% for cat, null in categories %}
<li><a href="{{ SITEURL }}/{{ cat.url }}">{{ cat }}</a>
    {% if all_articles|count > 0 %}
        {{- macros.Rechercher_Souscat(SITEURL, cat, all_articles) -}}
    {% else %}
        {{- macros.Rechercher_Souscat(SITEURL, cat, articles) -}}
    {% endif %}
</li>
{% endfor %}

Petite remarque sur le if utilisé ici : la variable articles ne va pas contenir la même chose suivant le template depuis lequel on l'utilise. En effet depuis index.html, cette variable ca contenir tous les articles, mais depuis tag.html ou category.html elle ne contiendra que les articles concernés. Et si on ne parcourt pas tous les articles, on risque de manquer certaines sous-catégories, et donc l'affichage sera incomplet.

Heureusement la documentation indique que si la variable articles de base est écrasée par un autre template, la variable all_articles prendra sa place.

Cette manip permet donc d'aller chercher la liste de tous les articles, quelque soit la page en cours.

J'ai bien conscience que ce procédé peut être lourd (pour chaque catégorie / sous-catégorie, on parcourt l'intégralité des articles à la recherche d'enfants), mais je n'ai pas trouvé mieux pour obtenir ce résultat. Peut-être serait-il possible d'améliorer le plugin pour prendre en compte ces contraintes, mais n'ayant pas de connaissances en python, ce n'est pas à ma portée.