FINALLY! RailsICalendar ical ics publish with ruby on rails

Das hat echt was Nerven gekostet, aber ich bin mehr als zufrieden mit dem Resultat.

Ich darf vorstellen: Wenn man seinen Kalender in Mac OS X (z. B. per Webdav) auf seinen Server lädt (bzw. synchronisiert) und auf diesem Server auch Ruby on Rails läuft, dann kann man seinen Kalender jetzt auf seiner Webseite veröffentlichen.

In fact I just realize I should better speak english, because someone’s German might be somewhat rusty :)

So again: You have ics files on your server (e.g. via webdav) and Ruby on Rails is running? Great, let’s publish your calendar. The idea came from the great PHPicalendar script.

This is what it will somewhat look like:
bild-1.png

I am sorry to not have made a plugin out of this yet, but, hey, the basics are there, help yourself :) If you have any questions feel free to comment.

Requirements:

  • Vpim plugin with sudo gem install vpim

Features:

  • Read several ICS files from a directory on the server
  • Parse all the ical events in them
  • Cache the current calendar in yaml files
  • (The cache will be refreshed when a ICS file was updated meanwhile)
  • HTML will be presented for the calendar
  • Currently you can only choose a date and see the next X days

A word on recurrence of events

  • It does do most of the recurrence rules!
  • Specifically: All that VPIM supports
  • PLUS: EXDATE is also supported!

The Code

Initializer (config/initializers/any_filename.rb)

1
2
3
4
5
# Path to icalendar *.ics files on your server
PATH_ICS       = "#{RAILS_ROOT}/private/calendars/"
PATH_ICS_CACHE = "#{RAILS_ROOT}/tmp/calendars/"
FileUtils::mkdir_p(PATH_ICS)       unless File.exists?(PATH_ICS)
FileUtils::mkdir_p(PATH_ICS_CACHE) unless File.exists?(PATH_ICS_CACHE)

Controller (app/controllers/calendars_controller.rb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
require 'vpim'
class CalendarsController < ApplicationController
 
  def index
    # Load parameters if submitted
    session[:date_year]  = params[:options]["date(1i)"] if !params[:options].blank? && !params[:options]["date(1i)"].empty?
    session[:date_month] = params[:options]["date(2i)"] if !params[:options].blank? && !params[:options]["date(2i)"].empty?
    session[:date_day]   = params[:options]["date(3i)"] if !params[:options].blank? && !params[:options]["date(3i)"].empty?
 
    # Load standard if nothing submitted
    session[:date_year]  = Time.now.year  if session[:date_year].blank?
    session[:date_month] = Time.now.month if session[:date_month].blank?
    session[:date_day]   = Time.now.day   if session[:date_day].blank?
 
    # Set variables
    @scope     = 7
    @events   = []
    @today    = Time.gm(session[:date_year], session[:date_month], session[:date_day])
    cachefile = File.join(PATH_ICS_CACHE, "#{@today.to_s(:ical)}_#{@scope}.yml")
 
    # Kill cache if outdated
    if File.exists?(cachefile)
      killcache = false
      Dir.glob(File.join(PATH_ICS, '*.ics')).each do |file|
        killcache = true if File.mtime(file) > File.mtime(cachefile)
      end
      if killcache
        Dir.glob(File.join(PATH_ICS_CACHE, '*.*')).each do |file|
          File.delete(file)
        end
      end
    end
 
    # Load calendar from cache
    if File.exists?(cachefile)
      @events = YAML.load_file cachefile 
    else
      # No cache, parse each icalendar *.ics file in PATH_ICS and check for event occurences
      Dir.glob(File.join(PATH_ICS, '*.ics')).each do |file|
        category = File.basename(file, '.ics')
        Vpim::Icalendar.decode(File.open(file)).each do |calendar|
          calendar.components do |event|
            for day in 0..@scope
              if start = event.occurs_in?(@today+(60*60*24*day), @today+(60*60*24)+(60*60*24*day))
                myend = start + (event.dtend - event.dtstart)
                @events < < {
                  'category' => category,
                  'day'      => day,
                  'start'    => start,
                  'duration' => ((event.dtend - event.dtstart) / 60).round, # In minutes
                  'end'      => myend,
                  'allday'   => start.hour == 0 && start.min == 0 && start.sec == 0 && myend.hour == 0 && myend.min == 0 && myend.sec == 0,
                  'data'     => event
                }
              end
            end
          end
        end
      end
      @events = @events.uniq # Just in case :)
      # Save cache
      File.open(cachefile, 'w') { |f| YAML.dump(@events, f) }
    end
  end
 
end

Single View (view/calendars/index.html.erb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
< %
  width  = 110      # Width of one day
  height = 6        # height of 15 minutes in pixels
  buffer = 28       # free space for dates in day column (at top of each day)
  minh   = 20       # Minimum of event height
  cutmornings = 120 # I don't have events between 0:00 and 6:00, cut these pixels off
%>
 
<div id="action">
 
< % for day in 0...@scope + 1 do 
  today = (@today+(60*60*24*day))
  case today.wday  
    when 0  
      dayclass = 'class="sunday"'
    when 6  
      dayclass = 'class="saturday"'
    else  
      dayclass = 'class="otherday"'
  end
  %>
  <div id="ical_day" <%= dayclass %>
    style=" width:  < %= width %>px;
            height: < %= height*96 + buffer - cutmornings %>px;
            left:   < %= day*width + day*10 %>px;">
    < % if today.year == Time.now.year && today.month == Time.now.month && today.day == Time.now.day %>
      <b>< %= 'Today' %></b>
    < % else %>
      < %= _(today.strftime("%a")) +', '+ today.to_s(:date) %>
    < % end %>
  </div>
< % end %>
 
< % tops = Array.new(@scope + 1, '')
@events.each do |event|
 
  # Exclude recurrence rule hack
  today = (@today+(60*60*24*event['day']))
  exme = false
  event['data'].propvaluearray('EXDATE').each do |exdate|
    exdate = exdate.to_time
    exme = true if today.year == exdate.year && today.mon == exdate.mon && today.day == exdate.day
  end
  next if exme # Skip this event
 
  if event['allday']
    # All-day events will be inserted later
    tops[event['day']] += '&nbsp; ' + event['data'].summary.to_s + '<br/>'
  else 
    eventheight = ((event['duration']/15)*height).round
    eventheight = minh if eventheight < minh
    %>
    <div id="ical_event"
      style=" background: #<%= eventcolor(event['category']) %>;
              width:  < %= width-2 %>px;
              height: < %= eventheight.to_s %>px;
              top:    < %= event['start'].hour*height*4 + (event['start'].min/15)*6 + buffer - cutmornings %>px;
              left:   < %= event['day']*width + event['day']*10 %>px;">
      < %= '<b>'+ event['start'].to_s(:time) +' - '+ event['end'].to_s(:time) +'<br />' %>
      < %= event['data'].summary %>
    </div>
  < % end %>
< % end %>
 
< % tops.each_with_index do |top, day| %>
    <div id="ical_event_top"
      style=" width: <%= width-2 %>px;
              height: < %= height %>px;
              top: < %= buffer %>px;
              left: < %= day * width + day * 10 %>px;">
      < %= top %>
    </div>
< % end %>
 
<div id="vertical_spacer" style="height: <%= height*96 + buffer*2 - cutmornings %>px;">&nbsp;</div>
 
</div>

Layout View (views/layouts/calendars.html.erb)

        < % form_tag :controller => 'calendars', :action => nil, :id => nil do |f| %>
          < %= date_select("options", "date", :default => @today, :order => [:day, :month, :year]) %>
            < %= submit_tag 'Show' + ' &raquo;', :class => 'date_button' %>
        < % end %>

Helper (for coloring events) (app/helpers/calendar_helper.rb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module CalendarsHelper
 
  def eventcolor(category)
    case category  
      when 'Wichtig'   # This is the name of the .ics file
        return 'f66'
      when 'Sonstiges'
        return '4f4'
      when 'Studium'
        return 'fb4'
      when 'Privat'
        return '77f'
      when 'Freunde'
        return 'f4f'
      else  
        return 'fb4'
    end
  end
 
end

Stylesheet (public/stylesheets/calendar.css)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**************** DIVs ***************************/
 
div#action {
  position: absolute;
  margin-left: 10px;
  margin-top: 10px;
  padding: 4px;
  border-left: 0px;
}
 
div#ical_day {
  position: absolute;
  padding-top: 5px;
  text-align: center;
  font-size: 10px;
}
 
.otherday {
  background: #ddd;
}
 
.sunday {
  background: #fdd;
}
 
.saturday {
  background: #ddf;
}
 
#ical_event {
  position: absolute;
  border: 1px #555 solid;
  background: #fb4;
  font-size: 8px;
  text-align: left;
}
 
#ical_event_top {
  position: absolute;
  border: 0px;
  font-size: 9px;
  font-weight: bold;
  text-align: left;
}
 
/**************** Fonts ***************************/
 
#title {
  float:right;
  color: #ddd;
  margin-top: 30px;
  margin-right: 10px;
  line-height: 11px;
  font-size: 16px;
  text-align: right;
}
#title .description {
  color: #777;
  font-size: 10px;
  margin: 0;
}

Routes (could be optional) (config/routes.rb)

1
2
  # CALENDAR Controllers
  map.connect 'calendar', :controller => 'calendars'

8 Responses to “FINALLY! RailsICalendar ical ics publish with ruby on rails”


  • nice job.
    this yaml cache is an awesome idee, who the fuck had it ;-)
    i haven’t tried it but i’m sure it works quite well.

  • Hi I had done all wat u say ..

    And I runned the server Then I got “LOAD ERROR”
    LoadError

    Expected /home/hasmukh/hafeez/newacal/app/helpers/calendar_helper.rb to define CalendarHelper

    How to rectify this

  • Hi hafeez,

    I’m sorry, I can’t recall that error. In fact, I am currently working on a new version to run on Rails 2.3

    It will come out in a few months. However, your error doesn’t look like it’s a big thing. You should try to copy and paste all this code from the helper:

    def eventcolor(category)
    case category
    when ‘Wichtig’ # This is the name of the .ics file
    return ‘f66′
    when ‘Sonstiges’
    return ’4f4′
    when ‘Studium’
    return ‘fb4′
    when ‘Privat’
    return ’77f’
    when ‘Freunde’
    return ‘f4f’
    else
    return ‘fb4′
    end
    end

    into the application_helper.rb

    That could solve your problem. Let us know!
    Greets, Marius

  • hi captain

    i am not getting the full display of index action and how i will display the ics files using this date_select helper

  • Hi!

    1. What do you mean by “I am not getting the full display”? Can you not see the code or is your application not having any output, or..?

    2. Do you see these lines:
    # Set variables
    @scope = 7
    @events = []
    @today = Time.gm(session[:date_year], session[:date_month], session[:date_day])

    The ics file (in the PATH_ICS path) will be displayed according to the date in @today. So when you set @today manually it will show you what happens on that date and the next 7 days (@scope). However, the date_select will make you have a dropdown box to select any date you want right on the website.

    Do I understand you correctly? :)

  • i have fixed this i was only getting the date_select submit form not proper calendar for ics file on PATH_ICS path

    thanks for reply

  • Wonderful.

  • Denise Schmidhuber

    Ganz schön formuliert – super.

Leave a Reply