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; }
References
Comments
On November 1,2010, dariush said:
Thanks. Can you please explain, how the appropiate event model looks like?
Or is it possible to use this calendar without any events at first?
Best regards Dariush
On November 1,2010, Tim said:
This generates static HTML, so you'd need to include any events you want into the HTML. You can add whatever you like. In this case I'm just adding some a href's into the HTML. The previous/next year/month links are done in the django template outside the calendar HTML.
On November 1,2010, dariush said:
My question regards the assignment of the variable lContestEvents. In my understanding you get its value from the database table ContestEvent. I am Django Beginner, please can you clarify what is happening at the line of code during the assignment of lContestEvents.
Thank you in advance !
On November 1,2010, Tim said:
The calendar django view is requested passing in a year and month - this is the year and month that you want the calendar displayed for. These are in the URL.
We then work out a datetime for the start and end of this month.
lCalendarFromMonth = datetime(lYear, lMonth, 1) lCalendarToMonth = datetime(lYear, lMonth, monthrange(lYear, lMonth)[1])Next, we do a select on the database for all the contest events that fall between the 1st and last day of this month.
lContestEvents = ContestEvent.objects.filter(date_of_event__gte=lCalendarFromMonth, date_of_event__lte=lCalendarToMonth)There are two filters here. One makes sure the date of the thing we are displaying is greater than or equal to the first of the month. The second makes sure the date of the thing we are displaying is less than or equal to the last day of the month.
Hope that helps!
Comments for this entry are closed.



