Grouping in Django templates

April 28, 2019

I’ve recently deployed a tiny changelog app in one of my Django projects. The models.py file looks like this:

# changelog/models.py (truncated)
class ChangeLog(models.Model):

    IMPROVEMENT = ('improvement', 'improvement')
    FEATURE = ('feature', 'feature')
    BUG = ('bugfix', 'bug fix')

    CHOICES = (IMPROVEMENT, FEATURE, BUG,)

    title = models.CharField(max_length=560)
    description = models.TextField(null=True, blank=True)
    category = models.CharField(choices=CHOICES, max_length=215)
    display_date = models.DateTimeField(editable=True)

Nothing special so far. The only slight oddity here is display_date: unlike what its name suggests, it’s actually a datetime field.

In this app’s main template, I wanted to sort (in reverse order) and group items by the date portion of their display_date so the output would be something like this:

<div class="changelog-day">
  <h3 class="changelog-heading">March 17, 2019</h3>
  <p>changelog #2 created on this date</p>
  <p>changelog #1 created on this date</p>
</div>

<div class="changelog-day">
  <h3 class="changelog-heading">March 15, 2019</h3>
  <p>changelog #1 created on this date</p>
</div>

So, ChangeLog objects that have the same date should all be inside the same div. This is the view I had wired up at the time:

# views.py
def changelog_index(request):
    changelog_items = ChangeLog.objects.order_by('-display_date')

    context = {
        'changelog_items': changelog_items
    }

    return render(request, 'changelog_index.html', context)

order_by takes care of sorting the changelog items in reverse chronological order. But there’s a step missing here: how to group these changelog items by date?

Grouping in the view

One way is to group inside the view:

# views.py - grouping in view
def changelog_index(request):
    changelogs = ChangeLog.objects.order_by('-display_date')
    
    dates_and_items = {}
    
    for changelog in changelogs:
        current_key = changelog.display_date.date()  # the item's date
        dates_and_items.setdefault(current_key, []).append(changelog)
    
    context['dates_items'] = dates_and_items

    return render(request, 'changelog_index.html', context)

Don’t worry if you don’t get what setdefault is doing, just know that this view creates a dictionary with dates as keys, and each such key holds a list of ChangeLog objects belonging to that date.

And then changelong_index.html would include something like this:

{# changelong_index.html - grouping in view #}
{% for date, item_list in dates_items.items %}
  <div class="changelog-day">
    <h3 class="changelog-heading"><b>{{ date }}</b></h3>
    {% for changelog in item_list %}        
      <p>{{ changelog.title }} - {{ changelog.description }}<p>
    {% endfor %}        
  </div>
{% endfor %}

Here we are iterating over each date in our dictionary, and in the nested for loop, we iterate over this key’s item_list.

Grouping in the template

The other option is to leave the views.py file untouched. Reminder:

# views.py - grouping in template
def changelog_index(request):
    changelog_items = ChangeLog.objects.order_by('-display_date')

    context = {
        'changelog_items': changelog_items
    }

    return render(request, 'changelog_index.html', context)

And use Django’s built-in {% regroup %} tag:

{# changelong_index.html - grouping in template #}
{% regroup changelog_items by display_date.date as dates_items %}
{% for date in dates_items %}
  <div class="changelog-day">
    <h3 class="changelog-heading"><b>{{ date.grouper }}</b></h3>
    {% for changelog in date.list %}
      <p>{{ changelog.title }} - {{ changelog.description }}<p>
    {% endfor %}
  </div>
{% endfor %}

Recognize that the markup is almost identical to the previous template snippet. Let’s go over the differences:

{% regroup changelog_items by display_date.date as dates_items %}

regroup is an aptly named tag. It takes a list-like collection, and regroups it by a common attribute. Above, we’re regrouping the changelog_items QuerySet by its items’ display_date.date, and calling this regrouped collection dates_items which we can then use in the for loop.1

If we wanted to regroup changelogs by category, we’d write:

{# group by each changelong_item's category #}
{% regroup changelog_items by category as cats_items %}

Anyway…we take this regrouped collection and iterate over it like so:

{# changelong_index.html - grouping in template, continued #}
{% for date in dates_items %}
  <div class="changelog-day">
    <h3 class="changelog-heading"><b>{{ date.grouper }}</b></h3>
    {% for changelog in date.list %}
      <p>{{ changelog.title }} - {{ changelog.description }}<p>
    {% endfor %}
{% endfor %}

Of special note are date.grouper in the H3 tag, and the date.list we iterate over in the nested for loop. These are objects that regroup creates: grouper is the item that was grouped-by, and list is the list of objects that belong to this group.

You can think of grouper as a key in the dictionary, and list as the value, which is list of items belonging to that key”. In our case, each grouper is a distinct date, which has a list of changelog items.

Important caveat

Note that {% regroup %} itself does not sort the collection it regroups. In our case, the ChangeLog objects were sorted in the view, so regroup works as expected. If they weren’t, regroup would create duplicate sections with the same date.

But there is a way to sort in the template, using the dictsort/dictsortreversed template tag:

{% regroup changelog_items|dictsortreversed:"display_date" by display_date.date as sorted_dates %}

Here, receiving a an unordered collection changelog_items, we sort by display_date in descending order (from latest to first), and then group by the display_date.date.2

Where to group?

I don’t proclaim to know the definitive answer, and I don’t think there is one. In the case above, grouping in the template involved less effort and took less time to write. One more possible case to utilize regroup is when you want to sort the same QuerySet by different attributes in the same view. In other cases, different considerations (like speed) may favor grouping in the view. Always weigh and balance.

As I wrote earlier this month, I generally prefer my templates to be as dumb as possible, but every rule has its exception, and it’s good to have regroup in one’s arsenal when the situation calls for it.


  1. Note that we never passed dates_items from the view.

  2. Using display_date (a datetime) in dictsortreversed means that while items are grouped by dates, more recent items within the same date are displayed first.