Monthly Calendar in Django

June 13, 2010

I wanted to represent contest events on my brass band results website using a month-to-view calendar. This proved to be easier than I thought, as Python comes with built in support for generating calendars in HTML.

HTMLCalendar

First of all we need to create a subclass of HTMLCalendar which will generate the HTML for us.

from django.utils.html import conditional_escape as esc
from django.utils.safestring import mark_safe
from itertools import groupby
from calendar import HTMLCalendar, monthrange

class ContestCalendar(HTMLCalendar):

    def __init__(self, pContestEvents):
        super(ContestCalendar, self).__init__()
        self.contest_events = self.group_by_day(pContestEvents)

    def formatday(self, day, weekday):
        if day != 0:
            cssclass = self.cssclasses[weekday]
            if date.today() == date(self.year, self.month, day):
                cssclass += ' today'
            if day in self.contest_events:
                cssclass += ' filled'
                body = []
                for contest in self.contest_events[day]:
                    body.append('<a href="%s">' % contest.get_absolute_url())
                    body.append(esc(contest.contest.name))
                    body.append('</a><br/>')
                return self.day_cell(cssclass, '<div class="dayNumber">%d</div> %s' % (day, ''.join(body)))
            return self.day_cell(cssclass, '<div class="dayNumber">%d</div>' % day)
        return self.day_cell('noday', '&nbsp;')

    def formatmonth(self, year, month):
        self.year, self.month = year, month
        return super(ContestCalendar, self).formatmonth(year, month)

    def group_by_day(self, pContestEvents):
        field = lambda contest: contest.date_of_event.day
        return dict(
            [(day, list(items)) for day, items in groupby(pContestEvents, field)]
        )

    def day_cell(self, cssclass, body):
        return '<td class="%s">%s</td>' % (cssclass, body)

Django View

Next, we need to create an instance of this class from our view, passing in the list of objects from the specified month. Here I'm using two view functions, one of which calls the other. There is also a named_month function which returns the name of a month given the month number.

monthrange(pYear, pMonth) returns a tuple of the day numbers in the month, ie (1,31) for December.

from calendar import monthrange

def named_month(pMonthNumber):
    """
    Return the name of the month, given the month number
    """
    return date(1900, pMonthNumber, 1).strftime('%B')

def home(request):
    """
    Show calendar of events this month
    """
    lToday = datetime.now()
    return calendar(request, lToday.year, lToday.month)

def calendar(request, pYear, pMonth):
    """
    Show calendar of events for specified month and year
    """
    lYear = int(pYear)
    lMonth = int(pMonth)
    lCalendarFromMonth = datetime(lYear, lMonth, 1)
    lCalendarToMonth = datetime(lYear, lMonth, monthrange(lYear, lMonth)[1])
    lContestEvents = ContestEvent.objects.filter(date_of_event__gte=lCalendarFromMonth, date_of_event__lte=lCalendarToMonth)
    lCalendar = ContestCalendar(lContestEvents).formatmonth(lYear, lMonth)
    lPreviousYear = lYear
    lPreviousMonth = lMonth - 1
    if lPreviousMonth == 0:
        lPreviousMonth = 12
        lPreviousYear = lYear - 1
    lNextYear = lYear
    lNextMonth = lMonth + 1
    if lNextMonth == 13:
        lNextMonth = 1
        lNextYear = lYear + 1
    lYearAfterThis = lYear + 1
    lYearBeforeThis = lYear - 1

    return render_auth(request, 'calendar/home.html', {'Calendar' : mark_safe(lCalendar),
                                                       'Month' : lMonth,
                                                       'MonthName' : named_month(lMonth),
                                                       'Year' : lYear,
                                                       'PreviousMonth' : lPreviousMonth,
                                                       'PreviousMonthName' : named_month(lPreviousMonth),
                                                       'PreviousYear' : lPreviousYear,
                                                       'NextMonth' : lNextMonth,
                                                       'NextMonthName' : named_month(lNextMonth),
                                                       'NextYear' : lNextYear,
                                                       'YearBeforeThis' : lYearBeforeThis,
                                                       'YearAfterThis' : lYearAfterThis,
                                                   })

HTML Template

The template simply displays {{Calendar}}, but there are also links for next and previous month and year.

<h1>Calendar</h1>
<table width="100%">
    <tr>
        <td width="20%" align="left">
          &lt;&lt; <a href="/calendar/{{PreviousYear}}/{{PreviousMonth}}/">{{PreviousMonthName}} {{PreviousYear}}</a>
        </td>
        <td width="20%" align="left">
          &lt;&lt; <a href="/calendar/{{YearBeforeThis}}/{{Month}}/">{{MonthName}} {{YearBeforeThis}}</a>
        </td>
        <td width="20%" align="center"><a href="/calendar">Today</a></td>
        <td width="20%" align="right">
          <a href="/calendar/{{YearAfterThis}}/{{Month}}/">{{MonthName}} {{YearAfterThis}}</a> &gt;&gt;
        </td>
        <td width="20%" align="right">
          <a href="/calendar/{{NextYear}}/{{NextMonth}}/">{{NextMonthName}} {{NextYear}}</a> &gt;&gt;
        </td>
    </tr>
</table>
<div id="calendar">
  {{Calendar}}
</div>

CSS

Finally, here's the CSS that I applied to get a nice look and feel consistent with the rest of the website.

#calendar table {
    width: 100%;
}

#calendar table tr th {
    text-align: center;
    font-size: 16px;
    background-color: #316497;
    color: #99ccff;
}

#calendar table tr td {
    width: 10%;
    border: 1px solid #555;
    vertical-align: top;
    height: 120px;
    padding: 2px;
}

#calendar td.noday {
    background-color: #eee;
}

#calendar td.filled {
    background-color: #99ccff;
}

#calendar td.today {
    border: 4px solid #316497;
}

#calendar .dayNumber {
    font-size: 16px !important;
    font-weight: bold;
}

#calendar a {
    font-size: 10px;
}

References