At the Forge - Dynamically Generated Calendars
Last column, we looked at Sunbird, a standalone application from the Mozilla Foundation for tracking calendars. As we saw, Sunbird is able to work with calendars in the iCalendar format. These calendars may be on the local filesystem or retrieved by HTTP from a remote server. We also saw how easy Sunbird makes it to use a calendar that a remote server has made available. We simply enter the URL into a dialog box, and after waiting for Sunbird to retrieve the iCalendar file, the new events are added to our calendar display.
A variety of remote calendars already exist on the Internet in iCalendar format, and you can find and subscribe to them without too much trouble. But doing so is helpful only if you want to subscribe to a calendar that already exists or is available publicly. What if your organization wants to standardize on iCalendar for exchanging event information? How can you create and distribute iCalendar files, such that others can keep track of the events they must attend?
This month, we look at the server side of iCalendar files and create calendars designed to be retrieved by calendar applications, such as Sunbird, within an organization.
If two computers are going to exchange calendars, we obviously need to have a standard that defines how those calendars should be formatted. The protocol over which they are exchanged is not defined, although both standards and daily use seem to indicate that HTTP is the overwhelming favorite for such transactions. The format for calendar exchange, defined in RFC 2445, reflects its age. Whereas a new calendar format would undoubtedly be defined to use XML, this RFC, dated November 1998, uses a set of name-value pairs, with some primitive nesting of elements within a hierarchy. For example, here is the the iCalendar file that we examined last month, when we first looked at Sunbird:
BEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN BEGIN:VEVENT UID :05e55cc2-1dd2-11b2-8818-f578cbb4b77d SUMMARY :LJ deadline STATUS :TENTATIVE CLASS :PRIVATE X-MOZILLA-ALARM-DEFAULT-LENGTH :0 DTSTART :20050211T140000 DTEND :20050211T150000 DTSTAMP :20050209T132231Z END:VEVENT END:VCALENDAR
As you can see, the file begins and ends with BEGIN:VCALENDAR and END:VCALENDAR tags. There is some calendar-wide data at the top of the file, VERSION and PRODID, but then the first and only event is defined, bracketed by BEGIN:VEVENT and END:VEVENT entries. You can imagine how a file could have many more entries than this single one.
iCalendar makes it possible for an event to recur at regular intervals. You thus could have a single VEVENT entry reminding you about the weekly Monday-afternoon meeting or reminding you to put out the trash every Tuesday and Friday morning. Each event also has a beginning and ending time, DTSTART and DTEND, allowing for different lengths.
Although it is not obvious from the above example, iCalendar also allows us to make exceptions to recurring events. So, if your Monday-afternoon meeting is not going to take place during a holiday week, you can insert an EXDATE entry. The application that displays your calendar then ignores the recurring event on that date.
Assuming that we already have an iCalendar file on our system, making it available on the Web is quite easy. Listing 1 contains a simple CGI program that I wrote in Python; it looks for an iCalendar file in a particular directory and returns the contents of that file to the requesting calendar application.
Listing 1. static-calendar.py, a simple CGI program in Python to open an iCalendar file and send it by HTTP.
#!/usr/bin/python # Grab the CGI module import cgi # Log any problems that we might have import cgitb cgitb.enable(display=0, logdir="/tmp") # Where is our calendar file? calendar_directory = '/usr/local/apache2/calendars/' calendar_file = calendar_directory + 'test.ics' # Send a content-type header to the user's browser print "Content-type: text/calendar\n\n" # Send the contents of the file to the browser calendar_filehandle = open(calendar_file, "rb") print calendar_filehandle.read() calendar_filehandle.close()
If you haven't written a CGI program in Python before, this example should demonstrate how straightforward it is. Load the CGI module for some basic CGI functionality. Then, load the cgitb, for CGI traceback, module, which allows us to put debugging information in a file, if and when a problem occurs.
We then send a text/calendar Content-type header. It's probably safe to assume that most content on the Web is sent with a Content-type of text/html (for HTML-formatted text), text/plain (for plain-text files), with many of types image/jpeg, image/png and image/gif thrown in for good measure. The iCalendar standard indicates that the appropriate Content-type to associate with calendar files is text/calendar, even if programs such as Sunbird are forgiving enough to accept the text/plain format as well. Finally, we end the program by sending the contents of the calendar file, which we read from the local filesystem.
If you have been doing Web programming for any length of time, this example should be raising all sorts of red flags. The idea that we would use a program to return a static file seems somewhat silly, although this does have the slight advantage of letting us hide the true location of the calendar file from outside users. There are undoubtedly better ways to accomplish this, however, including the Apache Alias directive. We could improve this program somewhat by passing the calendar's filename as a parameter, but that still would require that we have a set of statically generated files.
The real solution, and one that makes life more interesting, is to create the iCalendar file dynamically when the user requests it. That is, our CGI program does not return the contents of an existing iCalendar file; instead, it creates an iCalendar file programmatically, returning it to the user's calendar client program.
At first glance, this might seem to be a simple task. After all, the iCalendar file format appears to be straightforward, so maybe we can code something together ourselves. But upon closer examination, we discover that creating an iCalendar file is easier said than done, particularly if we want to include recurring events.
Given the increasing popularity of the iCalendar standard and the plethora of open-source projects, I was surprised to discover the relative lack of attention that iCalendar has received from the biggest open-source programming communities. Part of my surprise was because iCalendar has been around for several years, is used by many companies and is supported by many calendar programs, from Novell's Evolution to Lotus Notes to Microsoft Outlook. This combination usually is a recipe for several different options, in several different programming languages.
I first looked at Perl, whose CPAN archive is renowned for its many modules, including many for Internet standards of various sorts. Although several Perl modules are available that parse iCalendar files, no up-to-date module exists for building them. Net::ICal::Libical was going to be a wrapper around the C-language libical library but was last released in a pre-alpha version, several years ago. Net::ICal was part of a project called ReefKnot, which also appears to have been abandoned.
Luckily, the Danish developer Max M (see the on-line Resources) recently decided to fill this gap and wrote a Python package that makes it easy to create an iCalendar file. I downloaded and installed the package on my computer without any trouble, and I found that it is quite straightforward to create a calendar with this package. Combined with our simple CGI program from before, we should be able to create and publish a calendar without any trouble.
I downloaded and installed the iCalendar package from the maxm.dk site. Unlike many modern Python packages, it doesn't install automatically. You must copy it manually to your system's site-packages directory, which on my Fedora Core 3 system is located at /usr/lib/python-2.3/site-packages.
As you can see in Listing 2, I was able to use this newly installed iCalendar package to create new objects of type Calendar and Event. The first thing I had to do was import the appropriate packages into the current namespace:
from iCalendar import Calendar, Event
Listing 2. dynamic-calendar.py, a program that generates a calendar in iCalendar format.
#!/usr/bin/python # Grab the CGI module import cgi from iCalendar import Calendar, Event from datetime import datetime from iCalendar import UTC # timezone # Log any problems that we might have import cgitb cgitb.enable(display=0, logdir="/tmp") # Send a content-type header to the user's browser print "Content-type: text/calendar\n\n" # Create a calendar object cal = Calendar() # What product created the calendar? cal.add('prodid', '-//Python iCalendar 0.9.3//mxm.dk//') # Version 2.0 corresponds to RFC 2445 cal.add('version', '2.0') # Create one event event = Event() event.add('summary', 'ATF deadline') event.add('dtstart', datetime(2005,3,11,8,0,0,tzinfo=UTC())) event.add('dtend', datetime(2005,3,11,10,0,0,tzinfo=UTC())) event.add('dtstamp', datetime(2005,3,11,0,10,0,tzinfo=UTC())) event['uid'] = 'ATF20050311A@lerner.co.il' # Give this very high priority! event.add('priority', 5) # Add the event to the calendar cal.add_component(event) # Ask the calendar to render itself as an iCalendar # file, and return that file in an HTTP response! print cal.as_string()
The Calendar and Event modules inside of the iCalendar package correspond to the entire iCalendar file and one event in that file, respectively. We thus create a single instance of the Calendar object and one Event object for each event that we might want to create.
We then can create the calendar object:
cal = Calendar() cal.add('prodid', '-//Python iCalendar 0.9.3//mxm.dk//') cal.add('version', '2.0')
The second and third lines here, in which we invoke cal.add(), allow us to add identifying data to our iCalendar file. The first of these allows us to tell the client software which program generated the iCalendar file. This is useful for debugging; if we consistently get corrupt iCalendar files from a particular software package, we can contact the author or publisher and report a bug. The second line, in which we add a version identifier, indicates which version of the iCalendar specification we are following. RFC 2445 indicates that we should give this field a value of 2.0 if we are going to follow that specification.
Now that we have created a calendar, let's create an event and give it a summary line to be displayed in the calendar program of anyone subscribing to this iCalendar file:
event = Event() event.add('summary', 'ATF deadline')
Every event, as we have already seen in the file we examined, has three date/time fields associated with it: the starting date and time, dtstart; the ending date and time, dtend; and an indication of when this entry was added to the calendar, dtstamp. The iCalendar standard uses a strange if useful format for its dates and times, but the Event object knows how to work with those if we give it a datetime object from the standard datetime Python package. So, we can say:
event.add('dtstart', datetime(2005,3,11,14,0,0,tzinfo=UTC())) event.add('dtend', datetime(2005,3,11,16,0,0,tzinfo=UTC())) event.add('dtstamp', datetime(2005,3,11,0,10,0,tzinfo=UTC()))
Notice that the above three lines used UTC as the time zone. When the iCalendar file is displayed inside of a client Calendar application, it is shown with the user's local time zone, as opposed to UTC.
Once we have created the event, we need to give it a unique ID. When I say unique, I mean that the ID should be truly unique, across all calendars and computers in the world. This sounds trickier than it actually is. You can use a number of different strategies, including using a combination of the creation timestamp, IP address of the computer on which the event was created and a large random number. I decided to create a simple UID, but if you are creating an application to be shared across multiple computers, you probably should think about what sort of UIDs you want to create and then standardize on them:
event['uid'] = 'ATF20050311A@lerner.co.il'
Finally, we must give our event a priority, in the range of 0 through 9. An event with priority 5 is considered to be normal or average; urgent items get higher numbers and less-urgent items get lower ones:
event.add('priority', 5)
Once we have created our event, we attach it to the calendar object, which has been waiting for us to do something with it:
cal.add_component(event)
If we are so interested, we then could to add more events to the calendar. So long as each has a unique UID field, there won't be any problems.
Finally, we turn our Calendar object into an iCalendar file, using the as_string() method:
print cal.as_string()
Because print writes to standard output by default, and because CGI programs send their standard output back to the HTTP client, this has the effect of sending an iCalendar file back to whoever made the HTTP request. And because we have defined the MIME type to be of type text/calendar, the HTTP client knows to interpret this as a calendar and display it appropriately. If we look at the output ourselves, we see that it is indeed in iCalendar format:
BEGIN:VCALENDAR PRODID:-//Python iCalendar 0.9.3//mxm.dk// VERSION:2.0 BEGIN:VEVENT DTEND:20050311T160000Z DTSTAMP:20050311T001000Z DTSTART:20050311T140000Z PRIORITY:5 SUMMARY:ATF deadline UID:ATF20050311A@lerner.co.il END:VEVENT END:VCALENDAR
Now, I must admit that this example is almost as contrived as the previous one. True, we have exploited the fact that we can generate a calendar dynamically, but this event was hard-coded into the program, making it impossible for a nonprogrammer to add, modify or delete the event. That said, we have taken an additional step toward the programmatic calculation of events and dates. The next step is to store the dates in a file or even in a relational database and to use our program to convert the information on the fly.
This month, we looked at the creation of a dynamic calendar using the iCalendar module for Python wrapped inside of a simple CGI program. At the same time, we saw the limitations of having a calendar whose entries need to be on disk. A better solution would be to put that event information in a relational database, which has built-in support for dates, as well as security mechanisms for user and group access. Next month, we will extend our calendar program so that it retrieves information from a database, turning PostgreSQL tables into iCalendar files.
Resources for this article: /article/8197.
Reuven M. Lerner, a longtime Web/database consultant and developer, now is a graduate student in the Learning Sciences program at Northwestern University. His Weblog is at altneuland.lerner.co.il, and you can reach him at reuven@lerner.co.il.