Event Calendars in Rails
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.