calendarize 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +50 -0
  3. data/Rakefile +38 -0
  4. data/app/assets/javascripts/calendarize.js.coffee +84 -0
  5. data/app/helpers/calendarize_helper.rb +629 -0
  6. data/lib/calendarize.rb +5 -0
  7. data/lib/calendarize/engine.rb +11 -0
  8. data/lib/calendarize/version.rb +3 -0
  9. data/test/dummy/README.rdoc +261 -0
  10. data/test/dummy/Rakefile +7 -0
  11. data/test/dummy/app/assets/javascripts/application.js +5 -0
  12. data/test/dummy/app/assets/stylesheets/_variables.css.scss +117 -0
  13. data/test/dummy/app/assets/stylesheets/application.css +4 -0
  14. data/test/dummy/app/assets/stylesheets/calendarize.css.scss +48 -0
  15. data/test/dummy/app/assets/stylesheets/tables.css.scss +282 -0
  16. data/test/dummy/app/controllers/application_controller.rb +9 -0
  17. data/test/dummy/app/controllers/events_controller.rb +12 -0
  18. data/test/dummy/app/controllers/welcome_controller.rb +5 -0
  19. data/test/dummy/app/helpers/application_helper.rb +2 -0
  20. data/test/dummy/app/models/event.rb +5 -0
  21. data/test/dummy/app/views/layouts/application.html.haml +11 -0
  22. data/test/dummy/app/views/welcome/index.html.haml +40 -0
  23. data/test/dummy/config.ru +4 -0
  24. data/test/dummy/config/application.rb +17 -0
  25. data/test/dummy/config/boot.rb +10 -0
  26. data/test/dummy/config/database.yml +17 -0
  27. data/test/dummy/config/environment.rb +2 -0
  28. data/test/dummy/config/environments/development.rb +13 -0
  29. data/test/dummy/config/environments/production.rb +11 -0
  30. data/test/dummy/config/environments/test.rb +13 -0
  31. data/test/dummy/config/initializers/secret_token.rb +1 -0
  32. data/test/dummy/config/initializers/session_store.rb +1 -0
  33. data/test/dummy/config/initializers/wrap_parameters.rb +7 -0
  34. data/test/dummy/config/locales/en.yml +5 -0
  35. data/test/dummy/config/routes.rb +5 -0
  36. data/test/dummy/db/development.sqlite3 +0 -0
  37. data/test/dummy/db/migrate/20111213185551_create_events.rb +11 -0
  38. data/test/dummy/db/schema.rb +24 -0
  39. data/test/dummy/db/test.sqlite3 +0 -0
  40. data/test/dummy/log/development.log +23316 -0
  41. data/test/dummy/public/404.html +26 -0
  42. data/test/dummy/public/422.html +26 -0
  43. data/test/dummy/public/500.html +25 -0
  44. data/test/dummy/public/favicon.ico +0 -0
  45. data/test/dummy/script/rails +6 -0
  46. data/test/dummy/tmp/cache/assets/C73/050/sprockets%2Fd16f7f8d96d6856e11493371995e1963 +0 -0
  47. data/test/dummy/tmp/cache/assets/CBA/5C0/sprockets%2F99a55fcc61a90861078b64623dba7755 +0 -0
  48. data/test/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  49. data/test/dummy/tmp/cache/assets/CEE/890/sprockets%2F3e5c71e7d965ac071af5e389981a9018 +0 -0
  50. data/test/dummy/tmp/cache/assets/D04/120/sprockets%2Fb90e31ee74700b7b9c651fd266108b0b +0 -0
  51. data/test/dummy/tmp/cache/assets/D1E/BC0/sprockets%2F080adab657299ba98a417cb866e5596d +0 -0
  52. data/test/dummy/tmp/cache/assets/D2C/470/sprockets%2Fc20be7f875105eb1090b45b4b45b3af4 +0 -0
  53. data/test/dummy/tmp/cache/assets/D32/5B0/sprockets%2F38ae1e43e5b0cd2272a36df6d0f09074 +0 -0
  54. data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  55. data/test/dummy/tmp/cache/assets/D33/AD0/sprockets%2F7304efbb5a261ea5d15cc96030594f5a +0 -0
  56. data/test/dummy/tmp/cache/assets/D37/A50/sprockets%2F20f273b3aae704a026aaf9f8738b395c +0 -0
  57. data/test/dummy/tmp/cache/assets/D3A/3F0/sprockets%2F8db668af6e983e0f8d5679f1a968627f +0 -0
  58. data/test/dummy/tmp/cache/assets/D3E/310/sprockets%2F4bda6972b0c877d504d246e2cce4e183 +0 -0
  59. data/test/dummy/tmp/cache/assets/D3E/A90/sprockets%2F4059ec1ce475df14794877ba32b0cd3b +0 -0
  60. data/test/dummy/tmp/cache/assets/D44/E30/sprockets%2F139a3719f7f8361f76afefb710b1c5d3 +0 -0
  61. data/test/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  62. data/test/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  63. data/test/dummy/tmp/cache/assets/D71/BB0/sprockets%2F33958d26d9cad4b8311fae36cf2ff144 +0 -0
  64. data/test/dummy/tmp/cache/assets/DD0/480/sprockets%2Ffed1aeed65d2e88cb924a521cc431f68 +0 -0
  65. data/test/dummy/tmp/cache/assets/DD2/820/sprockets%2Ff62ac2c8d56b4409399c1831fbfabdfe +0 -0
  66. data/test/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  67. data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  68. metadata +194 -0
@@ -0,0 +1,20 @@
1
+ Copyright 2012 JODI GIORDANO
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,50 @@
1
+ == Calendarize
2
+
3
+ A easy-to-use calendar helper for your rails projects. Currently supports a weekly and daily calendar.
4
+
5
+
6
+ == Getting Started
7
+
8
+ 1. Add this gem in your *Gemfile* :
9
+
10
+ gem 'calendarize'
11
+
12
+ 2. Add an entry in *app/assets/javascripts/application.js* :
13
+
14
+ //= require calendarize
15
+
16
+ 3. Add this class method to any *controller* that will have a calendar in it's views :
17
+
18
+ class MyController < ApplicationController
19
+ calendarize
20
+ end
21
+
22
+ 4. Add this class method to any *model* that will represent an event in the calendar
23
+
24
+ class MyModel < ActiveRecord::Base
25
+ acts_as_event
26
+ end
27
+
28
+
29
+ == Example
30
+
31
+ Here is an example of how to use the calenderize helpers in your views. If you put *calendarize* in your controller, it gives you the *@calendar* variable. Also, in this example I added *acts_as_event* to my model *Event* so I have the scope *for_day*.
32
+
33
+ = daily_calendar @calendar[:date], Event.for_day(@calendar[:date]), verbose: @calendar[:verbose] do |c|
34
+ = l(c.event.start_time, format: :short)
35
+ = ' - '
36
+ = l(c.event.end_time, format: :short)
37
+ = ' : '
38
+ = c.event.title
39
+
40
+ Which will output:
41
+ http://github.com/ephemeregames/calendarize/raw/master/examples/screenshot1.png
42
+
43
+ And with a little bit of CSS:
44
+ http://github.com/ephemeregames/calendarize/raw/master/examples/screenshot2.png
45
+ http://github.com/ephemeregames/calendarize/raw/master/examples/screenshot3.png
46
+
47
+
48
+ == TODO
49
+
50
+ - Monthly calendar
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'Calendarize'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,84 @@
1
+ class @DailyCalendar
2
+
3
+ constructor: (id, opts) ->
4
+ @inner = $('#' + id)
5
+ @not_all_day = $('#not_all_day', @inner)
6
+ @first_row = $('.row_content:first', @not_all_day)
7
+
8
+ @options = {
9
+ height: @first_row.outerHeight(true)
10
+ width: 200
11
+ }
12
+
13
+ $.extend(@options, opts)
14
+
15
+ this.init()
16
+
17
+
18
+ init: =>
19
+ $('.calendar_event', @inner).each (index, element) =>
20
+ e = $(element)
21
+ e.width(@options['width'])
22
+ e.height(@options['height'] * (parseInt(e.data('row-end')) - parseInt(e.data('row-start'))) - 1)
23
+ console.log $('#row_content_' + e.data('row-start'), @not_all_day)
24
+
25
+ e.position({
26
+ my: 'left top',
27
+ at: 'left top',
28
+ of: $('#row_content_' + e.data('row-start'), @not_all_day),
29
+ offset: e.data('column') * (@options['width'] + 3) + ' 0',
30
+ collision: 'none'
31
+ })
32
+
33
+
34
+ class @WeeklyCalendar
35
+
36
+ constructor: (id, opts) ->
37
+ @inner = $('#' + id)
38
+
39
+ @options = {
40
+ height: 60
41
+ }
42
+
43
+ $.extend(@options, opts)
44
+
45
+ this.init()
46
+
47
+
48
+ init: =>
49
+ $('.row_unit', @inner).each (index, element) =>
50
+ e = $(element)
51
+ e.height(e.data('events-count') * @options['height'])
52
+
53
+ $('.calendar_event', @inner).each (index, element) =>
54
+ e = $(element)
55
+
56
+ # find the cell to put the event
57
+ cell = $('.row_' + e.data('row') + '.column_' + e.data('column'), @inner)
58
+
59
+ e.width(cell.outerWidth(true))
60
+ e.height(@options['height'])
61
+
62
+ e.position({
63
+ my: 'left top',
64
+ at: 'left top',
65
+ of: $('.row_' + e.data('row') + '.column_' + e.data('column'), @inner),
66
+ offset: '0 ' + e.data('index') * (@options['height']),
67
+ collision: 'none'
68
+ })
69
+
70
+
71
+ $(document).ready ->
72
+
73
+ # Initialize every daily calendar on the page
74
+ daily_calendars = []
75
+
76
+ $('.daily_calendar').each (index, element) =>
77
+ daily_calendars << new DailyCalendar(element.id)
78
+
79
+
80
+ # Initialize every weekly calendar on the page
81
+ weekly_calendars = []
82
+
83
+ $('.weekly_calendar').each (index, element) =>
84
+ weekly_calendars << new WeeklyCalendar(element.id)
@@ -0,0 +1,629 @@
1
+ module CalendarizeHelper
2
+
3
+ # Creates a daily calendar for events
4
+ #
5
+ # Usage: daily_calendar day, events, [options] { |c| ... }
6
+ #
7
+ # Params
8
+ # :day, Date, the day to display
9
+ # :events, ?, the events to display. An event must responds to:
10
+ # - start_time: a TimeWithZone object (from ActiveRecord)
11
+ # - end_time: a TimeWithZone object (from ActiveRecord)
12
+ # :block |c|, is yield for every event placed so you can customize it's content. :c is the calendar, which have:
13
+ # - @event: the current event that is rendering
14
+ # - @is_all_day: if the current event is an all-day one
15
+ #
16
+ # Options
17
+ # :unit, integer, the time unit in minutes between two rows, defaults to 60. Must be > 0.
18
+ # :date_format, symbol, the format of the date to find at I18n.l('date.formats.date_format'), defaults to :long
19
+ # :url, the url path to call for some actions (ex: previous day), some params will be appended, defaults to request.path
20
+ # :id, integer, id of the calendar, default to one provided by this helper
21
+ # :day_start, integer, when to start showing events on the calendar, in minutes, defaults to 0
22
+ # :day_end, integer, when to stop showing events on the calendar, in minutes, defaults to 1440 (24 hours)
23
+ # :verbose, boolean, show all the rows or only those with events, defaults to true
24
+ #
25
+ def daily_calendar_for(*args, &block)
26
+ DailyCalendarBuilder.new(self, *args).compute.render(&block)
27
+ end
28
+
29
+ alias_method :daily_calendar, :daily_calendar_for
30
+
31
+
32
+ # Creates a weekly calendar for events
33
+ #
34
+ # Usage: weekly_calendar day, events, [options] { |c| ... }
35
+ #
36
+ # Params
37
+ # :day, Date, the day to display. Will display the week that contains that day
38
+ # :events, ?, the events to display. An event must responds to:
39
+ # - start_time: a TimeWithZone object (from ActiveRecord)
40
+ # - end_time: a TimeWithZone object (from ActiveRecord)
41
+ # :block |c|, is yield for every event placed so you can customize it's content. :c is the calendar, which have:
42
+ # - @event: the current event that is rendering
43
+ #
44
+ # Options
45
+ # :unit, integer, the time unit in minutes between two rows, defaults to 60. Must be > 0.
46
+ # :date_format, symbol, the format of the date to find at I18n.l('date.formats.date_format'), defaults to :long
47
+ # :id, integer, id of the calendar, default to one provided by this helper
48
+ # :week_start, Date::DAYS_INTO_WEEK.keys or string, the day to start the week, defaults to :monday
49
+ # :week_end, Date::DAYS_INTO_WEEK.keys or string, the day to end the week, defaults to :sunday
50
+ #
51
+ def weekly_calendar_for(*args, &block)
52
+ WeeklyCalendarBuilder.new(self, *args).compute.render(&block)
53
+ end
54
+
55
+ alias_method :weekly_calendar, :weekly_calendar_for
56
+
57
+
58
+ # Returns the current params for a calendar
59
+ # Used when you want to keep track of the calendar between two requests
60
+ # Can be used with path helpers
61
+ #
62
+ # Usage: resource_path(calendar_current_params)
63
+ #
64
+ def calendar_current_params
65
+ { calendar: @calendar.clone }
66
+ end
67
+
68
+
69
+ # An enum object that hold the possible scopes for a calendar
70
+ # The scope can be used in a calendar view to know which calendar to show
71
+ #
72
+ class Scopes
73
+ DAILY = 'daily'
74
+ WEEKLY = 'weekly'
75
+ MONTHLY = 'monthly'
76
+ end
77
+
78
+
79
+ # Methods used in an existing controller to get the params associated to a calendar and pass them to the view
80
+ # Automatically included by the engine as a class helper
81
+ # Right now it only supports one calendar per view but it should not be hard to throw some :uuid in the process
82
+ #
83
+ # Usage:
84
+ #
85
+ # class MyCalendarController < ApplicationController
86
+ # calendarize
87
+ # end
88
+ #
89
+ module Controller
90
+
91
+ def calendarize
92
+
93
+ before_filter lambda {
94
+ @calendar = { }
95
+
96
+ # I hate time handling in RoR...
97
+ # Time.zone.parse is prefered to DateTime.parse because it preserve the timezone
98
+
99
+ if params[:calendar]
100
+ @calendar[:date] = Time.zone.parse(params[:calendar][:date]).to_datetime
101
+ @calendar[:verbose] = params[:calendar][:verbose] == 'true'
102
+ @calendar[:scope] = params[:calendar][:scope]
103
+ else
104
+ @calendar[:date] = DateTime.now.beginning_of_day
105
+ @calendar[:verbose] = true
106
+ @calendar[:scope] = 'daily'
107
+ end
108
+ }
109
+
110
+ end
111
+
112
+ end
113
+
114
+
115
+ # Methods used in an ActiveRecord model to make it compatible with the library
116
+ # Note: A model must respond to :start_time and :end_time (:datetime fields)
117
+ #
118
+ # Usage:
119
+ #
120
+ # class MyEventModel < ActiveRecord::Base
121
+ # acts_as_event
122
+ # end
123
+ #
124
+ #
125
+ # class MyEventsMigration < ActiveRecord::Migration
126
+ # def change
127
+ # create_table :my_events do |t|
128
+ # t.datetime :start_time
129
+ # t.datetime :end_time
130
+ # end
131
+ # end
132
+ # end
133
+ #
134
+ # I did not want to dictate how your event model should be; that's why you must
135
+ # create your event model and your migration. However, once this is done, you
136
+ # can use :acts_as_event to ease the process. It will add the scopes needed to
137
+ # send the events to the calendars.
138
+ #
139
+ # Note: You can have an event model that is not persisted in your database.
140
+ # As long as your model responds to the two methods :start_time and :end_time,
141
+ # you're OK. However, :acts_as_event won't help you in that case.
142
+ #
143
+ module Model
144
+
145
+ module ActsAsEvent
146
+
147
+ def acts_as_event
148
+ scope :for_day, lambda { |date = nil| where('start_time >= ? AND start_time <= ?', date ? date : DateTime.now.beginning_of_day, date ? date.end_of_day : DateTime.now.end_of_day) }
149
+ scope :for_week, lambda { |date = nil, week_start = :monday|
150
+ where(
151
+ 'start_time >= ? AND start_time < ?',
152
+ date ? date.to_time.beginning_of_week(week_start) : DateTime.now.beginning_of_week(week_start),
153
+ date ? date.to_time.end_of_week(week_start) : DateTime.now.end_of_week(week_start)
154
+ )
155
+ }
156
+ end
157
+
158
+ end
159
+
160
+
161
+ class ActiveRecord::Base
162
+
163
+ extend ActsAsEvent
164
+
165
+ end
166
+
167
+ end
168
+
169
+
170
+ private
171
+
172
+ class AbstractCalendarBuilder < ActionView::Base
173
+
174
+ @@uuid = 100
175
+
176
+ attr_reader :event, :is_all_day
177
+
178
+
179
+ def initialize(view_context, *args)
180
+ opts = args.extract_options!
181
+
182
+ @view_context = view_context
183
+
184
+ @options = {
185
+ unit: 60,
186
+ date_format: :long,
187
+ url: @view_context.request.path,
188
+ verbose: true,
189
+ day_start: 0,
190
+ day_end: 1440
191
+ }.merge!(opts)
192
+
193
+ @day = args.shift || Date.today
194
+ @events = args.shift || []
195
+ @event = nil # can be accessed by the passed block
196
+ @is_all_day = false
197
+
198
+ @@uuid += 1
199
+ end
200
+
201
+
202
+ def compute
203
+ day_time = @day.to_datetime
204
+
205
+ # Get the starting and the ending row of the calendar
206
+ # :starting_row and :ending_row are in minutes so if :starting_row == 60
207
+ # and the :unit == 60, that means one row equals 1 hour and we start the
208
+ # rows at hour 1.
209
+ @_starting_row = @options[:day_start] / @options[:unit]
210
+ @_ending_row = @options[:day_end] / @options[:unit]
211
+
212
+ # Get the rows (in :datetime)
213
+ # The indexes will be used as :ids in the rendering
214
+ @rows = (@_starting_row...@_ending_row).to_a
215
+ @rows_hours = @rows.map { |r| day_time + r * @options[:unit] / 1440.0 }
216
+
217
+ # Sort the events in ascending order of their :start_time
218
+ # Note: not done in-place the first time so the user can re-use it's collection
219
+ @events = @events.sort { |e1, e2| e1.start_time <=> e2.start_time }
220
+
221
+ self
222
+ end
223
+
224
+
225
+ def render
226
+
227
+ end
228
+
229
+
230
+ protected
231
+
232
+ def row(time_with_zone)
233
+ (time_with_zone.to_datetime.hour * 60 + time_with_zone.to_datetime.min)
234
+ end
235
+
236
+
237
+ def row_unit(time_with_zone)
238
+ row(time_with_zone) / @options[:unit]
239
+ end
240
+
241
+
242
+ def to_query_params(options = {})
243
+ {
244
+ calendar: {
245
+ date: Date.today,
246
+ verbose: @options[:verbose],
247
+ scope: @options[:scope]
248
+ }.merge(options)
249
+ }.to_query
250
+ end
251
+
252
+ end
253
+
254
+
255
+ class DailyCalendarBuilder < AbstractCalendarBuilder
256
+
257
+ def initialize(view_context, *args)
258
+
259
+ opts = {
260
+ id: "daily_calendar_#{@@uuid}",
261
+ scope: 'daily'
262
+ }.merge!(args.extract_options!)
263
+
264
+ args << opts
265
+
266
+ super(view_context, *args)
267
+ end
268
+
269
+
270
+ def compute
271
+
272
+ super
273
+
274
+ # Partition the events between all-day and :not all-day events
275
+ # Then, for the :not all-day events, put them in rows
276
+ rows_events = {}
277
+ @all_day_events, not_all_day_events = @events.partition { |e| e.end_time.to_date > @day }
278
+
279
+ not_all_day_events.each do |e|
280
+ row_unit = row_unit(e.start_time)
281
+
282
+ if !rows_events.has_key?(row_unit)
283
+ rows_events[row_unit] = [e]
284
+ else
285
+ rows_events[row_unit] << e
286
+ end
287
+ end
288
+
289
+ # We remove the events that:
290
+ # - start or end before the :starting_row
291
+ # - start or end after the :ending_row
292
+ rows_events.each_value { |r| r.reject! { |e| row_unit(e.start_time) >= @_ending_row || row_unit(e.start_time) < @_starting_row } }
293
+ rows_events.each_value { |r| r.reject! { |e| row_unit(e.end_time) >= @_ending_row || row_unit(e.end_time) < @_starting_row } }
294
+
295
+ # Sort each row (that now contains events) by the duration of the event
296
+ rows_events.each_value { |r| r.sort! { |e1, e2| e2.end_time - e2.start_time <=> e1.end_time - e1.start_time } }
297
+
298
+ # Put the events in columns, which give a tuple '[[row_start, row_end, column], event]' for the event
299
+ # that will be used to output the calendar. The algorithm works like this:
300
+ # - As we cycle in the rows (which contains events that start at the same :unit time), we create columns and
301
+ # we keep track of what we put in the columns
302
+ # - When to decide in which column to put an event, we start from the column 0 and check the :end_time of the
303
+ # last event inserted in that column. If this :end_time is less than the :start_time of the event we want
304
+ # to insert, we have a candidate!
305
+ columns = {}
306
+ @placed_events = []
307
+
308
+ rows_events.each do |i,v|
309
+ v.each do |e|
310
+ j = 0
311
+ placed = false
312
+
313
+ while (!placed) do
314
+ if !columns.has_key?(j)
315
+ columns[j] = [e]
316
+ @placed_events << [[i, i + ((e.end_time - e.start_time) / (60 * @options[:unit])).ceil, j], e]
317
+ placed = true
318
+ elsif (row(e.start_time) >= row(columns[j].last.end_time))
319
+ columns[j] << e
320
+ @placed_events << [[i, i + ((e.end_time - e.start_time) / (60 * @options[:unit])).ceil, j], e]
321
+ placed = true
322
+ else
323
+ j += 1
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ # We make a list of all the rows to render
330
+ # If we set the option :verbose to false, we only show the rows that have events
331
+ occupied_rows = []
332
+ @placed_events.each { |tuple| occupied_rows << (tuple[0][0]...tuple[0][1]).to_a }
333
+ occupied_rows.flatten!
334
+ occupied_rows.uniq!
335
+ @rows_to_render_indexes = @options[:verbose] ? @rows.map.with_index { |x, i| i } : occupied_rows.map { |r| @rows.index(r) }
336
+
337
+ self
338
+ end
339
+
340
+
341
+ def render(&block)
342
+ content_tag(:div, class: 'daily_calendar', id: @options[:id], style: 'position: relative;') do
343
+
344
+ tables = ''.html_safe
345
+
346
+ # controls
347
+ tables << content_tag(:table, id: 'controls', class: 'styled', style: 'width: 100%;') do
348
+ content_tag(:thead) do
349
+ content_tag(:tr) do
350
+ content = ''.html_safe
351
+ content << content_tag(:th, style: 'width: 33%;') { link_to(@options[:verbose] ? I18n.t('calendarize.daily_calendar.options.verbose', default: 'Compact') : I18n.t('calendarize.daily_calendar.options.not_verbose', default: 'Full'), @options[:url] + '?' + to_query_params({ date: @day.to_date, verbose: !@options[:verbose] })) }
352
+ content << content_tag(:th, style: 'width: 33%;') { I18n.l(@day.to_date, format: @options[:date_format]) }
353
+ content << content_tag(:th, style: 'width: 33%;') do
354
+ options = ''.html_safe
355
+ options << link_to(I18n.t('calendarize.daily_calendar.options.previous_day', default: 'Previous day'), @options[:url] + '?' + to_query_params({ date: @day.yesterday.to_date }))
356
+ options << ' | '
357
+ options << link_to(I18n.t('calendarize.daily_calendar.options.today', default: 'Today'), @options[:url] + '?' + to_query_params)
358
+ options << ' | '
359
+ options << link_to(I18n.t('calendarize.daily_calendar.options.next_day', default: 'Next day'), @options[:url] + '?' + to_query_params({ date: @day.tomorrow.to_date }))
360
+ options
361
+ end
362
+ content
363
+ end
364
+ end
365
+ end
366
+
367
+ # all day events
368
+ tables << content_tag(:table, id: 'all_day', class: 'styled', style: 'width: 100%;') do
369
+ content = ''.html_safe
370
+ content << content_tag(:thead) do
371
+ content_tag(:tr) do
372
+ content_tag(:th) { I18n.t('calendarize.daily_calendar.all_day_events', default: 'All day events') }
373
+ end
374
+ end
375
+
376
+ content << content_tag(:tbody) do
377
+
378
+ trs = ''.html_safe
379
+
380
+ @all_day_events.each_index do |i|
381
+ trs << content_tag(:tr) do
382
+ @event = @all_day_events[i]
383
+ @is_all_day = true
384
+ content_tag(:td, class: 'row_content', id: "row_content_#{i}") { @view_context.capture(self, &block) }
385
+ end
386
+ end
387
+
388
+ trs
389
+ end
390
+
391
+ content
392
+ end unless @all_day_events.empty?
393
+
394
+ # normal events
395
+ tables << content_tag(:table, id: 'not_all_day', class: 'styled', style: 'width: 100%;') do
396
+ content = ''.html_safe
397
+ content << content_tag(:thead) do
398
+ content_tag(:tr) do
399
+ header = ''.html_safe
400
+ header << content_tag(:th, style: 'width: 40px;') { I18n.t('calendarize.daily_calendar.hours', default: 'Hours') }
401
+ header << content_tag(:th) { }
402
+ header
403
+ end
404
+ end
405
+
406
+ content << content_tag(:tbody) do
407
+
408
+ trs = ''.html_safe
409
+
410
+ #raise @rows.inspect
411
+ @rows_to_render_indexes.each do |i|
412
+ trs << content_tag(:tr, class: 'row_unit', style: 'height: 50px;') do
413
+ tds = ''.html_safe
414
+ tds << content_tag(:td, class: 'row_header', id: "row_header_#{@rows[i]}", style: 'width: 40px') { @rows_hours[i].strftime('%H:%M') }
415
+ tds << content_tag(:td, class: 'row_content', id: "row_content_#{@rows[i]}") { }
416
+ tds
417
+ end
418
+ end
419
+
420
+ trs
421
+ end
422
+
423
+ content
424
+ end
425
+
426
+ # place the events at the end of the calendar
427
+ # they will be placed at the right place on the calendar with some javascript magic
428
+ @placed_events.each do |e|
429
+ #raise e.inspect
430
+ @event = e[1]
431
+ @is_all_day = false
432
+ tables << content_tag(:div, class: 'calendar_event', data: { row_start: e[0][0], row_end: e[0][1], column: e[0][2] }, style: 'z-index: 1;') do
433
+ content_tag(:div, class: 'content') { @view_context.capture(self, &block) }
434
+ end
435
+ end
436
+
437
+ tables
438
+ end
439
+ end
440
+
441
+ end
442
+
443
+
444
+ class WeeklyCalendarBuilder < AbstractCalendarBuilder
445
+
446
+ def initialize(view_context, *args)
447
+
448
+ opts = {
449
+ id: "weekly_calendar_#{@@uuid}",
450
+ week_start: :monday,
451
+ week_end: :sunday,
452
+ scope: 'weekly'
453
+ }.merge!(args.extract_options!)
454
+
455
+ opts[:week_start] = opts[:week_start].to_sym
456
+ opts[:week_end] = opts[:week_end].to_sym
457
+
458
+ args << opts
459
+
460
+ super(view_context, *args)
461
+
462
+ # We calculate the number of days between :week_start and :week_end
463
+ ws = Date::DAYS_INTO_WEEK[@options[:week_start]]
464
+ we = Date::DAYS_INTO_WEEK[@options[:week_end]]
465
+ we += 7 if we <= ws
466
+
467
+ @day_start = @day.beginning_of_week(@options[:week_start]).to_date
468
+ @day_end = @day_start + (we - ws)
469
+
470
+ end
471
+
472
+
473
+ def compute
474
+
475
+ super
476
+
477
+ # We put events in rows
478
+ @_rows_events = {}
479
+
480
+ @events.each do |e|
481
+ row_unit = row_unit(e.start_time)
482
+
483
+ if !@_rows_events.has_key?(row_unit)
484
+ @_rows_events[row_unit] = [e]
485
+ else
486
+ @_rows_events[row_unit] << e
487
+ end
488
+ end
489
+
490
+ # We remove the events that:
491
+ # - start or end before the :starting_row
492
+ # - start or end after the :ending_row
493
+ @_rows_events.each_value { |r| r.reject! { |e| row_unit(e.start_time) >= @_ending_row || row_unit(e.start_time) < @_starting_row } }
494
+ @_rows_events.each_value { |r| r.reject! { |e| row_unit(e.end_time) >= @_ending_row || row_unit(e.end_time) < @_starting_row } }
495
+
496
+ # Sort each row (that now contains events) by the duration of the event
497
+ @_rows_events.each_value { |r| r.sort! { |e1, e2| e2.end_time - e2.start_time <=> e1.end_time - e1.start_time } }
498
+
499
+ # We remove the events that are outside the days watched
500
+ @_rows_events.each_value { |r| r.reject! { |e| e.start_time.to_date < @day_start || e.start_time.to_date > @day_end } }
501
+
502
+ @days_shown = (@day_start..@day_end).to_a
503
+
504
+ # Put the events in columns, which give a tuple '[[row, column, index], event]' for the event
505
+ # that will be used to output the calendar. The :index is used to identify an event on a same
506
+ # :row and :column
507
+ @placed_events = []
508
+
509
+ @_rows_events.each do |i, v|
510
+ columns = {}
511
+ v.each { |e| columns[column(e.start_time)] = 0 }
512
+
513
+ v.each do |e|
514
+ column = column(e.start_time)
515
+
516
+ @placed_events << [[i, column, columns[column]], e]
517
+
518
+ columns[column] += 1
519
+ end
520
+ end
521
+
522
+ # We make a list of all the rows to render
523
+ # If we set the option :verbose to false, we only show the rows that have events
524
+ occupied_rows = []
525
+ @placed_events.each { |tuple| occupied_rows << tuple[0][0] }
526
+ occupied_rows.uniq!
527
+
528
+ @rows_to_render_indexes = @options[:verbose] ? @rows.map.with_index { |x, i| i } : occupied_rows.map { |r| @rows.index(r) }
529
+
530
+ self
531
+ end
532
+
533
+
534
+ def render(&block)
535
+ content_tag(:div, class: 'weekly_calendar', id: @options[:id], style: 'position: relative;') do
536
+
537
+ tables = ''.html_safe
538
+
539
+ # controls
540
+ tables << content_tag(:table, id: 'controls', class: 'styled', style: 'width: 100%;') do
541
+ content_tag(:thead) do
542
+ content_tag(:tr) do
543
+ content = ''.html_safe
544
+ content << content_tag(:th, style: 'width: 33%;') { link_to(@options[:verbose] ? I18n.t('calendarize.weekly_calendar.options.verbose', default: 'Compact') : I18n.t('calendarize.weekly_calendar.options.not_verbose', default: 'Full'), @options[:url] + '?' + to_query_params({ date: @day.to_date, verbose: !@options[:verbose] })) }
545
+ content << content_tag(:th, style: 'width: 33%;') { "#{I18n.t('calendarize.weekly_calendar.week_of', default: 'Week of')} #{ I18n.l(@day_start.to_date, format: @options[:date_format]) }" }
546
+ content << content_tag(:th, style: 'width: 33%;') do
547
+ options = ''.html_safe
548
+ options << link_to(I18n.t('calendarize.weekly_calendar.options.previous_week', default: 'Previous week'), @options[:url] + '?' + to_query_params({ date: @day_start.prev_week(@options[:week_start]).to_date }))
549
+ options << ' | '
550
+ options << link_to(I18n.t('calendarize.weekly_calendar.options.current_week', default: 'This week'), @options[:url] + '?' + to_query_params)
551
+ options << ' | '
552
+ options << link_to(I18n.t('calendarize.weekly_calendar.options.next_week', default: 'Next week'), @options[:url] + '?' + to_query_params({ date: @day_end.next_week(@options[:week_start]).to_date }))
553
+ options
554
+ end
555
+ content
556
+ end
557
+ end
558
+ end
559
+
560
+ # normal events
561
+ tables << content_tag(:table, id: 'not_all_day', class: 'styled', style: 'width: 100%;') do
562
+ content = ''.html_safe
563
+ content << content_tag(:thead) do
564
+ content_tag(:tr) do
565
+ header = ''.html_safe
566
+
567
+ header << content_tag(:th, style: 'width: 40px;') { I18n.t('calendarize.weekly_calendar.hours', default: 'Hours') }
568
+
569
+ @days_shown.each do |d|
570
+ header << content_tag(:th) {
571
+ link_to(I18n.l(d, format: @options[:date_format]), @options[:url] + '?' + to_query_params({ date: d, verbose: @options[:verbose], scope: CalendarizeHelper::Scopes::DAILY }))
572
+ }
573
+ end
574
+
575
+ header
576
+ end
577
+ end
578
+
579
+ content << content_tag(:tbody) do
580
+
581
+ trs = ''.html_safe
582
+
583
+ @rows_to_render_indexes.each do |i|
584
+ trs << content_tag(:tr, class: 'row_unit', data: { events_count: @_rows_events.include?(@rows[i]) ? @_rows_events[@rows[i]].map{ |e| column(e.start_time) }.group_by{ |i| i }.map{ |k, v| v.count }.max : 0 }) do
585
+ tds = ''.html_safe
586
+
587
+ tds << content_tag(:td, class: 'row_header', id: "row_header_#{@rows[i]}", style: 'width: 40px') { @rows_hours[i].strftime('%H:%M') }
588
+
589
+ @days_shown.each_index do |j|
590
+ tds << content_tag(:td, class: ["row_#{@rows[i]}", "column_#{j}"]) { }
591
+ end
592
+
593
+ tds
594
+ end
595
+ end
596
+
597
+ trs
598
+ end
599
+
600
+ content
601
+ end
602
+
603
+ # place the events at the end of the calendar
604
+ # they will be placed at the right place on the calendar with some javascript magic
605
+ @placed_events.each do |e|
606
+ @event = e[1]
607
+ @is_all_day = false
608
+ tables << content_tag(:div, class: 'calendar_event', data: { row: e[0][0], column: e[0][1], index: e[0][2] }, style: 'z-index: 1;') do
609
+ content_tag(:div, class: 'content') { @view_context.capture(self, &block) }
610
+ end
611
+ end
612
+
613
+ tables
614
+ end
615
+ end
616
+
617
+
618
+ private
619
+
620
+ def column(time_with_zone)
621
+ ws = Date::DAYS_INTO_WEEK[@options[:week_start]]
622
+ we = time_with_zone.strftime('%u').to_i - 1
623
+ we += 7 if we <= ws
624
+ (we - ws) % 7
625
+ end
626
+
627
+ end
628
+
629
+ end