Event Calendars in Rails

Aug 21, 2009 06:48PM

Build your own calendar with reoccurring events using ri_cal

First, a little context

The `RFC 2445` (iCalendar) spec has become a pseudo standard for internet scheduling. Google Calendars, Microsoft Outlook, and Apples iCal software all support rendering iCalendar feeds. RiCal is Rubys latest (and most complete) implementation for both creating and parsing the format.

So, why am I suddenly so interested in calendars? I've been doing a lot of freelance work lately for an Ottawa based company named Deja Technologies, and so happens, one of their clients needed a corporate calendaring system for their employees that could be integrated into the intranet site that I was building.

Google Calendar Woes

I've developed a bad habit of blindly choosing Google as my default solution provider. Google Maps is fantastic, Docs is amazing, why should their Calendar be any different? I had a seemingly simple idea: generate an iCalendar feed, and render it in in an embedded Google Calendar. It worked perfectly, almost. My software generated the iCalendar feed, Google Calendar displayed it correctly, and it embedded into the page easily.

What was the deal breaker? Caching. At the time of posting, Google Calendar caches iCalendar feeds for anywhere between 3-6 hours. This is not manually refreshable. Such a limitation makes Google Calendars virtually useless as an embedded calendar solution. Google Calendar is still amazing, but it wasn't quite the solution I was hoping for.

Won't Somebody Please, Render My Feed?

Maybe I was looking in the wrong places, but their aren't embedded many iCalendar rendering services on the web (note to self)! I attempted to use InstantCal, and everything worked fine, for awhile. Unfortunatly, they seem to have a bug in their feed parsing. It started doubling up reoccuring events after January 2010.

PHP iCalendar might have worked, but this was a Ruby project, and if I was going to start running my own calendar, I wanted a Ruby solution.

If You Want Something Done Right..

I decided that I had wasted enough time trying to find a solution. It would be much easier (and more fun) to just build one. I decided to use the ri_cal gem to parse my feed, and give me the events. Once I knew the events, drawing the calendar would be a piece of cake.

Install the require gems.

gem install ri_cal

Calendar & Calendar Event Models

class Calendar < ActiveRecord::Base require 'ri_cal' has_many :calendar_events attr_accessor :ics def to_ics(reload = false) if !@ics || reload icalendar = RiCal.Calendar do |c| self.calendar_events.each { |calendar_event| c.event do |e| e.summary = calendar_event.title.to_s e.description = calendar_event.description.to_s if calendar_event.all_day || calendar_event.start.to_date > calendar_event.end.to_date e.dtstart = calendar_event.start.to_date e.dtend = calendar_event.start.advance(:days => 1).to_date elsif calendar_event.all_day e.dtstart = calendar_event.start.to_date e.dtend = calendar_event.end.to_date else e.dtstart = calendar_event.start e.dtend = calendar_event.end end e.location = calendar_event.location.to_s if !calendar_event.frequency.to_s.empty? && CONFIG[:calendar]["frequency"].collect { |i| i[1] }.include?( calendar_event.frequency ) recurrence = [] recurrence << "FREQ=#{calendar_event.frequency}" if calendar_event.frequency recurrence << "INTERVAL=#{calendar_event.interval}" if calendar_event.interval recurrence << "UNTIL=#{ calendar_event.until.strftime('%Y%m%dT%H%M%SZ') }" if calendar_event.until e.rrule = recurrence.join(";") if recurrence.length > 0 end end } end @ics = icalendar end @ics end def bounded_events(start_date, end_date) events = self.to_ics.events.collect { |i| i.occurrences(:starting => start_date, :before => end_date).collect { |j| { :summary => j.summary, :description => j.description, :location => j.location, :start => j.dtstart, :end => j.dtend } } } events.reject { |i| i.empty? }.flatten end def bounded_events_by_date(start_date,end_date) events = self.bounded_events(start_date, end_date) rtn_hash = {} events.each { |e| rtn_hash[e[:start].strftime("%Y-%m-%d")] ||= [] rtn_hash[e[:start].strftime("%Y-%m-%d")] << e } rtn_hash.each_pair { |k,v| v.sort! { |a,b| a[:summary]<=>b[:summary] }.sort! { |a,b| a[:start]<=>b[:start] } } rtn_hash end end class CalendarEvent < ActiveRecord::Base require 'ri_cal' belongs_to :calendar validates_presence_of :title validates_presence_of :start validates_presence_of :end end

Database Migrations

class CreateCalendars < ActiveRecord::Migration def self.up create_table :calendars do |t| t.column :name, :string t.timestamps end end def self.down drop_table :calendars end end class CreateCalendarEvents < ActiveRecord::Migration def self.up create_table :calendar_events do |t| t.column :calendar_id, :integer t.column :start, :datetime t.column :end, :datetime t.column :frequency, :string t.column :interval, :integer t.column :until, :datetime t.column :description, :string t.column :location, :string t.column :all_day, :boolean t.timestamps end end def self.down drop_table :calendar_events end end

Stay tuned for part 2, where I'll go over how to turn all of this into a calendar.