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