Jinja Extensions and Reverse Dict Sort Using Pelican

Jul 2014
Fri 04

Additional Jinja Commands and Modifying DictSort

Although Pelican with Jinja does a great job at harnessing the power the power of Python to great and manage web content, there are occasions where you need a level of customisation that pushes the templates to the limit. This is not a flaw of the templates - they're not designed to be fully fledged python scripts. However, I have sometimes found myself frustrated with what seemed to be the simplest task. For instance, if our website's articles are stored by category in a list of tuples (as in Pelican), we can iterate through each category:

{% for cat_name, article_list in categories %}
    <p> cat {{ cat_name }} narticles  {{article_list|count}} </p>
{% endfor %}

The above will simply print the category name followed by the number of articles in that category. We utilise the Jinja |count filter to return the length of the list. But what if we wanted to iterate through in order of the most popular categories? As categories is a list of tuples, we can't simply use the |sort filter:

{% for cat_name, article_list in categories|sort %}

and even if we could this would simply order alphabetically by category name. In pure Python, there are lots of ways to do this; creating a new array, using a lambda function or best using the in built sorted function. In a template however, our options are limited.

The solution that I'm using is to create a new dictionary - we need some sort of associative array to both contain the category and the number of articles (which we sort by). Using a new dictionary we should be able to use Jinja's |dictsort filter to sort the dictionary by key or value.

The first hurdle I came across was dynamically creating the dictionary in the template. The main problem is the template cannot contain declarations of the form:

{% set cat_dict = {} %}
{% for cat_name, article_list in categories %}
    {% set cat_dict[cat_name] = article_list|count %}
{% endfor %}

To avoid this type of declaration, we can use the built-in update method of a Python dict:

a = {}
{'b': 1}
{'c': 2, 'b': 1}

However, we cannot use the Jinja set method to call update, we must use an extension to Jinja.

Jinja Extensions

In addition to the default Jinja template operations there are custom extensions available. I'll not focus on the details here, instead introduce the extension I've used to tackle the problem above; jinja2.ext.do.

jinja2.ext.do allows do declarations in a template - a non returning call. This allows us to do the following:

{% for cat_name, article_list in categories %}
    {% do cat_dict.update({cat_name:article_list|count})%}
{% endfor %}

Normally, when you are explicitly using Jinja, you import the extensions into your Python script using the jinja2.Environment class:

jinja_env = Environment(extensions=['jinja2.ext.do'])

As we are using Jinja through Pelican, we cannot do this without hacking the Pelican source code. Fortunately, the guys over at Pelican added an optional global list to pelicanconf.py in which you can add your required extensions:


JINJA_EXTENSIONS =['jinja2.ext.do',]

Dict Sorting

Now we have our new dict containing the category and the associate number of articles, we can sort using Jinja's |dictsort and iterate:

{% for cat_name,count in cat_dict|dictsort(by="value") %}
    <p> cat {{ cat_name }} narticles {{count}} </p>
{% endfor %}

A final annoying quirk here is that this is restricted to sort in ascending order i.e. the least populated category will appear at the top of the list. Rarely would you want this. Why I think this is a bit annoying is that Jinja's dictsort method simply calls Python's sorted method - a method that accepts a reverse keyword. There requires a very simple modification to the Jinja source to allow for reverse sorting:


def do_dictsort(value, case_sensitive=False, by='key',reverse=False):
"""Sort a dict and yield (key, value) pairs. Because python dicts are
unsorted you may want to use this function to order them by either
key or value:

.. sourcecode:: jinja

    {% for item in mydict|dictsort %}
        sort the dict by key, case insensitive

    {% for item in mydict|dictsort(true) %}
        sort the dict by key, case sensitive

    {% for item in mydict|dictsort(false, 'value') %}
        sort the dict by key, case insensitive, sorted
        normally and ordered by value.
if by == 'key':
    pos = 0
elif by == 'value':
    pos = 1
    raise FilterArgumentError('You can only sort by either '
                              '"key" or "value"')
def sort_func(item):
    value = item[pos]
    if isinstance(value, string_types) and not case_sensitive:
        value = value.lower()
    return value

return sorted(value.items(), key=sort_func, reverse=reverse)

With this modification in place, we can now print our categories in descending order or popularity:

{% for cat_name,count in cat_dict|dictsort(by="value",reverse=True) %}
    <p> cat {{ cat_name }} narticles {{count}} </p>
{% endfor %}