acts_as_span 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -3
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +0 -3
- data/README.rdoc +4 -16
- data/Rakefile +5 -1
- data/acts_as_span.gemspec +21 -5
- data/lib/acts_as_span.rb +55 -98
- data/lib/acts_as_span/no_overlap_validator.rb +51 -0
- data/lib/acts_as_span/span_instance.rb +17 -33
- data/lib/acts_as_span/span_instance/status.rb +12 -27
- data/lib/acts_as_span/span_instance/validations.rb +5 -53
- data/lib/acts_as_span/span_klass.rb +11 -19
- data/lib/acts_as_span/span_klass/status.rb +33 -12
- data/lib/acts_as_span/version.rb +2 -2
- data/lib/acts_as_span/within_parent_date_span_validator.rb +42 -0
- data/spec/lib/acts_as_span_spec.rb +18 -36
- data/spec/lib/delegation_spec.rb +45 -78
- data/spec/lib/no_overlap_validator_spec.rb +96 -0
- data/spec/lib/span_instance/named_scopes_on_spec.rb +193 -193
- data/spec/lib/span_instance/named_scopes_spec.rb +193 -191
- data/spec/lib/span_instance/overlap_spec.rb +193 -253
- data/spec/lib/span_instance/status_spec.rb +22 -35
- data/spec/lib/span_instance/validations_spec.rb +8 -44
- data/spec/lib/span_instance_spec.rb +5 -30
- data/spec/lib/within_parent_date_span_validator_spec.rb +115 -0
- data/spec/spec_helper.rb +19 -6
- data/spec/spec_models.rb +69 -0
- metadata +158 -58
- data/Gemfile.lock +0 -47
- data/lib/acts_as_span/span_instance/overlap.rb +0 -17
- data/lib/acts_as_span/span_klass/overlap.rb +0 -21
- data/spec/lib/negative_spec.rb +0 -30
- data/spec/spec.opts +0 -1
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9ed7f152ad93220467d76ca831763a2a4dbd02ba
|
4
|
+
data.tar.gz: eaf693675a0aa764b8d85a77464b9897787993be
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 686a48328384d0e29b0aba18af0d957cc06d0e8ab4fd051d72a33a8515c23115eca337427424f0e920edbf5970abb3605d12b582fa9df37799a0ed911624d8de
|
7
|
+
data.tar.gz: a35877b8ff3c5748a921fb095dd925791e1212246dc0442835a5e3e5c77cbdc30ec659a69d56cc55fbc0e4eee154f53babf7a70a26ee194674a9ad59e4cb0cdb
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
= acts_as_span
|
2
2
|
|
3
|
-
ActiveRecord model w/ a start_date and an end_date
|
3
|
+
ActiveRecord model w/ a start_date and an end_date == ActsAsSpan
|
4
|
+
|
5
|
+
Treat those date spans like the objects they are!
|
4
6
|
|
5
7
|
== Getting Started
|
6
8
|
|
@@ -14,20 +16,6 @@ In your model:
|
|
14
16
|
acts_as_span
|
15
17
|
end
|
16
18
|
|
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
19
|
== Copyright
|
32
20
|
|
33
|
-
Copyright (c) 2011 Annkissam. See LICENSE for details.
|
21
|
+
Copyright (c) 2011-2018 Annkissam. See LICENSE for details.
|
data/Rakefile
CHANGED
data/acts_as_span.gemspec
CHANGED
@@ -9,15 +9,31 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.email = ["eric.sullivan@annkissam.com"]
|
10
10
|
s.homepage = "https://github.com/annkissam/acts_as_span"
|
11
11
|
s.summary = ActsAsSpan::VERSION::SUMMARY
|
12
|
-
s.description = %q{start_date and end_date
|
12
|
+
s.description = %q{ActiveRecord model w/ a start_date and an end_date == ActsAsSpan}
|
13
|
+
s.license = "MIT"
|
13
14
|
|
14
|
-
|
15
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
16
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
17
|
+
if s.respond_to?(:metadata)
|
18
|
+
s.metadata["allowed_push_host"] = "https://rubygems.org"
|
19
|
+
else
|
20
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
21
|
+
"public gem pushes."
|
22
|
+
end
|
15
23
|
|
16
24
|
s.files = `git ls-files`.split("\n")
|
17
25
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
26
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
27
|
s.require_paths = ["lib"]
|
20
|
-
|
21
|
-
s.
|
22
|
-
s.
|
28
|
+
|
29
|
+
s.add_development_dependency "bundler", "~> 1.15"
|
30
|
+
s.add_development_dependency "rake", "~> 10.0"
|
31
|
+
s.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
s.add_development_dependency "sqlite3"
|
33
|
+
s.add_development_dependency "has_siblings", "~> 0.2.7"
|
34
|
+
s.add_development_dependency "temping"
|
35
|
+
s.add_development_dependency "pry-byebug"
|
36
|
+
|
37
|
+
s.add_runtime_dependency('activerecord', '>= 4.2.0')
|
38
|
+
s.add_runtime_dependency('activesupport', '>= 4.2.0')
|
23
39
|
end
|
data/lib/acts_as_span.rb
CHANGED
@@ -1,161 +1,118 @@
|
|
1
|
-
require 'active_support'
|
2
1
|
require 'ostruct'
|
3
|
-
require '
|
2
|
+
require 'acts_as_span/version'
|
3
|
+
require 'acts_as_span/span_klass'
|
4
|
+
require 'acts_as_span/span_instance'
|
5
|
+
|
6
|
+
require 'acts_as_span/no_overlap_validator'
|
7
|
+
require 'acts_as_span/within_parent_date_span_validator'
|
4
8
|
|
5
|
-
ACTS_AS_SPAN_PATH = File.dirname(__FILE__) + "/acts_as_span/"
|
6
9
|
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require ACTS_AS_SPAN_PATH + 'span_instance'
|
10
|
+
require 'active_support'
|
11
|
+
require 'active_record'
|
10
12
|
|
11
13
|
module ActsAsSpan
|
12
14
|
extend ActiveSupport::Concern
|
13
|
-
|
15
|
+
|
14
16
|
class << self
|
15
17
|
def options
|
16
18
|
@options ||= {
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:start_date_field_required => false,
|
20
|
-
:end_date_field_required => false,
|
21
|
-
:span_overlap_scope => nil,
|
22
|
-
:span_overlap_count => nil,
|
19
|
+
:start_field => :start_date,
|
20
|
+
:end_field => :end_date,
|
23
21
|
:name => :default
|
24
22
|
}
|
25
23
|
end
|
26
|
-
|
24
|
+
|
27
25
|
def configure
|
28
26
|
yield(self) if block_given?
|
29
27
|
end
|
30
28
|
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
|
-
|
29
|
+
|
38
30
|
module ClassMethods
|
39
31
|
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
32
|
self.send(:extend, ActsAsSpan::ExtendedClassMethods)
|
47
33
|
self.send(:include, ActsAsSpan::IncludedInstanceMethods)
|
48
|
-
|
34
|
+
|
49
35
|
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
|
-
|
36
|
+
|
54
37
|
acts_as_span_definitions[options.name] = options
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
38
|
+
|
39
|
+
# TODO add tests that check delegation of all methos in span
|
40
|
+
delegate :span_status,
|
41
|
+
:span_status_on,
|
42
|
+
:current?,
|
43
|
+
:current_on?,
|
44
|
+
:future?,
|
45
|
+
:future_on?,
|
46
|
+
:expired?,
|
47
|
+
:expired_on?,
|
48
|
+
:past?,
|
49
|
+
:past_on?, to: :span
|
50
|
+
|
51
|
+
delegate :acts_as_span_definitions, to: :class
|
52
|
+
|
53
|
+
# TODO idem above
|
71
54
|
class << self
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
55
|
+
delegate :current,
|
56
|
+
:current_on,
|
57
|
+
:future,
|
58
|
+
:future_on,
|
59
|
+
:expired,
|
60
|
+
:expired_on,
|
61
|
+
:past_on,
|
62
|
+
:past,
|
63
|
+
:current_or_future_on,
|
64
|
+
:current_or_future, to: :span
|
80
65
|
end
|
81
|
-
|
66
|
+
|
82
67
|
validate :validate_spans
|
83
68
|
end
|
84
|
-
|
69
|
+
|
85
70
|
def acts_as_span_definitions
|
86
71
|
@_acts_as_span_definitions ||= {}
|
87
72
|
end
|
88
73
|
end
|
89
|
-
|
74
|
+
|
90
75
|
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
76
|
def spans
|
100
77
|
acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
|
101
78
|
end
|
102
|
-
|
79
|
+
|
103
80
|
def span
|
104
81
|
span_for(:default)
|
105
82
|
end
|
106
|
-
|
83
|
+
|
107
84
|
def span_for(name = :default)
|
108
85
|
acts_as_span_klasses[name] ||= SpanKlass.new(name, self, acts_as_span_definitions[name])
|
109
86
|
end
|
110
|
-
|
87
|
+
|
111
88
|
def acts_as_span_klasses
|
112
89
|
@_acts_as_span_klasses ||= {}
|
113
90
|
end
|
114
91
|
end
|
115
|
-
|
92
|
+
|
116
93
|
module IncludedInstanceMethods
|
117
94
|
def spans
|
118
95
|
acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
|
119
96
|
end
|
120
|
-
|
97
|
+
|
121
98
|
def span
|
122
99
|
span_for(:default)
|
123
100
|
end
|
124
|
-
|
101
|
+
|
125
102
|
def span_for(name = :default)
|
126
103
|
acts_as_span_instances[name] ||= SpanInstance.new(name, self, acts_as_span_definitions[name])
|
127
104
|
end
|
128
|
-
|
105
|
+
|
129
106
|
def acts_as_span_instances
|
130
107
|
@_acts_as_span_instances ||= {}
|
131
108
|
end
|
132
|
-
|
109
|
+
|
133
110
|
def validate_spans
|
134
111
|
spans.each(&:validate)
|
135
112
|
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
113
|
end
|
159
114
|
end
|
160
115
|
|
161
|
-
|
116
|
+
if Object.const_defined?("ActiveRecord")
|
117
|
+
ActiveRecord::Base.send(:include, ActsAsSpan)
|
118
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
|
3
|
+
module ActsAsSpan
|
4
|
+
class NoOverlapValidator < ActiveModel::Validator
|
5
|
+
def validate(record)
|
6
|
+
overlapping_records = temporally_overlapping_for(record)
|
7
|
+
instance_scope = options[:instance_scope].is_a?(Proc) ? record.instance_eval(&options[:instance_scope]) : true
|
8
|
+
|
9
|
+
if overlapping_records.any? && instance_scope
|
10
|
+
|
11
|
+
error_type = overlapping_records.size == 1 ? "no_overlap.one" : "no_overlap.other"
|
12
|
+
|
13
|
+
record.errors.add(
|
14
|
+
:base,
|
15
|
+
error_type.to_sym,
|
16
|
+
model_name: record.class.model_name.human,
|
17
|
+
model_name_plural: record.class.model_name.plural.humanize,
|
18
|
+
start_date: record.start_date,
|
19
|
+
end_date: record.end_date,
|
20
|
+
count: overlapping_records.size,
|
21
|
+
overlapping_records_s: overlapping_records.join(",")
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#TODO add back condition for start_date nil
|
27
|
+
#TODO add configuration for span configuration
|
28
|
+
def temporally_overlapping_for(record)
|
29
|
+
scope = record.instance_eval(&options[:scope])
|
30
|
+
|
31
|
+
start_date = record.start_date || Date.current
|
32
|
+
end_date = record.end_date
|
33
|
+
arel_table = record.class.arel_table
|
34
|
+
|
35
|
+
if end_date
|
36
|
+
scope.where(
|
37
|
+
arel_table[:start_date].lteq(end_date).
|
38
|
+
and(
|
39
|
+
arel_table[:end_date].gteq(start_date).
|
40
|
+
or(arel_table[:end_date].eq(nil))
|
41
|
+
)
|
42
|
+
)
|
43
|
+
else
|
44
|
+
scope.where(
|
45
|
+
arel_table[:end_date].gteq(start_date).
|
46
|
+
or(arel_table[:end_date].eq(nil))
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,53 +1,37 @@
|
|
1
|
-
require '
|
1
|
+
require 'acts_as_span/span_instance/validations'
|
2
|
+
require 'acts_as_span/span_instance/status'
|
2
3
|
|
3
|
-
require
|
4
|
-
require ACTS_AS_SPAN_PATH + 'span_instance/status'
|
5
|
-
require ACTS_AS_SPAN_PATH + 'span_instance/overlap'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
6
5
|
|
7
6
|
module ActsAsSpan
|
8
7
|
class SpanInstance
|
9
|
-
extend Forwardable
|
10
|
-
|
11
8
|
include ActsAsSpan::SpanInstance::Validations
|
12
9
|
include ActsAsSpan::SpanInstance::Status
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
:span_overlap_scope,
|
21
|
-
:span_overlap_count
|
22
|
-
|
23
|
-
def_delegators :span_model, :new_record?
|
24
|
-
|
10
|
+
|
11
|
+
delegate :start_field,
|
12
|
+
:end_field,
|
13
|
+
:exclude_end, to: :@acts_as_span_definition
|
14
|
+
|
15
|
+
delegate :new_record?, to: :span_model
|
16
|
+
|
25
17
|
attr_reader :name, :span_model, :acts_as_span_definition
|
26
|
-
|
18
|
+
|
27
19
|
def initialize(name, span_model, acts_as_span_definition)
|
28
20
|
@name = name
|
29
21
|
@span_model = span_model
|
30
22
|
@acts_as_span_definition = acts_as_span_definition
|
31
23
|
end
|
32
|
-
|
24
|
+
|
33
25
|
def span_klass
|
34
26
|
@span_klass ||= span_model.class
|
35
27
|
end
|
36
|
-
|
28
|
+
|
37
29
|
def start_date
|
38
|
-
span_model[
|
30
|
+
span_model[start_field]
|
39
31
|
end
|
40
|
-
|
32
|
+
|
41
33
|
def end_date
|
42
|
-
span_model[
|
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
|
34
|
+
span_model[end_field]
|
49
35
|
end
|
50
|
-
|
51
|
-
alias_method :close_on!, :close!
|
52
36
|
end
|
53
|
-
end
|
37
|
+
end
|