acts_as_span 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/.tool-versions +1 -0
- data/acts_as_span.gemspec +1 -1
- data/config/locales/en/acts_as_span.yml +15 -0
- data/lib/acts_as_span.rb +3 -0
- data/lib/acts_as_span/end_date_propagator.rb +190 -0
- data/lib/acts_as_span/no_overlap_validator.rb +17 -15
- data/lib/acts_as_span/span_instance/validations.rb +7 -1
- data/lib/acts_as_span/version.rb +1 -1
- data/lib/acts_as_span/within_parent_date_span_validator.rb +6 -6
- data/spec/lib/end_date_propagator_spec.rb +306 -0
- data/spec/lib/no_overlap_validator_spec.rb +1 -1
- data/spec/spec_models.rb +113 -0
- metadata +24 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57404b0d110113b5f9c52537aa90970a560d69f4a5b7d5d318caacc229157679
|
|
4
|
+
data.tar.gz: 5702fafe912d1bc56b633aec32dd2ae76da9f886608c182eed29c0ca7d2ed509
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1e7265d3cfc7433b331f407fe991eb5549091e10c5dcef0298468099f1769dfaca71a7355363af8f6f88c381362d518fa494366d3f1dead8de8072458d95b2c
|
|
7
|
+
data.tar.gz: 6304cb8600c553e60bc7e470ed4f87e6cc145a4239243f5109556278f76d88b24d1624bbc904e499d7ce95d7196967d4d8822c962571c9184c875dbd20d6d57f
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 2.6.3
|
data/acts_as_span.gemspec
CHANGED
|
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
|
|
|
29
29
|
s.add_development_dependency "bundler", "~> 2.0.1"
|
|
30
30
|
s.add_development_dependency "rake", "~> 10.0"
|
|
31
31
|
s.add_development_dependency "rspec", "~> 3.0"
|
|
32
|
-
s.add_development_dependency "sqlite3", "~> 1.
|
|
32
|
+
s.add_development_dependency "sqlite3", "~> 1.4"
|
|
33
33
|
s.add_development_dependency "has_siblings", "~> 0.2.7"
|
|
34
34
|
s.add_development_dependency "temping"
|
|
35
35
|
s.add_development_dependency "pry-byebug"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
en:
|
|
2
|
+
activerecord:
|
|
3
|
+
errors:
|
|
4
|
+
messages:
|
|
5
|
+
end_date_propagator:
|
|
6
|
+
propagation_failure: "%{parent} could not propagate
|
|
7
|
+
%{end_date_field_name} to %{child}:\n%{reason}"
|
|
8
|
+
# TODO: let pluralize handle pluralization
|
|
9
|
+
no_overlap:
|
|
10
|
+
one: "A %{model_name} already exists between
|
|
11
|
+
%{start_date} - %{end_date}: %{overlapping_records_s}"
|
|
12
|
+
other: "%{count} %{model_name_plural} already exist between
|
|
13
|
+
%{start_date} - %{end_date}: %{overlapping_records_s}"
|
|
14
|
+
not_within_parent_date_span: Must exist within the %{parent} date span
|
|
15
|
+
start_date_after_end_date: Must be on or after %{start_field}
|
data/lib/acts_as_span.rb
CHANGED
|
@@ -6,10 +6,13 @@ require 'acts_as_span/span_instance'
|
|
|
6
6
|
require 'acts_as_span/no_overlap_validator'
|
|
7
7
|
require 'acts_as_span/within_parent_date_span_validator'
|
|
8
8
|
|
|
9
|
+
require 'acts_as_span/end_date_propagator'
|
|
9
10
|
|
|
10
11
|
require 'active_support'
|
|
11
12
|
require 'active_record'
|
|
12
13
|
|
|
14
|
+
I18n.load_path += Dir[File.join(File.dirname(__dir__), 'config', 'locales', '**', 'acts_as_span.yml')]
|
|
15
|
+
|
|
13
16
|
module ActsAsSpan
|
|
14
17
|
extend ActiveSupport::Concern
|
|
15
18
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_Literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsSpan
|
|
4
|
+
# # End Date Propagator
|
|
5
|
+
#
|
|
6
|
+
# When editing the `end_date` of a record, the record's children often also
|
|
7
|
+
# need to be updated. This propagator takes care of that.
|
|
8
|
+
# For each of the child records (defined below in the function `children`),
|
|
9
|
+
# the child record's `end_date` is updated to match that of the original
|
|
10
|
+
# object. The function `propagate` is recursive, propagating to
|
|
11
|
+
# children of children and so on.
|
|
12
|
+
# Records that should not have their end dates propagated in this manner
|
|
13
|
+
# (e.g. StatusRecords) are manually excluded in `skipped_classes`.
|
|
14
|
+
# If there is some error preventing propagation, the child record is NOT saved
|
|
15
|
+
# and that error message is added to the object's `errors`. These errors
|
|
16
|
+
# propagate upwards into a flattened array of error messages.
|
|
17
|
+
#
|
|
18
|
+
# This class uses its own definition of 'child' for an object. For a given
|
|
19
|
+
# object, the objects the propagator considers its children are:
|
|
20
|
+
# * Associated via `has_many` association
|
|
21
|
+
# * Association `:dependent` option is `:delete` or `:destroy`
|
|
22
|
+
# * acts_as_span (checked via `respond_to?(:span)`)
|
|
23
|
+
# * Not blacklisted via `skipped_classes` array
|
|
24
|
+
#
|
|
25
|
+
# The return value for `call` is the given object, updated to have children's
|
|
26
|
+
# errors added to its `:base` errors if any children had errors.
|
|
27
|
+
#
|
|
28
|
+
# ## Usage:
|
|
29
|
+
#
|
|
30
|
+
# Propagate end dates for an object that acts_as_span and has propagatable
|
|
31
|
+
# children to all propagatable children:
|
|
32
|
+
# ```
|
|
33
|
+
# ActsAsSpan::EndDatePropagator.call(object)
|
|
34
|
+
# ```
|
|
35
|
+
#
|
|
36
|
+
# To propagate to a subset of its propagatable children:
|
|
37
|
+
# ```
|
|
38
|
+
# ActsAsSpan::EndDatePropagator.call(
|
|
39
|
+
# object, skipped_classes: [ClassOne, ClassTwo]
|
|
40
|
+
# )
|
|
41
|
+
# ```
|
|
42
|
+
# ... where ClassOne and ClassTwo are the classes to be excluded.
|
|
43
|
+
#
|
|
44
|
+
# The EndDatePropagator does not use transactions. If the propagation should
|
|
45
|
+
# be run in a transaction, wrap the call in one like so:
|
|
46
|
+
# ```
|
|
47
|
+
# ActiveRecord::Base.transaction do
|
|
48
|
+
# ActsAsSpan::EndDatePropagator.call(
|
|
49
|
+
# obj, skipped_classes: [ClassOne, ClassTwo]
|
|
50
|
+
# )
|
|
51
|
+
# end
|
|
52
|
+
# ```
|
|
53
|
+
#
|
|
54
|
+
# One use case for the transaction wrapper would be to not follow through
|
|
55
|
+
# with propagation if the object has errors:
|
|
56
|
+
# ```
|
|
57
|
+
# ActiveRecord::Base.transaction do
|
|
58
|
+
# result = ActsAsSpan::EndDatePropagator.call(obj)
|
|
59
|
+
# if result.errors.present?
|
|
60
|
+
# fail OhNoMyObjetHasErrorsError, "Oh, no! My object has errors!"
|
|
61
|
+
# end
|
|
62
|
+
# end
|
|
63
|
+
# ```
|
|
64
|
+
#
|
|
65
|
+
# Currently only propagates "default" span. The approach to implementing such
|
|
66
|
+
# a feature is ambiguous - would all children have the same span propagated?
|
|
67
|
+
# Would each acts_as_span model need a method to tell which span to
|
|
68
|
+
# propagate to? Once there is a solid use case for using this object on
|
|
69
|
+
# models with multiple spans, that will inform the implementation strategy.
|
|
70
|
+
class EndDatePropagator
|
|
71
|
+
attr_reader :object,
|
|
72
|
+
:errors_cache,
|
|
73
|
+
:skipped_classes
|
|
74
|
+
|
|
75
|
+
def initialize(object, errors_cache: [], skipped_classes: [])
|
|
76
|
+
@object = object
|
|
77
|
+
@errors_cache = errors_cache
|
|
78
|
+
@skipped_classes = skipped_classes
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# class-level call: enable the usage of ActsAsSpan::EndDatePropagator.call
|
|
82
|
+
def self.call(object, **opts)
|
|
83
|
+
new(object, opts).call
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def call
|
|
87
|
+
result = propagate
|
|
88
|
+
# only add new errors to the object
|
|
89
|
+
result.errors.each do |error, message|
|
|
90
|
+
unless object.errors[error].include? message
|
|
91
|
+
object.errors[error] << message
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
object
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def propagate
|
|
100
|
+
# return if there is nothing to propagate
|
|
101
|
+
return object unless should_propagate_from? object
|
|
102
|
+
|
|
103
|
+
children(object).each do |child|
|
|
104
|
+
# End the record, its children too. And their children, forever, true.
|
|
105
|
+
propagated_child = assign_end_date(child, object.span.end_date)
|
|
106
|
+
|
|
107
|
+
# save child and add errors to cache
|
|
108
|
+
save_with_errors(object, child, propagated_child)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# add just the strings, prevent ugly nested arrays in the view
|
|
112
|
+
object.errors[:base].push(*errors_cache.flatten)
|
|
113
|
+
|
|
114
|
+
# return the object, with any newly-added errors
|
|
115
|
+
object
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# returns the given child, but possibly with errors
|
|
119
|
+
def assign_end_date(child, new_end_date)
|
|
120
|
+
child.assign_attributes({ child.span.end_field => new_end_date })
|
|
121
|
+
ActsAsSpan::EndDatePropagator.call(
|
|
122
|
+
child, errors_cache: errors_cache, skipped_classes: skipped_classes
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# save the child record, add errors.
|
|
127
|
+
def save_with_errors(object, child, propagated_child)
|
|
128
|
+
if object_has_errors?(propagated_child)
|
|
129
|
+
errors_cache << propagation_error_message(object, child)
|
|
130
|
+
end
|
|
131
|
+
child.save
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def propagation_error_message(object, child)
|
|
135
|
+
I18n.t(
|
|
136
|
+
'propagation_failure',
|
|
137
|
+
scope: %i[activerecord errors messages end_date_propagator],
|
|
138
|
+
end_date_field_name: child.class.human_attribute_name(
|
|
139
|
+
child.span.end_field
|
|
140
|
+
),
|
|
141
|
+
parent: object.model_name.human,
|
|
142
|
+
child: child.model_name.human,
|
|
143
|
+
reason: child.errors.full_messages.join('; ')
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def object_has_errors?(object)
|
|
148
|
+
!object.valid? ||
|
|
149
|
+
(object.errors.present? && object.errors.messages.values.flatten.any?)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# check if the end_date analog is dirtied
|
|
153
|
+
def end_date_changed?(object)
|
|
154
|
+
end_date_field = object.span.end_field.to_s
|
|
155
|
+
object.changed.include? end_date_field
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def should_propagate_from?(object)
|
|
159
|
+
object.respond_to?(:span) &&
|
|
160
|
+
end_date_changed?(object) &&
|
|
161
|
+
!object.span.end_date.nil?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Use acts_as_span to determine whether a record has an end date
|
|
165
|
+
def should_propagate_to?(klass)
|
|
166
|
+
klass.respond_to?(:span) &&
|
|
167
|
+
@skipped_classes.exclude?(klass)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def child_associations(object)
|
|
171
|
+
object.class.reflect_on_all_associations(:has_many).select do |reflection|
|
|
172
|
+
%i[delete destroy].include?(reflection.options[:dependent]) &&
|
|
173
|
+
should_propagate_to?(reflection.klass)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def children(object)
|
|
178
|
+
child_objects = child_associations(object).flat_map do |reflection|
|
|
179
|
+
object.send(reflection.name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# skip previously-ended children
|
|
183
|
+
child_objects.reject do |child|
|
|
184
|
+
child.span.end_date && child.span.end_date < object.span.end_date
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
attr_writer :object, :errors_cache
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -4,19 +4,21 @@ module ActsAsSpan
|
|
|
4
4
|
class NoOverlapValidator < ActiveModel::Validator
|
|
5
5
|
def validate(record)
|
|
6
6
|
overlapping_records = temporally_overlapping_for(record)
|
|
7
|
-
instance_scope = options[:instance_scope].is_a?
|
|
7
|
+
instance_scope = if options[:instance_scope].is_a? Proc
|
|
8
|
+
record.instance_eval&options[:instance_scope]
|
|
9
|
+
else
|
|
10
|
+
true
|
|
11
|
+
end
|
|
8
12
|
|
|
9
13
|
if overlapping_records.any? && instance_scope
|
|
10
14
|
|
|
11
|
-
error_type = overlapping_records.size == 1 ? "no_overlap.one" : "no_overlap.other"
|
|
12
|
-
|
|
13
15
|
record.errors.add(
|
|
14
16
|
:base,
|
|
15
|
-
|
|
17
|
+
:no_overlap,
|
|
16
18
|
model_name: record.class.model_name.human,
|
|
17
19
|
model_name_plural: record.class.model_name.plural.humanize,
|
|
18
|
-
start_date: record.start_date,
|
|
19
|
-
end_date: record.end_date,
|
|
20
|
+
start_date: record.span.start_date,
|
|
21
|
+
end_date: record.span.end_date,
|
|
20
22
|
count: overlapping_records.size,
|
|
21
23
|
overlapping_records_s: overlapping_records.join(",")
|
|
22
24
|
)
|
|
@@ -24,26 +26,26 @@ module ActsAsSpan
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
#TODO add back condition for start_date nil
|
|
27
|
-
#TODO add
|
|
29
|
+
#TODO add support for multiple spans (currently only checks :default)
|
|
28
30
|
def temporally_overlapping_for(record)
|
|
29
31
|
scope = record.instance_eval(&options[:scope])
|
|
30
32
|
|
|
31
|
-
start_date = record.start_date || Date.current
|
|
32
|
-
end_date = record.end_date
|
|
33
|
+
start_date = record.span.start_date || Date.current
|
|
34
|
+
end_date = record.span.end_date
|
|
33
35
|
arel_table = record.class.arel_table
|
|
34
36
|
|
|
35
37
|
if end_date
|
|
36
38
|
scope.where(
|
|
37
|
-
arel_table[
|
|
38
|
-
and(
|
|
39
|
-
arel_table[
|
|
40
|
-
|
|
39
|
+
arel_table[record.span.start_field].lteq(end_date)
|
|
40
|
+
.and(
|
|
41
|
+
arel_table[record.span.end_field].gteq(start_date)
|
|
42
|
+
.or(arel_table[record.span.end_field].eq(nil))
|
|
41
43
|
)
|
|
42
44
|
)
|
|
43
45
|
else
|
|
44
46
|
scope.where(
|
|
45
|
-
arel_table[
|
|
46
|
-
or(arel_table[
|
|
47
|
+
arel_table[record.span.end_field].gteq(start_date)
|
|
48
|
+
.or(arel_table[record.span.end_field].eq(nil))
|
|
47
49
|
)
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -10,7 +10,13 @@ module ActsAsSpan
|
|
|
10
10
|
|
|
11
11
|
def validate_start_date_less_than_or_equal_to_end_date
|
|
12
12
|
if start_date && end_date && end_date < start_date
|
|
13
|
-
span_model.errors.add(
|
|
13
|
+
span_model.errors.add(
|
|
14
|
+
end_field,
|
|
15
|
+
:start_date_after_end_date,
|
|
16
|
+
start_field: span_model.class.human_attribute_name(
|
|
17
|
+
span_model.span.start_field
|
|
18
|
+
)
|
|
19
|
+
)
|
|
14
20
|
end
|
|
15
21
|
end
|
|
16
22
|
end
|
data/lib/acts_as_span/version.rb
CHANGED
|
@@ -22,21 +22,21 @@ module ActsAsSpan
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def child_record_started_before_parent_record(record, parent)
|
|
25
|
-
record.start_date.present? && parent.start_date.present? &&
|
|
26
|
-
record.start_date < parent.start_date
|
|
25
|
+
record.span.start_date.present? && parent.span.start_date.present? &&
|
|
26
|
+
record.span.start_date < parent.span.start_date
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def child_record_ended_after_parent_record(record, parent)
|
|
30
|
-
record.end_date.present? && parent.end_date.present? &&
|
|
31
|
-
record.end_date > parent.end_date
|
|
30
|
+
record.span.end_date.present? && parent.span.end_date.present? &&
|
|
31
|
+
record.span.end_date > parent.span.end_date
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def child_record_without_start_date(record, parent)
|
|
35
|
-
record.start_date.nil? && parent.start_date.present?
|
|
35
|
+
record.span.start_date.nil? && parent.span.start_date.present?
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def child_record_without_end_date(record, parent)
|
|
39
|
-
record.end_date.nil? && parent.end_date.present?
|
|
39
|
+
record.span.end_date.nil? && parent.span.end_date.present?
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe ActsAsSpan::EndDatePropagator do
|
|
4
|
+
let(:end_date_propagator) do
|
|
5
|
+
ActsAsSpan::EndDatePropagator.new(
|
|
6
|
+
base_instance, skipped_classes: skipped_classes
|
|
7
|
+
)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
let(:base_instance) do
|
|
11
|
+
Base.create(end_date: initial_end_date)
|
|
12
|
+
end
|
|
13
|
+
let(:initial_end_date) { nil }
|
|
14
|
+
|
|
15
|
+
let(:other_base_instance) do
|
|
16
|
+
OtherBase.create(end_date: other_end_date)
|
|
17
|
+
end
|
|
18
|
+
let(:other_end_date) { nil }
|
|
19
|
+
|
|
20
|
+
let(:skipped_classes) { [] }
|
|
21
|
+
|
|
22
|
+
let!(:child_instance) do
|
|
23
|
+
Child.create(
|
|
24
|
+
base: base_instance,
|
|
25
|
+
emancipation_date: child_end_date
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
let(:child_end_date) { nil }
|
|
29
|
+
|
|
30
|
+
let!(:dog_instance) do
|
|
31
|
+
Dog.create(
|
|
32
|
+
base: base_instance,
|
|
33
|
+
end_date: dog_end_date
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
let(:dog_end_date) { nil }
|
|
37
|
+
|
|
38
|
+
let!(:bird_instance) do
|
|
39
|
+
Bird.create(
|
|
40
|
+
child: child_instance,
|
|
41
|
+
end_date: child_end_date
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
let(:bird_end_date) { nil }
|
|
45
|
+
|
|
46
|
+
let(:tale_instance) do
|
|
47
|
+
base_instance.tales.create(start_date: Date.current, end_date: nil)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '@errors_cache' do
|
|
51
|
+
let(:base_start_date) { Date.current - 7 }
|
|
52
|
+
let(:initial_end_date) { nil }
|
|
53
|
+
let(:end_date) { Date.current }
|
|
54
|
+
|
|
55
|
+
let(:child_start_date) { base_start_date + 1 }
|
|
56
|
+
let!(:child_instance) do
|
|
57
|
+
Child.create(
|
|
58
|
+
base: base_instance,
|
|
59
|
+
date_of_birth: child_start_date,
|
|
60
|
+
emancipation_date: child_end_date
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
let(:bird_start_date) { child_start_date + 1 }
|
|
64
|
+
let!(:bird_instance) do
|
|
65
|
+
Bird.create(
|
|
66
|
+
child: child_instance,
|
|
67
|
+
start_date: bird_start_date,
|
|
68
|
+
end_date: bird_end_date
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
before do
|
|
73
|
+
base_instance.start_date = base_start_date
|
|
74
|
+
base_instance.save!
|
|
75
|
+
base_instance.end_date = end_date
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context 'when all child records are successfully saved' do
|
|
79
|
+
it 'the parent record does not have any errors' do
|
|
80
|
+
expect(
|
|
81
|
+
end_date_propagator.call.errors.full_messages
|
|
82
|
+
).to be_empty
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
context 'when one grandchild record is not valid' do
|
|
87
|
+
before do
|
|
88
|
+
bird_instance.start_date = child_start_date - 1
|
|
89
|
+
bird_instance.save(validate: false)
|
|
90
|
+
end
|
|
91
|
+
it "the parent shows that grandchild's errors" do
|
|
92
|
+
expect(
|
|
93
|
+
end_date_propagator.call.errors.values.join
|
|
94
|
+
).to include(
|
|
95
|
+
I18n.t(
|
|
96
|
+
'not_within_parent_date_span',
|
|
97
|
+
parent: 'Child',
|
|
98
|
+
scope: %i[activerecord errors messages]
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context 'when multiple child records are not valid' do
|
|
105
|
+
before do
|
|
106
|
+
child_instance.date_of_birth = base_instance.span.start_date - 1
|
|
107
|
+
child_instance.save(validate: false)
|
|
108
|
+
bird_instance.start_date = child_instance.span.start_date - 1
|
|
109
|
+
bird_instance.save(validate: false)
|
|
110
|
+
end
|
|
111
|
+
it "the parent gains all children's errors" do
|
|
112
|
+
expect(
|
|
113
|
+
end_date_propagator.call.errors.values.join
|
|
114
|
+
).to include(
|
|
115
|
+
I18n.t(
|
|
116
|
+
'not_within_parent_date_span',
|
|
117
|
+
parent: 'Child',
|
|
118
|
+
scope: %i[activerecord errors messages]
|
|
119
|
+
)
|
|
120
|
+
).and include(
|
|
121
|
+
I18n.t(
|
|
122
|
+
'not_within_parent_date_span',
|
|
123
|
+
parent: 'Base',
|
|
124
|
+
scope: %i[activerecord errors messages]
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe '.call' do
|
|
132
|
+
subject(:result) do
|
|
133
|
+
ActsAsSpan::EndDatePropagator.call(obj, call_options)
|
|
134
|
+
end
|
|
135
|
+
let(:obj) { base_instance }
|
|
136
|
+
|
|
137
|
+
context 'when no skipped classes are passed' do
|
|
138
|
+
let(:call_options) { {} }
|
|
139
|
+
|
|
140
|
+
it 'forwards the correct arguments to :new' do
|
|
141
|
+
expect(ActsAsSpan::EndDatePropagator)
|
|
142
|
+
.to receive(:new).with(obj, call_options).and_call_original
|
|
143
|
+
expect(result).to eq(obj)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
context 'when skipped classes are passed' do
|
|
148
|
+
let(:call_options) { { skipped_classes: ['bungus'] } }
|
|
149
|
+
|
|
150
|
+
it 'forwards the correct arguments to :new' do
|
|
151
|
+
expect(ActsAsSpan::EndDatePropagator)
|
|
152
|
+
.to receive(:new).with(obj, call_options).and_call_original
|
|
153
|
+
expect(result).to eq(obj)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe '#call' do
|
|
159
|
+
context 'without an end_date' do
|
|
160
|
+
let(:object_instance) { SpannableModel.new }
|
|
161
|
+
|
|
162
|
+
it 'does not raise an error' do
|
|
163
|
+
expect do
|
|
164
|
+
ActsAsSpan::EndDatePropagator.new(object_instance).call
|
|
165
|
+
end.not_to raise_error
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
context 'updates children' do
|
|
170
|
+
before do
|
|
171
|
+
base_instance
|
|
172
|
+
base_instance.end_date = end_date
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
context 'base_instance.end_date nil -> !nil' do
|
|
176
|
+
let(:initial_end_date) { nil }
|
|
177
|
+
let(:end_date) { Date.current }
|
|
178
|
+
|
|
179
|
+
context 'child_end_date == initial_end_date' do
|
|
180
|
+
let(:child_end_date) { initial_end_date }
|
|
181
|
+
|
|
182
|
+
it 'propagates to the child_instance' do
|
|
183
|
+
expect{ end_date_propagator.call }.to change{
|
|
184
|
+
child_instance.reload.emancipation_date }
|
|
185
|
+
.from(child_end_date).to(base_instance.end_date)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
context 'child_end_date >= initial_end_date' do
|
|
190
|
+
let(:child_end_date) { end_date + 3 }
|
|
191
|
+
|
|
192
|
+
it 'propagates to the child_instance' do
|
|
193
|
+
expect{ end_date_propagator.call }.to change{
|
|
194
|
+
child_instance.reload.emancipation_date}
|
|
195
|
+
.from(child_end_date).to(base_instance.end_date)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
context 'child_end_date <= initial_end_date' do
|
|
200
|
+
let(:child_end_date) { end_date - 3 }
|
|
201
|
+
|
|
202
|
+
it 'does not propagate to the child_instance' do
|
|
203
|
+
expect{ end_date_propagator.call }.not_to change{
|
|
204
|
+
child_instance.reload.emancipation_date}
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
context 'when a child cannot have its end date updated' do
|
|
209
|
+
before do
|
|
210
|
+
# add a "within parent date span" error to child
|
|
211
|
+
base_instance.start_date = Date.current - 1
|
|
212
|
+
child_instance.date_of_birth = Date.current - 2
|
|
213
|
+
child_instance.save(validate: false)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "the parent's end date is not updated" do
|
|
217
|
+
expect{ end_date_propagator.call }.to change{
|
|
218
|
+
base_instance.errors[:base]
|
|
219
|
+
}.from([])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
context 'and the child is the child of a child' do
|
|
223
|
+
before do
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "the parent's end date is not updated" do
|
|
227
|
+
expect{ end_date_propagator.call }.not_to change{
|
|
228
|
+
base_instance.reload.end_date
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
context 'base_instance.end_date !nil -> nil' do
|
|
236
|
+
let(:initial_end_date) { Date.current }
|
|
237
|
+
let(:end_date) { nil }
|
|
238
|
+
let(:child_end_date) { initial_end_date }
|
|
239
|
+
|
|
240
|
+
it 'does not propagate to the child_instance' do
|
|
241
|
+
expect{ end_date_propagator.call }.not_to change{
|
|
242
|
+
child_instance.reload.emancipation_date }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
context 'base_instance.end_date not changed' do
|
|
247
|
+
let(:end_date) { initial_end_date }
|
|
248
|
+
|
|
249
|
+
it 'does not propagate to the child_instance' do
|
|
250
|
+
expect{ end_date_propagator.call }.not_to change{
|
|
251
|
+
child_instance.reload.emancipation_date }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
context 'has access to all children via has_many associations' do
|
|
256
|
+
let(:end_date) { Date.current }
|
|
257
|
+
|
|
258
|
+
it 'changes the end_date of all child associations' do
|
|
259
|
+
expect{ end_date_propagator.call }.to change{
|
|
260
|
+
child_instance.reload.emancipation_date }.
|
|
261
|
+
from(child_instance.emancipation_date).to(base_instance.end_date)
|
|
262
|
+
.and change{ dog_instance.reload.end_date }
|
|
263
|
+
.from(dog_instance.end_date).to(base_instance.end_date)
|
|
264
|
+
.and change{ bird_instance.reload.end_date }
|
|
265
|
+
.from(bird_instance.end_date).to(base_instance.end_date)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
context 'when child record does not have end_date to update' do
|
|
271
|
+
let!(:cat_owner_instance) do
|
|
272
|
+
CatOwner.create(end_date: initial_end_date)
|
|
273
|
+
end
|
|
274
|
+
let!(:cat_instance) do
|
|
275
|
+
Cat.create(cat_owner: cat_owner_instance)
|
|
276
|
+
end
|
|
277
|
+
let(:cat_end_date) { nil }
|
|
278
|
+
let(:end_date) { Date.current }
|
|
279
|
+
|
|
280
|
+
before do
|
|
281
|
+
cat_owner_instance.end_date = end_date
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it 'does not throw an error' do
|
|
285
|
+
expect(cat_instance).not_to respond_to(:end_date)
|
|
286
|
+
expect{ end_date_propagator.call }.not_to raise_error
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
context 'when a class is skipped' do
|
|
291
|
+
let(:end_date) { Date.current }
|
|
292
|
+
let(:skipped_classes) { [Tale] }
|
|
293
|
+
|
|
294
|
+
before do
|
|
295
|
+
base_instance
|
|
296
|
+
tale_instance.save!
|
|
297
|
+
base_instance.end_date = end_date
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it 'does not propagate to that class' do
|
|
301
|
+
expect{ end_date_propagator.call }.not_to change{
|
|
302
|
+
tale_instance.reload.end_date }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
data/spec/spec_models.rb
CHANGED
|
@@ -64,6 +64,8 @@ Temping.create :mama do
|
|
|
64
64
|
t.date :end_date
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
acts_as_span
|
|
68
|
+
|
|
67
69
|
has_many :one_parent_children
|
|
68
70
|
has_many :two_parent_children
|
|
69
71
|
end
|
|
@@ -74,5 +76,116 @@ Temping.create :papa do
|
|
|
74
76
|
t.date :end_date
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
acts_as_span
|
|
80
|
+
|
|
77
81
|
has_many :one_parent_children
|
|
78
82
|
end
|
|
83
|
+
|
|
84
|
+
# fulfill association requirements for EndDatePropagator
|
|
85
|
+
Temping.create :base do
|
|
86
|
+
has_many :children, dependent: :destroy
|
|
87
|
+
has_many :dogs, dependent: :destroy
|
|
88
|
+
has_many :birds, through: :children
|
|
89
|
+
has_many :tales, dependent: :destroy
|
|
90
|
+
|
|
91
|
+
acts_as_span
|
|
92
|
+
|
|
93
|
+
with_columns do |t|
|
|
94
|
+
t.date :end_date
|
|
95
|
+
t.date :start_date
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Temping.create :cat_owner do
|
|
100
|
+
has_many :cats, dependent: :destroy
|
|
101
|
+
|
|
102
|
+
acts_as_span
|
|
103
|
+
|
|
104
|
+
with_columns do |t|
|
|
105
|
+
t.date :start_date
|
|
106
|
+
t.date :end_date
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
Temping.create :cat do
|
|
111
|
+
belongs_to :cat_owner
|
|
112
|
+
|
|
113
|
+
with_columns do |t|
|
|
114
|
+
t.belongs_to :cat_owner
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Temping.create :other_base do
|
|
119
|
+
has_many :children, dependent: :destroy
|
|
120
|
+
|
|
121
|
+
acts_as_span
|
|
122
|
+
|
|
123
|
+
with_columns do |t|
|
|
124
|
+
t.date :end_date
|
|
125
|
+
t.date :start_date
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# has non-standard start_ and end_field names
|
|
130
|
+
Temping.create :child do
|
|
131
|
+
belongs_to :base
|
|
132
|
+
belongs_to :other_base
|
|
133
|
+
has_many :birds, dependent: :destroy
|
|
134
|
+
|
|
135
|
+
validates_with ActsAsSpan::WithinParentDateSpanValidator,
|
|
136
|
+
parents: [:base]
|
|
137
|
+
|
|
138
|
+
acts_as_span(
|
|
139
|
+
start_field: :date_of_birth,
|
|
140
|
+
end_field: :emancipation_date
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
with_columns do |t|
|
|
144
|
+
t.date :date_of_birth
|
|
145
|
+
t.date :emancipation_date
|
|
146
|
+
t.string :manual_invalidation
|
|
147
|
+
t.belongs_to :base
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
Temping.create :dog do
|
|
152
|
+
belongs_to :base
|
|
153
|
+
|
|
154
|
+
acts_as_span
|
|
155
|
+
|
|
156
|
+
with_columns do |t|
|
|
157
|
+
t.date :start_date
|
|
158
|
+
t.date :end_date
|
|
159
|
+
t.belongs_to :base
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
Temping.create :bird do
|
|
165
|
+
belongs_to :child
|
|
166
|
+
|
|
167
|
+
validates_with ActsAsSpan::WithinParentDateSpanValidator,
|
|
168
|
+
parents: [:child]
|
|
169
|
+
|
|
170
|
+
acts_as_span
|
|
171
|
+
|
|
172
|
+
with_columns do |t|
|
|
173
|
+
t.date :end_date
|
|
174
|
+
t.date :start_date
|
|
175
|
+
t.belongs_to :child
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
Temping.create :tale do
|
|
180
|
+
belongs_to :base
|
|
181
|
+
|
|
182
|
+
acts_as_span
|
|
183
|
+
|
|
184
|
+
with_columns do |t|
|
|
185
|
+
t.date :end_date
|
|
186
|
+
t.date :start_date
|
|
187
|
+
t.belongs_to :base
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acts_as_span
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Sullivan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-03-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 1.
|
|
61
|
+
version: '1.4'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 1.
|
|
68
|
+
version: '1.4'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: has_siblings
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -145,13 +145,16 @@ extra_rdoc_files: []
|
|
|
145
145
|
files:
|
|
146
146
|
- ".gitignore"
|
|
147
147
|
- ".rspec"
|
|
148
|
+
- ".tool-versions"
|
|
148
149
|
- ".travis.yml"
|
|
149
150
|
- Gemfile
|
|
150
151
|
- LICENSE
|
|
151
152
|
- README.rdoc
|
|
152
153
|
- Rakefile
|
|
153
154
|
- acts_as_span.gemspec
|
|
155
|
+
- config/locales/en/acts_as_span.yml
|
|
154
156
|
- lib/acts_as_span.rb
|
|
157
|
+
- lib/acts_as_span/end_date_propagator.rb
|
|
155
158
|
- lib/acts_as_span/no_overlap_validator.rb
|
|
156
159
|
- lib/acts_as_span/span_instance.rb
|
|
157
160
|
- lib/acts_as_span/span_instance/status.rb
|
|
@@ -162,6 +165,7 @@ files:
|
|
|
162
165
|
- lib/acts_as_span/within_parent_date_span_validator.rb
|
|
163
166
|
- spec/lib/acts_as_span_spec.rb
|
|
164
167
|
- spec/lib/delegation_spec.rb
|
|
168
|
+
- spec/lib/end_date_propagator_spec.rb
|
|
165
169
|
- spec/lib/no_overlap_validator_spec.rb
|
|
166
170
|
- spec/lib/span_instance/named_scopes_on_spec.rb
|
|
167
171
|
- spec/lib/span_instance/named_scopes_spec.rb
|
|
@@ -192,9 +196,21 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
192
196
|
- !ruby/object:Gem::Version
|
|
193
197
|
version: '0'
|
|
194
198
|
requirements: []
|
|
195
|
-
|
|
196
|
-
rubygems_version: 2.7.6
|
|
199
|
+
rubygems_version: 3.1.2
|
|
197
200
|
signing_key:
|
|
198
201
|
specification_version: 4
|
|
199
|
-
summary: acts_as_span 1.
|
|
200
|
-
test_files:
|
|
202
|
+
summary: acts_as_span 1.1.0
|
|
203
|
+
test_files:
|
|
204
|
+
- spec/lib/acts_as_span_spec.rb
|
|
205
|
+
- spec/lib/delegation_spec.rb
|
|
206
|
+
- spec/lib/end_date_propagator_spec.rb
|
|
207
|
+
- spec/lib/no_overlap_validator_spec.rb
|
|
208
|
+
- spec/lib/span_instance/named_scopes_on_spec.rb
|
|
209
|
+
- spec/lib/span_instance/named_scopes_spec.rb
|
|
210
|
+
- spec/lib/span_instance/overlap_spec.rb
|
|
211
|
+
- spec/lib/span_instance/status_spec.rb
|
|
212
|
+
- spec/lib/span_instance/validations_spec.rb
|
|
213
|
+
- spec/lib/span_instance_spec.rb
|
|
214
|
+
- spec/lib/within_parent_date_span_validator_spec.rb
|
|
215
|
+
- spec/spec_helper.rb
|
|
216
|
+
- spec/spec_models.rb
|