public
Last active

  • Download Gist
Making a Real Calendar in Rails.rb
Ruby
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
##
# Calendar helper with proper events
# http://www.cuppadev.co.uk/webdev/making-a-real-calendar-in-rails/
#
# (C) 2009 James S Urquhart (jamesu at gmail dot com)
# Derived from calendar_helper
# (C) Jeremy Voorhis, Geoffrey Grosenbach, Jarkko Laine, Tom Armitage, Bryan Larsen
# Licensed under MIT. http://www.opensource.org/licenses/mit-license.php
##
 
# Ever wanted a calendar_helper with proper listed events, like all-day events in ical or google calendar?
# Well here is how you do it!
 
# Firstly, lets start off with the modified calendar_helper helpers.
 
def calendar(options = {}, &block)
block ||= Proc.new {|d| nil}
 
defaults = {
:year => Time.now.year,
:month => Time.now.month,
:table_class => 'calendar',
:month_name_class => 'monthName',
:other_month_class => 'otherMonth',
:day_name_class => 'dayName',
:day_class => 'day',
:abbrev => (0..2),
:first_day_of_week => 0,
:accessible => false,
:show_today => true,
:previous_month_text => nil,
:next_month_text => nil,
:start => nil,
:event_strips => nil, # [[nil]*days, ...]
:event_width => 81, # total width per day (including margins)
:event_height => 24, # height
:event_margin => 2 # height margin
}
options = defaults.merge options
options[:month_name_text] ||= Date::MONTHNAMES[options[:month]]
 
first = Date.civil(options[:year], options[:month], 1)
last = Date.civil(options[:year], options[:month], -1)
start = options[:start]
event_strips = options[:event_strips]
event_width = options[:event_width]
event_height = options[:event_height]
event_margin = options[:event_margin]
 
first_weekday = first_day_of_week(options[:first_day_of_week])
last_weekday = last_day_of_week(options[:first_day_of_week])
day_names = Date::DAYNAMES.dup
first_weekday.times do
day_names.push(day_names.shift)
end
 
# TODO Use some kind of builder instead of straight HTML
cal = %(<table class="#{options[:table_class]}" border="0" cellspacing="0" cellpadding="0">)
cal << %(<thead><tr>)
if options[:previous_month_text] or options[:next_month_text]
cal << %(<th colspan="2">#{options[:previous_month_text]}</th>)
colspan=3
else
colspan=7
end
cal << %(<th colspan="#{colspan}" class="#{options[:month_name_class]}">#{options[:month_name_text]}</th>)
cal << %(<th colspan="2">#{options[:next_month_text]}</th>) if options[:next_month_text]
cal << %(</tr><tr class="#{options[:day_name_class]}">)
day_names.each do |d|
unless d[options[:abbrev]].eql? d
cal << "<th scope='col'><abbr title='#{d}'>#{d[options[:abbrev]]}</abbr></th>"
else
cal << "<th scope='col'>#{d[options[:abbrev]]}</th>"
end
end
cal << "</tr></thead><tbody><tr>"
beginning_of_week(first, first_weekday).upto(first - 1) do |d|
cal << %(<td class="#{options[:other_month_class]})
cal << " weekendDay" if weekend?(d)
if options[:accessible]
cal << %(">#{d.day}<span class="hidden"> #{Date::MONTHNAMES[d.month]}</span></td>)
else
cal << %(">#{d.day}</td>)
end
end unless first.wday == first_weekday
start_row = beginning_of_week(first, first_weekday)
last_row = start_row
first.upto(last) do |cur|
cell_text, cell_attrs = nil#block.call(cur)
cell_text ||= cur.mday
cell_attrs ||= {:class => options[:day_class]}
cell_attrs[:class] += " weekendDay" if [0, 6].include?(cur.wday)
cell_attrs[:class] += " today" if (cur == Date.today) and options[:show_today]
cell_attrs = cell_attrs.map {|k, v| %(#{k}="#{v}") }.join(" ")
cal << "<td #{cell_attrs}>#{cell_text}</td>"
if cur.wday == last_weekday
content = calendar_row(event_strips,
event_width,
event_height,
start_row,
last_row..cur,
&block)
cal << "</tr>#{event_row(content, event_height, event_margin)}<tr>"
last_row = cur + 1
end
end
(last + 1).upto(beginning_of_week(last + 7, first_weekday) - 1) do |d|
cal << %(<td class="#{options[:other_month_class]})
cal << " weekendDay" if weekend?(d)
if options[:accessible]
cal << %(">#{d.day}<span class='hidden'> #{Date::MONTHNAMES[d.mon]}</span></td>)
else
cal << %(">#{d.day}</td>)
end
end unless last.wday == last_weekday
content = calendar_row(event_strips,
event_width,
event_height,
start_row,
last_row..(beginning_of_week(last + 7, first_weekday) - 1),
&block)
cal << "</tr>#{event_row(content, event_height, event_margin)}</tbody></table>"
end
 
def calendar_row(event_strips, event_width, event_height, start, date_range, &block)
start_date = date_range.first
range = ((date_range.first - start).to_i)...((date_range.last - start + 1).to_i)
idx = -1
last_offs = 0
event_strips.collect do |strip|
idx += 1
range.collect do |r|
event = strip[r]
if !event.nil?
# Clip event dates (if it extends before or beyond the row)
dates = event.clip_range(start_date, date_range.last)
if dates[0] - start_date == r-range.first
# Event somewhere on this row
cur_offs = (event_width*(r-range.first))
start_d = event.start_date.to_date
end_d = event.end_date.nil? ? start_d+1 : event.end_date.to_date+1
block.call(event, dates[1]-dates[0], cur_offs, idx)
else
nil
end
else
nil
end
end.compact
end
end
def event_row(content, height, margin)
"<tr><td colspan=\"7\"><div class=\"events\" style=\"height:#{(height+margin)*content.length}px\">#{content.join}</div><div class=\"clear\"></div></td></tr>"
end
 
##
## What is the difference?
##
 
# Instead of yielding for each day column, we yield for displaying each event displayed in the
# supplied event_strip.
# Instead of getting clumped in a single column, events are placed in rows after each set of day cells,
# so they can be spread over multiple days.
 
##
## Events?
##
 
# Events are merely ActiveRecord objects with the following schema:
 
create_table :events do |t|
t.integer "calendar_id"
t.string :title
t.datetime :start_date
t.datetime :end_date, :default => nil
t.text :description
end
 
# They also have two crucial helper functions:
 
def to_date
(end_date || start_date).to_date + 1
end
def clip_range(start_d, end_d)
# Clip start date
if (start_date < start_d and to_date > start_d)
clipped_start = start_d
else
clipped_start = start_date.to_date
end
# Clip end date
if (to_date >= end_d)
clipped_end = end_d + 1
else
clipped_end = to_date
end
[clipped_start, clipped_end]
end
 
##
## Event strip?
##
 
# An event strip is a list of arrays containing events corresponding to what goes on in a particular day,
# encompassing the whole period displayed in the calendar.
# An example is as follows:
 
# [
# [ Event(0), nil ,Event(1), Event(1), Event(2), nil, nil, ... ]
# [ Event(3), Event(3),Event(3), Event(3), Event(3), Event(3), Event(3), ... ]
# ]
 
# So we can see, the event strip closely resembles what should be displayed on the calendar,
# with each array representing a separate "row" in which the events should be placed.
 
# Events 0 through 2 dont conflict with one another, so they can exist on the same row.
# Event 3 however exists for a whole 7 days and thus conflicts with events 0 through 2,
# so it gets placed on its own row.
 
##
## Ok, so how do we generate these event strips?
##
 
# The algorithm is simple:
# 1) Start off with the initial blank event strip encompassing all the dates represented
# in the calendar ends.
# 2) For each event:
# 3) Find out the range of dates it encompasses in the strip
# 4) For each existing strip
# 5) If the range is free, set it and go to the next event
# 6) Else, go to the next strip
# 7) If the event didn't fit in the existing strips, make a new strip
# 8) Fit the event in the new strip and go to the next event
 
# Thus in the controller you will need something like the following,
# which grabs all the events for the calendar and inserts them into the event strips
# according to the algorithm.
 
@month = params[:month].nil? ? Time.now.month : params[:month].to_i
@year = params[:year].nil? ? Time.now.year : params[:year].to_i
# Start of month, end of month
@now_date = Date.civil(@year, @month)
@prev_date = @now_date - 1.month
@prev_date = Date.civil(@prev_date.year, @prev_date.month, -1)
@next_date = @now_date + 1.month
@next_date = Date.civil(@next_date.year, @next_date.month)
@first_day_of_week = 1
# offset by weekdays
@strip_start = beginning_of_week(@now_date, @first_day_of_week)
@next_date = beginning_of_week(@next_date + 7, @first_day_of_week)-1
# initial event strip
@event_strips = [[nil] * (@next_date - @strip_start + 1)]
@events = Event.find(:all,
:include => :calendar,
:conditions => ['((start_date >= ? AND start_date < ?) OR
(end_date NOT NULL AND
(end_date > ? AND start_date < ?)
))',
@strip_start, @next_date,
@strip_start, @next_date+1], :order => 'start_date ASC').collect do |evt|
cur_date = evt.start_date.to_date
end_date = evt.to_date
cur_date, end_date = evt.clip_range(@strip_start, @next_date)
range = ((cur_date - @strip_start).to_i)...((end_date - @strip_start).to_i)
# Find strip
found_strip = nil
for strip in @event_strips
is_in = true
# Are all the spaces free?
range.each do |r|
if !strip[r].nil?
is_in = false
break
end
end
# Found it yet?
if is_in
found_strip = strip
break
end
end
# Make strip or add to found strip
if !found_strip.nil?
range.each {|r| found_strip[r] = evt}
else
found_strip = [nil] * (@next_date - @strip_start + 1)
range.each {|r| found_strip[r] = evt}
@event_strips << found_strip
end
evt
end
 
# (Note that i had to borrow the beginning_of_week function from calendar_helper to get the same dates)
 
##
## I've got events, but how do i display them?
##
 
# Somewhere in your view, you should have:
 
calendar events_calendar_opts do |event, days, cur_offs, idx|
"<div class=\"event\" style=\"background: #{event.color}; width: #{(81*days)-1}px; top: #{idx*18}px; left:#{cur_offs}px; \"><div>#{h(event.title)}</div></div>"
end
 
# As for styling, ensure the following:
# - Your events need to be absolutely positioned within the events block
# - Width of the day column (in my case, 81) should match the event width specified for the helper.
# - For columns, don't use border. Instead make a repeating background image
 
# i.e. somethng like this...
 
"
content { width: 600px; }
 
table.calendar {
background-image: url('../images/cal.png');
background-repeat: repeat-y;
}
 
.day { width: 100px; }
.events { position:relative; border-bottom: 1px solid #d5d5d5; }
 
.event {
overflow:hidden;
font-size: 12px;
text-align: left;
position:absolute;
height: 16px;
}
 
.event div { cursor: pointer; padding-left: 6px; color:#ffffff; text-decoration: none; }
"
 
##
## To conclude
##
 
# Any suggestions or improvements? Feel free to fork this gist.
 
# - JamesU

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.