calendarize 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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