If you need to integrate your Rails app with some calendaring service, such as Google Calendar, sooner or later you will find yourself writing code that outputs your Event object as an ICS file. But if your events have recurrence rules, you are out of luck, the popular Icalendar gem doesn’t support them. I’ll show how to make it work.
ICS is a calendar standard supported by a wide range of Calendar applications. In my case, I needed to attach ICS file to outgoing emails that are sent to users every time a new event is scheduled. The reason? The nice “Add to your Calendar” box that is added automatically in Gmail and other clients, that allows the user to quickly add the event to his calendar.
Icalendar gem is a simple library that allows populating an Icalendar::Event object and outputting it as ICS string.
require 'icalendar' module Event::ExportsICS def to_ics @cal = Icalendar::Calendar.new @cal.event do |e| e.dtstart = self.starts_at e.dtend = self.ends_at e.summary = self.calendar_title e.organizer = Icalendar::Values::CalAddress.new( "mailto:#{self.organizers.first.email}", cn: self.organizers.first.full_name) e.description = self.description e.location = self.location end end end
But it’s missing a crucial component – a support for recurring events. After some searching, I ended up writing a build_recurrence_rule method that builds a recurrence rule from my Event object (referred as self in the code)
def build_recurrence_rule rule = "RRULE:FREQ=#{self.repetition.repeat_frequency.upcase};"\ "INTERVAL=#{self.repetition.repeat_interval};" if self.repetition.end_date.present? rule << "UNTIL=#{self.repetition.end_date.iso8601};" end if self.repetition.yearly? rule << "BYMONTHDAY=#{self.starts_at.day};"\ "BYMONTH=#{self.starts_at.month};" elsif self.repetition.monthly? && self.repetition.week_of_month.present? by_month_day = self.repetition.week_of_month.to_s + self.starts_at.strftime("%A")&.upcase&.slice(0..1) rule << "BYDAY=#{by_month_day};" elsif self.repetition.days_of_week.present? by_week_day = self.repetition.days_of_week.map do |d| d.slice(0..1).upcase end.join(',') rule << "BYDAY=#{by_week_day};" end [rule] end
Now, all that is left is to somehow add the generated recurrence rule to the Icalendar::Event object we created earlier and output the result as a string.
require 'icalendar' module Event::ExportsICS def to_ics @cal = Icalendar::Calendar.new @cal.event do |e| # populate the Icalendar::Event as I've shown earlier end # there is no way to add the recurrence rule to the object, # so we have to work with the output string ical_str = @cal.to_ical if self.repetition.present? recurrence_string = build_recurrence_rule.join('\r\n') ical_str = ical_str.gsub('END:VEVENT', "#{recurrence_string}\r\nEND:VEVENT") end ical_str end end
And now, all you have to do is add the generated ICS as an attachment in your mailer:
def add_ics_attachment attachments['event.ics'] = { mime_type: 'text/calendar', content: @event.to_ics } end
And you are good to go.