acts_as_span 0.0.5 → 0.0.6
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.
- 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
|