Using Python str, datetime, lists and sets to group dates

Sep 21, 2008 Django Python

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 %}