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/History.txt +15 -1
- data/Manifest.txt +17 -0
- data/bin/flog +52 -23
- data/lib/flog.rb +230 -179
- data/lib/gauntlet_flog.rb +193 -0
- data/spec/flog_command_spec.rb +352 -0
- data/spec/flog_integration_spec.rb +944 -0
- data/spec/flog_spec.rb +1123 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +36 -0
- data/spec_fixtures/collection/bigger_example/acts/date_range.rb +199 -0
- data/spec_fixtures/collection/bigger_example/acts/range.rb +391 -0
- data/spec_fixtures/collection/bigger_example/association_extensions/date_ranged.rb +11 -0
- data/spec_fixtures/collection/bigger_example/association_extensions/ranged.rb +13 -0
- data/spec_fixtures/collection/bigger_example/reflection_extensions/ranged.rb +50 -0
- data/spec_fixtures/directory/bot_filter.rb +70 -0
- data/spec_fixtures/directory/bot_parser.rb +79 -0
- data/spec_fixtures/directory/bot_parser_format.rb +23 -0
- data/spec_fixtures/directory/bot_sender.rb +46 -0
- data/spec_fixtures/empty/empty.rb +0 -0
- data/spec_fixtures/simple/simple.rb +191 -0
- metadata +21 -4
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|