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', ' ') 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"> << <a href="/calendar/{{PreviousYear}}/{{PreviousMonth}}/">{{PreviousMonthName}} {{PreviousYear}}</a> </td> <td width="20%" align="left"> << <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> >> </td> <td width="20%" align="right"> <a href="/calendar/{{NextYear}}/{{NextMonth}}/">{{NextMonthName}} {{NextYear}}</a> >> </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; }