Using Python str, datetime, lists and sets to group dates
I'm in the process of writing my own blog app (project 'belleville'), one that will eventually replace the Wordpress blog that this site is using, and one of the things I need is a templatetag for the sidebar that lists archive dates, grouped by month - it should look something like the "Archive" list just there to the right, or like this:
The problem is that the date field I have to work with is a MySQL datetime column:
created_at = models.DateTimeField(auto_now_add=True)
... and I only need a unique list of year-month values for all of my active posts.
Solution #1
Solution #1 means returning the entire list of posts (this is going to be an expensive query as my number of posts grows), then assigning a 'month' value to each post in the returned list using a combination of Python datetime attributes and str methods.
import datetime
from django import template
from myentries.models import Post
register = template.Library()
def sidebar_date_list():
posts = Post.objects.filter(publish=1)
for post in posts:
post.month = str(post.created_at.year)+'-'+str(post.created_at.month).rjust(2, '0')
return {'posts': posts}
register.inclusion_tag('date_list.html')(sidebar_date_list)
This gives me a long list of all the existing dates in the format I need:
2008-09
2008-09
2008-09
2008-09
2008-09
2008-09
2008-08
2008-08
2008-08
2008-08
2008-08
2008-08
2008-07
2008-07
2008-07
2008-07
2008-07
2008-07
2008-07
2008-07
2008-07
2008-07
2008-07
2008-06
2008-06
2008-06
Then I can do the date grouping in the template using the Django's regroup tag:
{% if posts %}
<h2>Archive</h2>
<ul>
{% regroup posts by month as month_list %}
{% for month in month_list %}
<li><a href="/date/{{ month.grouper }}">{{ month.grouper }}</a></li>
{% endfor %}
</ul>
{% endif %}
Solution #2
But let's face it, all that logic should not be happening in the template. So, back to the template tag. I'd love to be able to do a select and just group by date, but since my 'created_at' column is a datetime, it would just return a unique date for every post anyway. Instead, I'm using using those string and datetime methods to append all the dates to a new list:
def sidebar_date_list():
posts = Post.objects.filter(publish=1).order_by('-created_at')
month_list = []
for post in posts:
post.month = datetime.datetime(post.created_at.year, post.created_at.month, 1)
month_list.append(post.month)
months = set(month_list)
months = list(months)
months.sort(reverse=True)
return {'months': months}
register.inclusion_tag('date_list.html')(sidebar_date_list)
The month list looks like this:
[datetime.datetime(2008, 9, 1, 0, 0), datetime.datetime(2008, 9, 1, 0, 0), datetime.datetime(2008, 9, 1, 0, 0),
datetime.datetime(2008, 9, 1, 0, 0), datetime.datetime(2008, 9, 1, 0, 0), datetime.datetime(2008, 9, 1, 0, 0),
datetime.datetime(2008, 8, 1, 0, 0), datetime.datetime(2008, 8, 1, 0, 0), datetime.datetime(2008, 8, 1, 0, 0),
datetime.datetime(2008, 8, 1, 0, 0), datetime.datetime(2008, 8, 1, 0, 0), datetime.datetime(2008, 8, 1, 0, 0),
datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 7, 1, 0, 0),
datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 7, 1, 0, 0),
datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 7, 1, 0, 0),
datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 6, 1, 0, 0),
datetime.datetime(2008, 6, 1, 0, 0), datetime.datetime(2008, 6, 1, 0, 0)]
But converting the list to a set eliminates all the duplicate elements:
months = set(month_list)
set([datetime.datetime(2008, 6, 1, 0, 0), datetime.datetime(2008, 9, 1, 0, 0),
datetime.datetime(2008, 7, 1, 0, 0), datetime.datetime(2008, 8, 1, 0, 0)])
Then convert the set back to a list so it can be sorted:
months = list(months)
months.sort(reverse=True)
And you can iterate over that list in the template, it's just that simple:
{% if months %}
<h2>Archive</h2>
<ul>
{% for month in months %}
<li><a href="/date/{{ month|date:"Y-m" }}">{{ month|date:"Y-m" }}</a></li>
{% endfor %}
</ul>
{% endif %}
Comment by
Malcolm Tredinnick
on Oct 02, 2008:
I like your explanation and logic, going back to avoid a mess in the template. But are you aware that you've more or less just reinvented the dates() method on querysets? Something like Post.objects.date('created_at', 'month') will be the same as your Python function, except it will happen at the SQL level.
Comment by
omtv
on Oct 17, 2008:
is there method to do this kind of stuff more effeciently?
Comment by
bbolli
on Oct 17, 2008:
I'd skip the SQL order_by, since you're going to sort the month list anyway later on.
And, oh, you could use a list comprehension to eliminate the for loop ;-)
But Malcolm is (more!) right, at any rate.
Comment by
Hugh Brown
on Jun 19, 2009:
How about:
def sidebar_date_list():
from collections import defaultdict
month_dict = defaultdict(int)
for post in Post.objects.filter(publish=1):
year_month = datetime.date(post.created_at.year, post.created_at.month, 1)
month_dict[year_month] += 1
months = [ (k, month_dict[k]) for k in sorted(month_dict.keys(), reverse=True) ]
return {'months': months}
Then you get all the goodness of a set plus sorting in descending order *and* you get a count of the articles in that month for free.
You'd have to change the template since months is now a list of tuples of date and int.
Comment by
Hugh Brown
on Jun 19, 2009:
Alternatively, I think this would work, too.
def sidebar_date_list():
from itertools import groupby
data = [datetime.date(post.created_at.year, post.created_at.month, 1) for post in Post.objects.filter(publish=1).order_by('-created_at') ]
return {'months': [k for k,_ in groupby(data)]}
Notice that the SQL has to be sorted for groupby() to use.
Comment by
Hugh Brown
on Jun 19, 2009:
And it looks like Malcolm was right. This would do the trick:
def sidebar_date_list():
return {'months': Post.objects.filter(publish=1).dates('created_at', 'month', order='DESC')}
http://docs.djangoproject.com/en/dev/ref/models/querysets/#dates-field-kind-order-asc
Comment by
Rollladen
on May 26, 2011:
For me that second solution works better, thanks for sharing!

