yorzi (owner)

Fork Of

gist: 54116 by jamesu http://www.cuppadev.co.uk/w...

Revisions

gist: 74281 Download_button fork
public
Public Clone URL: git://gist.github.com/74281.git
Embed All Files: show embed
Making a Real Calendar in Rails.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
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
365
366
##
# 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