acts_as_span 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +47 -0
- data/LICENSE +22 -0
- data/README.rdoc +33 -0
- data/Rakefile +4 -0
- data/acts_as_span.gemspec +23 -0
- data/lib/acts_as_span.rb +161 -0
- data/lib/acts_as_span/span_instance.rb +53 -0
- data/lib/acts_as_span/span_instance/overlap.rb +17 -0
- data/lib/acts_as_span/span_instance/status.rb +58 -0
- data/lib/acts_as_span/span_instance/validations.rb +67 -0
- data/lib/acts_as_span/span_klass.rb +31 -0
- data/lib/acts_as_span/span_klass/overlap.rb +21 -0
- data/lib/acts_as_span/span_klass/status.rb +29 -0
- data/lib/acts_as_span/version.rb +12 -0
- data/spec/lib/acts_as_span_spec.rb +64 -0
- data/spec/lib/delegation_spec.rb +128 -0
- data/spec/lib/negative_spec.rb +30 -0
- data/spec/lib/span_instance/named_scopes_on_spec.rb +304 -0
- data/spec/lib/span_instance/named_scopes_spec.rb +302 -0
- data/spec/lib/span_instance/overlap_spec.rb +597 -0
- data/spec/lib/span_instance/status_spec.rb +49 -0
- data/spec/lib/span_instance/validations_spec.rb +54 -0
- data/spec/lib/span_instance_spec.rb +41 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +112 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
acts_as_span (0.0.5)
|
5
|
+
activerecord (>= 3)
|
6
|
+
activesupport
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (3.0.10)
|
12
|
+
activesupport (= 3.0.10)
|
13
|
+
builder (~> 2.1.2)
|
14
|
+
i18n (~> 0.5.0)
|
15
|
+
activerecord (3.0.10)
|
16
|
+
activemodel (= 3.0.10)
|
17
|
+
activesupport (= 3.0.10)
|
18
|
+
arel (~> 2.0.10)
|
19
|
+
tzinfo (~> 0.3.23)
|
20
|
+
activesupport (3.0.10)
|
21
|
+
acts_as_fu (0.0.7.2)
|
22
|
+
activerecord
|
23
|
+
sqlite3-ruby
|
24
|
+
arel (2.0.10)
|
25
|
+
builder (2.1.2)
|
26
|
+
diff-lcs (1.1.2)
|
27
|
+
i18n (0.5.0)
|
28
|
+
rspec (2.6.0)
|
29
|
+
rspec-core (~> 2.6.0)
|
30
|
+
rspec-expectations (~> 2.6.0)
|
31
|
+
rspec-mocks (~> 2.6.0)
|
32
|
+
rspec-core (2.6.4)
|
33
|
+
rspec-expectations (2.6.0)
|
34
|
+
diff-lcs (~> 1.1.2)
|
35
|
+
rspec-mocks (2.6.0)
|
36
|
+
sqlite3 (1.3.4)
|
37
|
+
sqlite3-ruby (1.3.3)
|
38
|
+
sqlite3 (>= 1.3.3)
|
39
|
+
tzinfo (0.3.29)
|
40
|
+
|
41
|
+
PLATFORMS
|
42
|
+
ruby
|
43
|
+
|
44
|
+
DEPENDENCIES
|
45
|
+
acts_as_fu
|
46
|
+
acts_as_span!
|
47
|
+
rspec
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011 Annkissam
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
= acts_as_span
|
2
|
+
|
3
|
+
ActiveRecord model w/ a start_date and an end_date
|
4
|
+
|
5
|
+
== Getting Started
|
6
|
+
|
7
|
+
In your Gemfile:
|
8
|
+
|
9
|
+
gem "acts_as_span"
|
10
|
+
|
11
|
+
In your model:
|
12
|
+
|
13
|
+
class SpanRecord < ActiveRecord::Base
|
14
|
+
acts_as_span
|
15
|
+
end
|
16
|
+
|
17
|
+
In your migrations:
|
18
|
+
|
19
|
+
class AddSpanToSpanRecord < ActiveRecord::Migration
|
20
|
+
def self.up
|
21
|
+
add_column :span_records, :start_date, :date
|
22
|
+
add_column :span_records, :end_date, :date
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.down
|
26
|
+
remove_column :span_records, :start_date
|
27
|
+
remove_column :span_records, :end_date
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
== Copyright
|
32
|
+
|
33
|
+
Copyright (c) 2011 Annkissam. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "acts_as_span/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "acts_as_span"
|
7
|
+
s.version = ActsAsSpan::VERSION::STRING
|
8
|
+
s.authors = ["Eric Sullivan"]
|
9
|
+
s.email = ["eric.sullivan@annkissam.com"]
|
10
|
+
s.homepage = "https://github.com/annkissam/acts_as_span"
|
11
|
+
s.summary = ActsAsSpan::VERSION::SUMMARY
|
12
|
+
s.description = %q{start_date and end_date as a span w/ ActiveRecord}
|
13
|
+
|
14
|
+
s.rubyforge_project = "acts_as_span"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency(%q<activerecord>, [">= 3"])
|
22
|
+
s.add_dependency('activesupport')
|
23
|
+
end
|
data/lib/acts_as_span.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
ACTS_AS_SPAN_PATH = File.dirname(__FILE__) + "/acts_as_span/"
|
6
|
+
|
7
|
+
require ACTS_AS_SPAN_PATH + 'version'
|
8
|
+
require ACTS_AS_SPAN_PATH + 'span_klass'
|
9
|
+
require ACTS_AS_SPAN_PATH + 'span_instance'
|
10
|
+
|
11
|
+
module ActsAsSpan
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def options
|
16
|
+
@options ||= {
|
17
|
+
:start_date_field => :start_date,
|
18
|
+
:end_date_field => :end_date,
|
19
|
+
:start_date_field_required => false,
|
20
|
+
:end_date_field_required => false,
|
21
|
+
:span_overlap_scope => nil,
|
22
|
+
:span_overlap_count => nil,
|
23
|
+
:name => :default
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def configure
|
28
|
+
yield(self) if block_given?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#by default, all model classess & their instances will return false w/ acts_as_span?
|
33
|
+
included do
|
34
|
+
self.send(:extend, ActsAsSpan::NegativeMethods)
|
35
|
+
self.send(:include, ActsAsSpan::NegativeMethods)
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
def acts_as_span(*args)
|
40
|
+
#this model & its instances will return true w/ acts_as_span?
|
41
|
+
self.send(:extend, ActsAsSpan::PositiveMethods)
|
42
|
+
self.send(:include, ActsAsSpan::PositiveMethods)
|
43
|
+
|
44
|
+
self.send(:extend, Forwardable)
|
45
|
+
|
46
|
+
self.send(:extend, ActsAsSpan::ExtendedClassMethods)
|
47
|
+
self.send(:include, ActsAsSpan::IncludedInstanceMethods)
|
48
|
+
|
49
|
+
options = OpenStruct.new(args.last.is_a?(Hash) ? ActsAsSpan.options.merge(args.pop) : ActsAsSpan.options)
|
50
|
+
|
51
|
+
#if span_overlap_scope is specified the span_overlap_count defaults to 0
|
52
|
+
options.span_overlap_count ||= 0 if options.span_overlap_scope
|
53
|
+
|
54
|
+
acts_as_span_definitions[options.name] = options
|
55
|
+
|
56
|
+
def_delegators :span, :close!,
|
57
|
+
:close_on!,
|
58
|
+
:span_status,
|
59
|
+
:span_status_on,
|
60
|
+
:span_status_to_s,
|
61
|
+
:span_status_to_s_on,
|
62
|
+
:current?,
|
63
|
+
:current_on?,
|
64
|
+
:future?,
|
65
|
+
:future_on?,
|
66
|
+
:expired?,
|
67
|
+
:expired_on?
|
68
|
+
|
69
|
+
def_delegators 'self.class', :acts_as_span_definitions
|
70
|
+
|
71
|
+
class << self
|
72
|
+
self.send(:extend, Forwardable)
|
73
|
+
|
74
|
+
def_delegators :span, :current,
|
75
|
+
:current_on,
|
76
|
+
:future,
|
77
|
+
:future_on,
|
78
|
+
:expired,
|
79
|
+
:expired_on
|
80
|
+
end
|
81
|
+
|
82
|
+
validate :validate_spans
|
83
|
+
end
|
84
|
+
|
85
|
+
def acts_as_span_definitions
|
86
|
+
@_acts_as_span_definitions ||= {}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module ExtendedClassMethods
|
91
|
+
def overlap(test_record)
|
92
|
+
overlap_for(test_record, :default, :default)
|
93
|
+
end
|
94
|
+
|
95
|
+
def overlap_for(test_record, test_record_span_name = :default, this_span_name = :default)
|
96
|
+
span_for(this_span_name).overlap(test_record.span_for(test_record_span_name))
|
97
|
+
end
|
98
|
+
|
99
|
+
def spans
|
100
|
+
acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def span
|
104
|
+
span_for(:default)
|
105
|
+
end
|
106
|
+
|
107
|
+
def span_for(name = :default)
|
108
|
+
acts_as_span_klasses[name] ||= SpanKlass.new(name, self, acts_as_span_definitions[name])
|
109
|
+
end
|
110
|
+
|
111
|
+
def acts_as_span_klasses
|
112
|
+
@_acts_as_span_klasses ||= {}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module IncludedInstanceMethods
|
117
|
+
def spans
|
118
|
+
acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def span
|
122
|
+
span_for(:default)
|
123
|
+
end
|
124
|
+
|
125
|
+
def span_for(name = :default)
|
126
|
+
acts_as_span_instances[name] ||= SpanInstance.new(name, self, acts_as_span_definitions[name])
|
127
|
+
end
|
128
|
+
|
129
|
+
def acts_as_span_instances
|
130
|
+
@_acts_as_span_instances ||= {}
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_spans
|
134
|
+
spans.each(&:validate)
|
135
|
+
end
|
136
|
+
|
137
|
+
#This syntax assumes :default span
|
138
|
+
def overlap?(other_record)
|
139
|
+
overlap_for?(other_record, :default, :default)
|
140
|
+
end
|
141
|
+
|
142
|
+
#record.span_for(:this_span_name).overlap?(other_record.span_for(:other_record_span_name))
|
143
|
+
def overlap_for?(other_record, this_span_name = :default, other_record_span_name = :default)
|
144
|
+
span_for(this_span_name).overlap?(other_record.span_for(other_record_span_name))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
module PositiveMethods
|
149
|
+
send(:define_method, "acts_as_span?") do
|
150
|
+
true
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
module NegativeMethods
|
155
|
+
send(:define_method, "acts_as_span?") do
|
156
|
+
false
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
ActiveRecord::Base.send(:include, ActsAsSpan)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
require ACTS_AS_SPAN_PATH + 'span_instance/validations'
|
4
|
+
require ACTS_AS_SPAN_PATH + 'span_instance/status'
|
5
|
+
require ACTS_AS_SPAN_PATH + 'span_instance/overlap'
|
6
|
+
|
7
|
+
module ActsAsSpan
|
8
|
+
class SpanInstance
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
include ActsAsSpan::SpanInstance::Validations
|
12
|
+
include ActsAsSpan::SpanInstance::Status
|
13
|
+
include ActsAsSpan::SpanInstance::Overlap
|
14
|
+
|
15
|
+
def_delegators :@acts_as_span_definition, :start_date_field,
|
16
|
+
:end_date_field,
|
17
|
+
:start_date_field_required,
|
18
|
+
:end_date_field_required,
|
19
|
+
:exclude_end,
|
20
|
+
:span_overlap_scope,
|
21
|
+
:span_overlap_count
|
22
|
+
|
23
|
+
def_delegators :span_model, :new_record?
|
24
|
+
|
25
|
+
attr_reader :name, :span_model, :acts_as_span_definition
|
26
|
+
|
27
|
+
def initialize(name, span_model, acts_as_span_definition)
|
28
|
+
@name = name
|
29
|
+
@span_model = span_model
|
30
|
+
@acts_as_span_definition = acts_as_span_definition
|
31
|
+
end
|
32
|
+
|
33
|
+
def span_klass
|
34
|
+
@span_klass ||= span_model.class
|
35
|
+
end
|
36
|
+
|
37
|
+
def start_date
|
38
|
+
span_model[start_date_field]
|
39
|
+
end
|
40
|
+
|
41
|
+
def end_date
|
42
|
+
span_model[end_date_field]
|
43
|
+
end
|
44
|
+
|
45
|
+
def close!(close_date = Date.today)
|
46
|
+
if end_date.blank?
|
47
|
+
span_model.update_attributes!(end_date_field => close_date)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
alias_method :close_on!, :close!
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module ActsAsSpan
|
4
|
+
class SpanInstance
|
5
|
+
module Overlap
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
#http://stackoverflow.com/questions/699448/ruby-how-do-you-check-whether-a-range-contains-a-subset-of-another-range
|
10
|
+
#start_date <= record_end_date && record_start_date <= end_date
|
11
|
+
def overlap?(other_span)
|
12
|
+
(start_date.nil? || other_span.end_date.nil? || start_date <= other_span.end_date) && (end_date.nil? || other_span.start_date.nil? || other_span.start_date <= end_date)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module ActsAsSpan
|
4
|
+
class SpanInstance
|
5
|
+
module Status
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
def span_status(query_date = Date.today)
|
10
|
+
if future?(query_date)
|
11
|
+
:future
|
12
|
+
elsif expired?(query_date)
|
13
|
+
:expired
|
14
|
+
elsif current?(query_date)
|
15
|
+
:current
|
16
|
+
else
|
17
|
+
:unknown
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method :span_status_on, :span_status
|
22
|
+
|
23
|
+
def span_status_to_s(query_date = Date.today)
|
24
|
+
case span_status(query_date)
|
25
|
+
when :future
|
26
|
+
"Future"
|
27
|
+
when :expired
|
28
|
+
"Expired"
|
29
|
+
when :current
|
30
|
+
"Current"
|
31
|
+
when :unknown
|
32
|
+
"Unknown"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
alias_method :span_status_to_s_on, :span_status_to_s
|
37
|
+
|
38
|
+
def current?(query_date = Date.today)
|
39
|
+
!future?(query_date) && !expired?(query_date)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :current_on?, :current?
|
43
|
+
|
44
|
+
def future?(query_date = Date.today)
|
45
|
+
start_date && start_date > query_date
|
46
|
+
end
|
47
|
+
|
48
|
+
alias_method :future_on?, :future?
|
49
|
+
|
50
|
+
def expired?(query_date = Date.today)
|
51
|
+
end_date && end_date < query_date
|
52
|
+
end
|
53
|
+
|
54
|
+
alias_method :expired_on?, :expired?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|