flog 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/spec/spec.opts ADDED
@@ -0,0 +1,3 @@
1
+ --format
2
+ s
3
+ --colour
@@ -0,0 +1,36 @@
1
+ # this is my favorite way to require ever
2
+ begin
3
+ require 'spec'
4
+ rescue LoadError
5
+ require 'rubygems'
6
+ gem 'rspec'
7
+ require 'spec'
8
+ end
9
+
10
+ begin
11
+ require 'mocha'
12
+ rescue LoadError
13
+ require 'rubygems'
14
+ gem 'mocha'
15
+ require 'mocha'
16
+ end
17
+
18
+
19
+ module Spec::Example::ExampleGroupMethods
20
+ def currently(name, &block)
21
+ it("*** CURRENTLY *** #{name}", &block)
22
+ end
23
+ end
24
+
25
+ Spec::Runner.configure do |config|
26
+ config.mock_with :mocha
27
+ end
28
+
29
+ def fixture_files(paths)
30
+ paths.collect do |path|
31
+ File.expand_path(File.dirname(__FILE__) + '/../spec_fixtures/' + path)
32
+ end
33
+ end
34
+
35
+
36
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
@@ -0,0 +1,199 @@
1
+ module Centerstone #:nodoc:
2
+ module Acts #:nodoc:
3
+ module DateRange
4
+ include ActionView::Helpers::DateHelper
5
+
6
+ def self.included(base) # :nodoc:
7
+ base.extend ClassMethods
8
+ unless ActiveRecord::Base.respond_to?(:end_dated_association_date)
9
+ class << ActiveRecord::Base
10
+ attr :end_dated_association_date, true
11
+ end
12
+ ActiveRecord::Base.end_dated_association_date = Proc.new { Time.now }
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+ def self.included(base) # :nodoc:
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ %w{begin end}.each do |bound|
22
+ define_method "acts_as_range_#{bound}" do
23
+ send(self.class.send("acts_as_range_#{bound}_attr").to_sym)
24
+ end
25
+
26
+ define_method "acts_as_range_#{bound}=" do |val|
27
+ send((self.class.send("acts_as_range_#{bound}_attr").to_s + '=').to_sym, val)
28
+ end
29
+ end
30
+
31
+ # does the range for this object include the specified point in time?
32
+ def include?(t)
33
+ true if self.class.find(self.id, :on => t)
34
+ rescue
35
+ false
36
+ end
37
+
38
+ # expire this object
39
+ def expire(time = Time.now)
40
+ return false if acts_as_range_end
41
+ if time.is_a?(Time)
42
+ self.acts_as_range_end = 1.second.ago(time)
43
+ elsif time.is_a?(Date)
44
+ self.acts_as_range_end = time - 1
45
+ end
46
+ save!
47
+ end
48
+
49
+ # see if this object is expired
50
+ def expired?(time = Time.now)
51
+ return false if not acts_as_range_end
52
+ return acts_as_range_end <= time
53
+ end
54
+
55
+ # return a description of how long this object (has) lived (as of some specified point in time)
56
+ def lifetime(time = Time.now)
57
+ return 'forever' if acts_as_range_begin.nil?
58
+ return 'in future' if acts_as_range_end.nil? and acts_as_range_begin > time
59
+ distance_of_time_in_words(acts_as_range_begin, acts_as_range_end || time)
60
+ end
61
+
62
+ def limit_date_range(&block)
63
+ time = [ acts_as_range_begin, acts_as_range_end ]
64
+ prior, ActiveRecord::Base.end_dated_association_date = ActiveRecord::Base.end_dated_association_date, Proc.new { time }
65
+ yield
66
+ ensure
67
+ ActiveRecord::Base.end_dated_association_date = prior
68
+ end
69
+
70
+ def destroy_without_callbacks
71
+ unless new_record?
72
+ if acts_as_range_configuration[:end_dated]
73
+ now = self.default_timezone == :utc ? Time.now.utc : Time.now
74
+ self.class.update_all self.class.send(:sanitize_sql, ["#{acts_as_range_end_attr} = ?", now]), "id = #{quote_value(id)}"
75
+ else
76
+ super
77
+ end
78
+ end
79
+ freeze
80
+ end
81
+
82
+
83
+ module ClassMethods
84
+
85
+ # find objects with intervals including the current time
86
+ def with_current_time_scope(&block)
87
+ t = ActiveRecord::Base.end_dated_association_date.call
88
+ if t.respond_to? :first
89
+ with_overlapping_scope(t.first, t.last, &block)
90
+ else
91
+ with_containing_scope(t, t, &block)
92
+ end
93
+ end
94
+
95
+ end
96
+ end
97
+
98
+ module ClassMethods
99
+ def acts_as_date_range(options = {})
100
+ return if acts_as_date_range? # don't let this be done twice
101
+ raise "options must be a Hash" unless options.is_a?(Hash)
102
+
103
+ acts_as_range({ :begin => :begin_time, :end => :end_time }.update(options))
104
+ acts_as_date_range_configure_class(options)
105
+ end
106
+
107
+ def acts_as_date_range?
108
+ included_modules.include?(InstanceMethods)
109
+ end
110
+
111
+ def sequentialized?
112
+ sequentialized_on ? true : false
113
+ end
114
+
115
+ def sequentialized_on
116
+ acts_as_range_configuration[:sequentialize]
117
+ end
118
+
119
+ protected
120
+
121
+ def acts_as_date_range_singleton_sequentialize_class
122
+ before_validation_on_create do |obj|
123
+ obj.acts_as_range_begin ||= Time.now
124
+ true
125
+ end
126
+
127
+ before_create do |obj|
128
+ # Expiring any open object
129
+ obj.class.find(:all, :conditions => "#{acts_as_range_end_attr} is null").each {|o| o.expire(obj.acts_as_range_begin)}
130
+ true
131
+ end
132
+
133
+ validate_on_create do |obj|
134
+ # If any record defines a date after the begin_date then data is corrupt
135
+ if obj.class.count(:conditions => ["#{acts_as_range_begin_attr} >= ? or #{acts_as_range_end_attr} > ?",
136
+ obj.acts_as_range_begin, obj.acts_as_range_begin]) > 0
137
+ obj.errors.add(acts_as_range_begin_attr, 'Begin time is before other keys begin time or end time')
138
+ end
139
+ true
140
+ end
141
+ end
142
+
143
+ module ParamExtension
144
+ def to_sql
145
+ collect { |elem| "#{elem} = ?" }.join(' and ')
146
+ end
147
+
148
+ def to_attributes_for(obj)
149
+ collect { |elem| obj.attributes[elem.to_s] }
150
+ end
151
+ end
152
+
153
+
154
+ def acts_as_date_range_param_sequentialize_class(*params)
155
+ params.extend(ParamExtension)
156
+
157
+ before_validation_on_create do |obj|
158
+ obj.acts_as_range_begin ||= Time.now
159
+ true
160
+ end
161
+
162
+ before_create do |obj|
163
+ # Expiring any open object
164
+ obj.class.find(:all, :conditions => ["#{acts_as_range_end_attr} is null and #{params.to_sql}",
165
+ params.to_attributes_for(obj)].flatten).each do |o|
166
+ o.expire(obj.acts_as_range_begin)
167
+ end
168
+ true
169
+ end
170
+
171
+ validate_on_create do |obj|
172
+ # If any record defines a date after the begin_date then data is corrupt
173
+ if obj.class.count(:conditions => ["#{params.to_sql} and (#{acts_as_range_begin_attr} >= ? or #{acts_as_range_end_attr} > ?)",
174
+ params.to_attributes_for(obj), obj.acts_as_range_begin, obj.acts_as_range_begin].flatten) > 0
175
+ obj.errors.add(acts_as_range_begin_attr, 'Begin time is before other begin time or end time')
176
+ end
177
+ true
178
+ end
179
+ end
180
+
181
+ def acts_as_date_range_sequentialize_class(*params)
182
+ params.flatten!
183
+ if params == [true]
184
+ acts_as_date_range_singleton_sequentialize_class
185
+ else
186
+ acts_as_date_range_param_sequentialize_class(*params)
187
+ end
188
+ end
189
+
190
+ def acts_as_date_range_configure_class(options = {})
191
+ write_inheritable_attribute(:acts_as_date_range_configuration, options)
192
+ include InstanceMethods
193
+
194
+ acts_as_date_range_sequentialize_class(options[:sequentialize]) if options[:sequentialize]
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,391 @@
1
+ module Centerstone #:nodoc:
2
+ module Acts #:nodoc:
3
+ module Range
4
+
5
+ def self.included(base) # :nodoc:
6
+ base.extend ClassMethods
7
+ unless ActiveRecord::Base.respond_to?(:end_dated_association_date)
8
+ class << ActiveRecord::Base
9
+ attr :end_dated_association_date, true
10
+ end
11
+ ActiveRecord::Base.end_dated_association_date = Proc.new { Time.now }
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ def self.included(base) # :nodoc:
17
+ base.extend ClassMethods
18
+ end
19
+
20
+ %w{begin end}.each do |bound|
21
+ define_method "acts_as_range_#{bound}" do
22
+ send(self.class.send("acts_as_range_#{bound}_attr").to_sym)
23
+ end
24
+
25
+ define_method "acts_as_range_#{bound}=" do |val|
26
+ send((self.class.send("acts_as_range_#{bound}_attr").to_s + '=').to_sym, val)
27
+ end
28
+ end
29
+
30
+ # convert this object into a range
31
+ # do something about nil/unspecified ends?
32
+ def to_range
33
+ begin_point = acts_as_range_begin
34
+ end_point = acts_as_range_end
35
+
36
+ begin_point ... end_point
37
+ end
38
+
39
+ # does the range for this object include the specified point?
40
+ def include?(point)
41
+ true if self.class.find(self.id, :on => point)
42
+ rescue
43
+ false
44
+ end
45
+
46
+ def contained_by?(range)
47
+ if range.respond_to?(:acts_as_range_begin)
48
+ # if this is really a range-ish object
49
+ if range.acts_as_range_begin and range.acts_as_range_end
50
+ # if it can actually be represented as a range
51
+ return contained_by?(range.to_range)
52
+ elsif range.acts_as_range_begin or range.acts_as_range_end
53
+ # if at least one bound is defined, compare that bound
54
+ if range.acts_as_range_begin
55
+ return false unless acts_as_range_begin
56
+ return acts_as_range_begin >= range.acts_as_range_begin
57
+ end
58
+
59
+ if range.acts_as_range_end
60
+ return false unless acts_as_range_end
61
+ return acts_as_range_end <= range.acts_as_range_end
62
+ end
63
+ else
64
+ # the given "range" has no bounds, thus it contains all
65
+ return true
66
+ end
67
+ end
68
+
69
+ # if the given range includes both bounds, it contains this object
70
+ # Note: Checking the end bound is a little tricky because of the exclusion choice with ranges (but not with acts_as_range)
71
+ range.include?(acts_as_range_begin) and (range.include?(acts_as_range_end) or ((range.last == acts_as_range_end) and (range.exclude_end? == to_range.exclude_end?)))
72
+ end
73
+
74
+ def containing?(target)
75
+ if target.respond_to?(:acts_as_range_begin)
76
+ # if this is really a range-ish object
77
+ # enough work has been done on contained_by?, so let's use that if we can
78
+ target.contained_by?(self)
79
+ else
80
+ if target.is_a?(::Range) # core Range class, not this Range module
81
+ if acts_as_range_begin and acts_as_range_end
82
+ # if this object can actually be represented as a range
83
+ # if this object's range includes both bounds of the given range, it contains that range
84
+ # Note: Checking the end bound is a little tricky because of the exclusion choice with ranges (but not with acts_as_range)
85
+ to_range.include?(target.first) and (to_range.include?(target.last) or ((to_range.last == target.last) and (to_range.exclude_end? == target.exclude_end?)))
86
+ elsif acts_as_range_begin or acts_as_range_end
87
+ # if at least one bound is defined, compare that bound
88
+ if acts_as_range_begin
89
+ return acts_as_range_begin <= target.first
90
+ end
91
+
92
+ if acts_as_range_end
93
+ # note the problem with checking the end bound above
94
+ return (acts_as_range_end > target.last or ((acts_as_range_end == target.last) and target.exclude_end?))
95
+ end
96
+ else
97
+ # this object's "range" has no bounds, thus it contains all
98
+ true
99
+ end
100
+ else
101
+ # this is a single point, so check it
102
+ begin_point = acts_as_range_begin || target
103
+ end_point = acts_as_range_end || target
104
+
105
+ (begin_point ... end_point).include?(target) or ((begin_point <= target) and !acts_as_range_end)
106
+ end
107
+ end
108
+ end
109
+ alias_method :contains?, :containing?
110
+
111
+ def overlapping?(target)
112
+ # contained by target or containing target
113
+ return true if contained_by?(target) or containing?(target)
114
+
115
+ # or containing either bound
116
+ if target.respond_to?(:acts_as_range_begin)
117
+ containing_begin = containing?(target.acts_as_range_begin) if target.acts_as_range_begin
118
+ containing_end = containing?(target.acts_as_range_end) if target.acts_as_range_end
119
+
120
+ return (containing_begin or containing_end)
121
+ else
122
+ if target.is_a?(::Range) # core Range class, not this Range module
123
+ return (containing?(target.first) or containing?(target.last))
124
+ end
125
+ end
126
+
127
+ # if it got this far
128
+ false
129
+ end
130
+ alias_method :overlaps?, :overlapping?
131
+
132
+ def before?(point)
133
+ return false unless point
134
+ return false unless acts_as_range_end
135
+
136
+ if point.respond_to?(:acts_as_range_begin)
137
+ return before?(point.acts_as_range_begin)
138
+ end
139
+
140
+ acts_as_range_end < point
141
+ end
142
+
143
+ def after?(point)
144
+ return false unless point
145
+ return false unless acts_as_range_begin
146
+
147
+ if point.respond_to?(:acts_as_range_end)
148
+ return after?(point.acts_as_range_end)
149
+ end
150
+
151
+ acts_as_range_begin > point
152
+ end
153
+
154
+ module ClassMethods
155
+
156
+ %w{begin end}.each do |bound|
157
+ define_method "acts_as_range_#{bound}_attr" do
158
+ acts_as_range_configuration[bound.to_sym]
159
+ end
160
+ end
161
+
162
+ # add new options to Foo.find:
163
+ # :contain => t1 .. t2 - return objects whose spans contain this time interval
164
+ # :containing => t1 .. t2 - return objects whose spans contain this time interval
165
+ # :contained_by => t1 .. t2 - return objects whose spans are contained by this time interval
166
+ # :overlapping => t1 .. t2 - return objects whose spans overlap this time interval
167
+ # :on => t1 - return objects whose spans contain this time point
168
+ # :before => t1 - return objects whose spans are completed on or before this time point
169
+ # :after => t1 - return objects whose spans begin on or after this time point
170
+ #
171
+ # Note that each of the time interval methods will also take an object of this
172
+ # class and will use the time interval from that object as search parameters.
173
+ def find_with_range_restrictions(*args)
174
+ original_args = args.dup
175
+ options = extract_options_from_args!(args)
176
+
177
+ # which new arguments do we recognize, and which scoping methods do they use?
178
+ # eh, I don't like 'on' or 'contain'. Like the non-database instance methods
179
+ # above, 'containing' could handle both cases of range and point
180
+ method_map = { :contain => :with_containing_scope,
181
+ :containing => :with_containing_scope,
182
+ :contained_by => :with_contained_scope,
183
+ :overlapping => :with_overlapping_scope,
184
+ :on => nil,
185
+ :before => nil,
186
+ :after => nil }
187
+
188
+ # find objects with time intervals containing this time point
189
+ if options.has_key? :on
190
+ return with_containing_scope(options[:on], options[:on]) do
191
+ find_without_range_restrictions(*remove_args(original_args, method_map.keys))
192
+ end
193
+ end
194
+
195
+ # find objects with time intervals containing this time point
196
+ if options.has_key? :before
197
+ return with_before_scope(options[:before]) do
198
+ find_without_range_restrictions(*remove_args(original_args, method_map.keys))
199
+ end
200
+ end
201
+
202
+ # find objects with time intervals containing this time point
203
+ if options.has_key? :after
204
+ return with_after_scope(options[:after]) do
205
+ find_without_range_restrictions(*remove_args(original_args, method_map.keys))
206
+ end
207
+ end
208
+
209
+ # otherwise, find objects with time intervals matching this range
210
+ method_map.keys.each do |kind|
211
+ if options.has_key? kind
212
+ x = ranged_lookup(options[kind]) do |start, stop|
213
+ self.send(method_map[kind], start, stop) do
214
+ find_without_range_restrictions(*remove_args(original_args, method_map.keys))
215
+ end
216
+ end
217
+ # Patch for find :first, :conditions => 'impossible'
218
+ # Could be cleaner, but this handles it
219
+ return x == [nil] ? nil : x
220
+ end
221
+ end
222
+
223
+ # otherwise, find objects with time intervals active now
224
+ if acts_as_range_configuration[:end_dated]
225
+ with_current_time_scope { find_without_range_restrictions(*original_args) }
226
+ else
227
+ find_without_range_restrictions(*original_args)
228
+ end
229
+ end
230
+
231
+ def count_with_range_restrictions(*args)
232
+ if acts_as_range_configuration[:end_dated]
233
+ with_current_time_scope { count_without_range_restrictions(*args) }
234
+ else
235
+ count_without_range_restrictions(*args)
236
+ end
237
+ end
238
+
239
+ def calculate_with_range_restrictions(*args)
240
+ if acts_as_range_configuration[:end_dated]
241
+ with_current_time_scope { calculate_without_range_restrictions(*args) }
242
+ else
243
+ calculate_without_range_restrictions(*args)
244
+ end
245
+ end
246
+
247
+ # break out an args list, add in new options, return a new args list
248
+ def add_args(args, added)
249
+ args << extract_options_from_args!(args).merge(added)
250
+ end
251
+
252
+ protected
253
+
254
+ # break out an args list, remove specified options, return a new args list
255
+ def remove_args(args, removed)
256
+ options = extract_options_from_args!(args)
257
+ removed.each {|k| options.delete(k)}
258
+ args << options
259
+ args.last.keys.length > 0 ? args : args.first
260
+ end
261
+
262
+ # provide for lookups on a date range /or/ an acts_as_range object (filtering object out)
263
+ def ranged_lookup(obj)
264
+ filter_object = obj.respond_to?(:acts_as_range_begin)
265
+ start, stop = filter_object ? [obj.acts_as_range_begin, obj.acts_as_range_end] : [obj.first, obj.last]
266
+ result = yield(start, stop)
267
+ filter_object ? (Set.new(result) - Set.new([obj])).to_a : result
268
+ end
269
+
270
+ # find objects with intervals including the current time
271
+ def with_current_time_scope(&block)
272
+ t = ActiveRecord::Base.end_dated_association_date.call
273
+ if t.respond_to? :first
274
+ with_overlapping_scope(t.first, t.last, &block)
275
+ else
276
+ with_containing_scope(t, t, &block)
277
+ end
278
+ end
279
+
280
+ # find objects which are entirely before the specified time
281
+ def with_before_scope(t, &block)
282
+ with_scope({:find => { :conditions => ["(#{table_name}.#{acts_as_range_end_attr} is not null and #{table_name}.#{acts_as_range_end_attr} < ?)", t ] } }, :merge, &block)
283
+ end
284
+
285
+ # find objects which are entirely after the specified time
286
+ def with_after_scope(t, &block)
287
+ with_scope({:find => { :conditions => ["(#{table_name}.#{acts_as_range_begin_attr} is not null and #{table_name}.#{acts_as_range_begin_attr} > ?)", t ] } }, :merge, &block)
288
+ end
289
+
290
+ # find objects with intervals contained by the interval t1 .. t2
291
+ def with_contained_scope(t1, t2, &block)
292
+ conditions = []
293
+ args = []
294
+
295
+ if t1.nil?
296
+ if t2.nil?
297
+ conditions << "(1=1)"
298
+ else
299
+ conditions << "(#{table_name}.#{acts_as_range_end_attr} is not NULL and #{table_name}.#{acts_as_range_end_attr} < ?)"
300
+ args << t2
301
+ end
302
+ elsif t2.nil?
303
+ conditions << "(#{table_name}.#{acts_as_range_begin_attr} is not NULL and #{table_name}.#{acts_as_range_begin_attr} >= ?)"
304
+ args << t1
305
+ else
306
+ conditions << "(#{table_name}.#{acts_as_range_begin_attr} is not NULL and #{table_name}.#{acts_as_range_begin_attr} >= ?)"
307
+ args << t1
308
+ conditions << "(#{table_name}.#{acts_as_range_end_attr} is not NULL and #{table_name}.#{acts_as_range_end_attr} < ?)"
309
+ args << t2
310
+ end
311
+
312
+ conditions = ([ conditions.join(' AND ') ] << args).flatten
313
+ with_scope({:find => { :conditions => conditions } }, :merge, &block)
314
+ end
315
+
316
+ # find objects with intervals containing the interval t1 .. t2
317
+ def with_containing_scope(t1, t2, &block)
318
+ conditions = []
319
+ args = []
320
+
321
+ if t1.nil?
322
+ conditions << "(#{table_name}.#{acts_as_range_begin_attr} is NULL)"
323
+ else
324
+ conditions << "(#{table_name}.#{acts_as_range_begin_attr} <= ? or #{table_name}.#{acts_as_range_begin_attr} IS NULL)"
325
+ args << t1
326
+ end
327
+
328
+ if t2.nil?
329
+ conditions << "(#{table_name}.#{acts_as_range_end_attr} is NULL)"
330
+ else
331
+ conditions << "(#{table_name}.#{acts_as_range_end_attr} > ? or #{table_name}.#{acts_as_range_end_attr} IS NULL)"
332
+ args << t2
333
+ end
334
+
335
+ conditions = ([ conditions.join(' AND ') ] << args).flatten
336
+ with_scope({:find => { :conditions => conditions } }, :merge, &block)
337
+ end
338
+
339
+ # find objects with intervals overlapping the interval t1 .. t2
340
+ def with_overlapping_scope(t1, t2, &block)
341
+ [with_containing_scope(t1, t1, &block)].flatten | [with_containing_scope(t2, t2, &block)].flatten | [with_contained_scope(t1, t2, &block)].flatten
342
+ end
343
+ end
344
+ end
345
+
346
+ module ClassMethods
347
+ def acts_as_range(options = {})
348
+ return if acts_as_range? # don't let this be done twice
349
+ class_inheritable_reader :acts_as_range_configuration
350
+ raise "options must be a Hash" unless options.is_a?(Hash)
351
+
352
+ acts_as_range_configure_class({ :begin => :begin, :end => :end }.update(options))
353
+ end
354
+
355
+ def acts_as_range?
356
+ included_modules.include?(InstanceMethods)
357
+ end
358
+
359
+ # ensure that the beginning of the interval does not follow its end
360
+ def validates_interval
361
+ configuration = { :message => "#{acts_as_range_configuration[:begin].to_s.humanize} must be before #{acts_as_range_configuration[:end].to_s.humanize}.", :on => [ :save, :update ] }
362
+ configuration[:on].each do |symbol|
363
+ send(validation_method(symbol)) do |record|
364
+ unless configuration[:if] && !evaluate_condition(configuration[:if], record)
365
+ start, stop = record.acts_as_range_begin, record.acts_as_range_end
366
+ unless start.nil? or stop.nil? or start <= stop
367
+ record.errors.add(acts_as_range_configuration[:begin], configuration[:message])
368
+ end
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ protected
375
+
376
+ def acts_as_range_configure_class(options = {})
377
+ include InstanceMethods
378
+ write_inheritable_attribute(:acts_as_range_configuration, options)
379
+
380
+ class << self
381
+ %w{find count calculate}.each do |method|
382
+ alias_method_chain method.to_sym, :range_restrictions
383
+ end
384
+ end
385
+
386
+ validates_interval
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end