Most calendar programs today accept URLs to iCalendar files to keep up to date on something. That’s how I get Radio 1190’s concerts. There are two ways to build such a thing in Django. You can either use a template like normal, and just set the content type to text/calendar
. Or you can use vObject
. I’m going with the first, because I’m just publishing a list of concerts. If there was any kind of feedback or incoming data, I would need vObject
. In either case, make the URL end in .ics
.
There are two good validators. http://arnout.engelen.eu/icalendar-validator/ is standards compliant. If there is a single thing off, it will let you know and point to the part of the spec you’re violating. http://icalvalid.cloudapp.net/ only cares about whether it’s understood. Since user agents don’t follow the the spec precisely, you need to find those problems too. Use both. While they both find major mistakes, there are errors that only show up in one or the other.
I’m going to go though the template and tech you what each line does and how it is understood.
{% autoescape off %}
Because this isn’t HTMLBEGIN:VCALENDAR
RequiredVERSION:2.0
You need a PRODID. Don’t use someone else’s.
METHOD:PUBLISH
PRODID:Peter of the Norse wrote this though Django X-WR-CALNAME:Radio 1190 Concerts
A suggested name for the new calendar.CALSCALE:GREGORIAN
Since I only use one time zone, this acts as a hint for some user agents to speed things up.
X-WR-TIMEZONE:America/Denver BEGIN:VTIMEZONE
This is a copy/paste of the time zone information. While required, it’s not actually used.TZID:America/Denver
You need to use a recognized time zone name. Some user agents don’t parse time zones and will fail if they don’t recognize it. Others don’t parse time zones when they know the name. Others ignore all time zone information when they see X-WR-TIMEZONE. And a small number actually use the VTIMEZONE.BEGIN:DAYLIGHT
Now we get into some actual code.
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:MDT
TZOFFSETTO:-0600
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0600
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:MST
TZOFFSETTO:-0700
END:STANDARD
END:VTIMEZONE
{% for event in list %}BEGIN:VEVENT UID:Radio1190-concert-{{event.id}}
Each event needs a unique id. Life’s too short to develop a UUID.ORGANIZER:http://www.Radio1190.org/
Some events have times; others don’t.
{% if event.time %}DTSTART;TZID=America/Denver:{{event.date|date:"Ymd"}}T{{event.time|time:"Hi"}}00 DURATION:PT2H
We only know the start time. Two hours shorter than most concerts, but if we did four or five, every concert would overlap. I might even change it to one hour.{% else %}DTSTART;VALUE=DATE:{{event.date|date:"Ymd"}}
This should be the last modified date, but we don’t keep that kind of information.
DURATION:P1D
{% endif %}DTSTAMP:{{event.date|date:"Ymd"}}T000000Z LOCATION:{{event.venue|escape}}
I wrote a filter to escape newlines, semicolons, and commas. They have special meaning to ics files.SUMMARY:{{ event.performer_set.all|join:"—"|escape }}
DESCRIPTION:{% if event.we_present %}Radio 1190 presents!\n{% endif %}{% if event.minimum_age %}{{event.minimum_age|escape}}\n{% endif %}{{event.info|escape}}
{% if event.site %}URL;VALUE=URI:{{event.site|escape}}
{% else %}{% if event.venue.homepage %}URL;VALUE=URI:{{event.venue.homepage|escape}}
{% endif %}{% endif %}END:VEVENT
{% endfor %}END:VCALENDAR{% endautoescape %}
All the rest is more of the same. Since blank lines break some user agents, there are a lot of run-on code lines. It makes the code a mess. Run your results against the validators again and again. You will make mistakes more than once, even when you’re working off of someone else’s example.