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:

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']] += ' ' + 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;"> </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' + ' »', :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' |