flog 1.2.0 → 2.0.0

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.
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