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

Django    Python    2008-09-21

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