William John Bert

Loves to write words and code.

Simple Django Event Calendar

Background

I’ve been teaching myself Django by writing a web app that tracks reading series in cities: Readsr. A reading series is a kind of recurring event. It defines a time, location, and recurrence rule such as first Monday of the month. Writing a list view to display upcoming readings was easy, but I also wanted to create a calendar view, similar to what Google Calendar provides. That took a little more work.

Existing Solutions

First I searched for existing Django calendar solutions. I found several. Swingtime and django-agenda seemed very well thought out and comprehensive, but were also perhaps overkill for what I needed. django-gencal doesn’t appear to be maintained and I had trouble understanding the documentation (though you may have better results, as I am slow).

A Way Forward

I found that Python’s calendar module has a built template called HTMLCalendar, which sounded promising. Then I found a couple examples of people inheriting from HTMLCalendar to add data to the calendar display. This sounded right on, so I adapted this code for my reading events.

Problem: Presentation and Content Mixed

I noticed a problem in the code I was adapting. The view was producing HTML. That seemed to violate separation of content and presentation. Shouldn’t the HTML be generated in the template? And since the templating language itself isn’t powerful enough to generate an HTML table from a list of objects, that meant I needed to write my own template tag. Yikes.

Writing My First Template Tag

Django’s documentation made it easy, though. A template tag consists of a few parts. Below is the code; it goes into a subdirectory of the django app called “templatetags.”

The first part follows directly from the Django docs: get a register object.

Then I define a function to parse the template tag arguments and return the node (the HTML code from which the page is eventually build). The template syntax is defined here.

Then I define the node itself, which is made thread-safe by storing and retrieving the variables passed through the template tag from a context (again, this is straight from the Django docs).

Then I inherit from HTMLCalendar and redefine the format methods to add the particular reading event data. You could adapt this class to any kind of event that has an associated date/time by changing the groupby lambda function to use whatever field your event object uses to store its date and time (my reading object simply calls it “date_and_time”).

Finally, I register this template tag so it is available to templates.

Here’s the code.

Reading calendar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

from calendar import HTMLCalendar
from django import template
from datetime import date
from itertools import groupby

from django.utils.html import conditional_escape as esc

register = template.Library()

def do_reading_calendar(parser, token):
  """
 The template tag's syntax is { % reading_calendar year month reading_list %}
 """

  try:
      tag_name, year, month, reading_list = token.split_contents()
  except ValueError:
      raise template.TemplateSyntaxError, "%r tag requires three arguments" % token.contents.split()[0]
  return ReadingCalendarNode(year, month, reading_list)
  

class ReadingCalendarNode(template.Node):
  """
 Process a particular node in the template. Fail silently.
 """
  
  def __init__(self, year, month, reading_list):
      try:
          self.year = template.Variable(year)
          self.month = template.Variable(month)
          self.reading_list = template.Variable(reading_list)
      except ValueError:
          raise template.TemplateSyntaxError
      
  def render(self, context):
      try:
          # Get the variables from the context so the method is thread-safe.
          my_reading_list = self.reading_list.resolve(context)
          my_year = self.year.resolve(context)
          my_month = self.month.resolve(context)
          cal = ReadingCalendar(my_reading_list)
          return cal.formatmonth(int(my_year), int(my_month))
      except ValueError:
          return           
      except template.VariableDoesNotExist:
          return


class ReadingCalendar(HTMLCalendar):
  """
 Overload Python's calendar.HTMLCalendar to add the appropriate reading events to
 each day's table cell.
 """
  
  def __init__(self, readings):
      super(ReadingCalendar, self).__init__()
      self.readings = self.group_by_day(readings)

  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.readings:
              cssclass += ' filled'
              body = ['<ul>']
              for reading in self.readings[day]:
                  body.append('<li>')
                  body.append('<a href="%s">' % reading.get_absolute_url())
                  body.append(esc(reading.series.primary_name))
                  body.append('</a></li>')
              body.append('</ul>')
              return self.day_cell(cssclass, '<span class="dayNumber">%d</span> %s' % (day, ''.join(body)))
          return self.day_cell(cssclass, '<span class="dayNumberNoReadings">%d</span>' % (day))
      return self.day_cell('noday', '&nbsp;')

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

  def group_by_day(self, readings):
      field = lambda reading: reading.date_and_time.day
      return dict(
          [(day, list(items)) for day, items in groupby(readings, field)]
      )

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

# Register the template tag so it is available to templates
register.tag("reading_calendar", do_reading_calendar)

Then, here’s the view (and a couple helper functions) that gets called with the arguments from the URL, including the year, month, and series to display events for.

View
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def named_month(month_number):

  """
 Return the name of the month, given the number.
 """
  return date(1900, month_number, 1).strftime("%B")
  
def this_month(request):
  """
 Show calendar of readings this month.
 """
  today = datetime.now()
  return calendar(request, today.year, today.month)
  
  
def calendar(request, year, month, series_id=None):
  """
 Show calendar of readings for a given month of a given year.
 ``series_id``
 The reading series to show. None shows all reading series.
 """
  
  my_year = int(year)
  my_month = int(month)
  my_calendar_from_month = datetime(my_year, my_month, 1)
  my_calendar_to_month = datetime(my_year, my_month, monthrange(my_year, my_month)[1])

  my_reading_events = Reading.objects.filter(date_and_time__gte=my_calendar_from_month).filter(date_and_time__lte=my_calendar_to_month)
  if series_id:
      my_reading_events = my_reading_events.filter(series=series_id)

  # Calculate values for the calendar controls. 1-indexed (Jan = 1)
  my_previous_year = my_year
  my_previous_month = my_month - 1
  if my_previous_month == 0:
      my_previous_year = my_year - 1
      my_previous_month = 12
  my_next_year = my_year
  my_next_month = my_month + 1
  if my_next_month == 13:
      my_next_year = my_year + 1
      my_next_month = 1
  my_year_after_this = my_year + 1
  my_year_before_this = my_year - 1
  return render_to_response("cal_template.html", { 'readings_list': my_reading_events,
                                                      'month': my_month,
                                                      'month_name': named_month(my_month),
                                                      'year': my_year,
                                                      'previous_month': my_previous_month,
                                                      'previous_month_name': named_month(my_previous_month),
                                                      'previous_year': my_previous_year,
                                                      'next_month': my_next_month,
                                                      'next_month_name': named_month(my_next_month),
                                                      'next_year': my_next_year,
                                                      'year_before_this': my_year_before_this,
                                                      'year_after_this': my_year_after_this,
  }, context_instance=RequestContext(request))

And finally, here’s the template where we load the template tag and employ it, passing the year, month, and list from the view (you would also want to write some control elements that use previous_year, previous_month, etc. to allow the user to change what the calendar displays, but because I want to wrap this up I’ll forgo writing that out):

Use template tag
1
2
3
4
5
{% load reading_tags %}

<div id="calendar">
  {% reading_calendar year month reading_list %}
</div>

Hopefully that makes sense. Enjoy!

You can also see this code (made generic for any kind of event) on django snippets.

Comments