Grouping in Django templates
April 28, 2019I’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.