acts_as_span 0.0.6 → 1.2.1
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 +5 -5
- data/.tool-versions +1 -0
- data/.travis.yml +8 -2
- data/acts_as_span.gemspec +24 -23
- data/config/locales/en/acts_as_span.yml +15 -0
- data/lib/acts_as_span.rb +14 -0
- data/lib/acts_as_span/end_date_propagator.rb +196 -0
- data/lib/acts_as_span/no_overlap_validator.rb +60 -25
- data/lib/acts_as_span/span_instance.rb +8 -0
- data/lib/acts_as_span/span_instance/validations.rb +7 -1
- data/lib/acts_as_span/span_klass/status.rb +14 -4
- data/lib/acts_as_span/version.rb +5 -3
- data/lib/acts_as_span/within_parent_date_span_validator.rb +9 -7
- data/spec/lib/acts_as_span_spec.rb +21 -0
- data/spec/lib/end_date_propagator_spec.rb +319 -0
- data/spec/lib/no_overlap_validator_spec.rb +34 -1
- data/spec/lib/span_instance_spec.rb +12 -0
- data/spec/lib/span_klass/status_spec.rb +38 -0
- data/spec/lib/within_parent_date_span_validator_spec.rb +11 -0
- data/spec/spec_models.rb +158 -1
- metadata +40 -34
@@ -33,5 +33,13 @@ module ActsAsSpan
|
|
33
33
|
def end_date
|
34
34
|
span_model[end_field]
|
35
35
|
end
|
36
|
+
|
37
|
+
def start_date_changed?
|
38
|
+
span_model.will_save_change_to_attribute?(start_field)
|
39
|
+
end
|
40
|
+
|
41
|
+
def end_date_changed?
|
42
|
+
span_model.will_save_change_to_attribute?(end_field)
|
43
|
+
end
|
36
44
|
end
|
37
45
|
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
|
@@ -8,10 +8,7 @@ module ActsAsSpan
|
|
8
8
|
included do
|
9
9
|
def current(query_date = Date.current)
|
10
10
|
klass.where(
|
11
|
-
(
|
12
|
-
and(
|
13
|
-
arel_table[end_field].eq(nil).or(arel_table[end_field].gteq(query_date))
|
14
|
-
)
|
11
|
+
current_condition(query_date: query_date, table: arel_table)
|
15
12
|
)
|
16
13
|
end
|
17
14
|
|
@@ -44,6 +41,19 @@ module ActsAsSpan
|
|
44
41
|
end
|
45
42
|
|
46
43
|
alias_method :current_or_future, :current_or_future_on
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# returns an Arel node usable within an ActiveRecord `where` clause
|
48
|
+
def current_condition(query_date:, table:)
|
49
|
+
start_col = arel_table[start_field]
|
50
|
+
end_col = arel_table[end_field]
|
51
|
+
|
52
|
+
start_condition = start_col.lteq(query_date).or(start_col.eq(nil))
|
53
|
+
end_condition = end_col.eq(nil).or(end_col.gteq(query_date))
|
54
|
+
|
55
|
+
start_condition.and(end_condition)
|
56
|
+
end
|
47
57
|
end
|
48
58
|
end
|
49
59
|
end
|
data/lib/acts_as_span/version.rb
CHANGED
@@ -3,8 +3,10 @@ module ActsAsSpan
|
|
3
3
|
def validate(record)
|
4
4
|
parents = options[:parent] || options[:parents]
|
5
5
|
|
6
|
+
error_message = options[:message] || :not_within_parent_date_span
|
7
|
+
|
6
8
|
Array(parents).each do |parent|
|
7
|
-
record.errors.add(:base,
|
9
|
+
record.errors.add(:base, error_message, parent: record.class.human_attribute_name(parent)) if outside_of_parent_date_span?(record, parent)
|
8
10
|
end
|
9
11
|
end
|
10
12
|
|
@@ -22,21 +24,21 @@ module ActsAsSpan
|
|
22
24
|
private
|
23
25
|
|
24
26
|
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
|
27
|
+
record.span.start_date.present? && parent.span.start_date.present? &&
|
28
|
+
record.span.start_date < parent.span.start_date
|
27
29
|
end
|
28
30
|
|
29
31
|
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
|
32
|
+
record.span.end_date.present? && parent.span.end_date.present? &&
|
33
|
+
record.span.end_date > parent.span.end_date
|
32
34
|
end
|
33
35
|
|
34
36
|
def child_record_without_start_date(record, parent)
|
35
|
-
record.start_date.nil? && parent.start_date.present?
|
37
|
+
record.span.start_date.nil? && parent.span.start_date.present?
|
36
38
|
end
|
37
39
|
|
38
40
|
def child_record_without_end_date(record, parent)
|
39
|
-
record.end_date.nil? && parent.end_date.present?
|
41
|
+
record.span.end_date.nil? && parent.span.end_date.present?
|
40
42
|
end
|
41
43
|
end
|
42
44
|
end
|
@@ -1,6 +1,27 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe "acts_as_span" do
|
4
|
+
it 'raises an ArgumentError when unsupported arguments are passed' do
|
5
|
+
expect do
|
6
|
+
SpannableModel.acts_as_span(
|
7
|
+
start_field: :starting_date,
|
8
|
+
end_field: :ending_date,
|
9
|
+
span_overlap_scope: [:unique_by_date_range]
|
10
|
+
)
|
11
|
+
end.to raise_error(
|
12
|
+
ArgumentError, "Unsupported option(s): 'span_overlap_scope'"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "doesn't raise an ArgumentError when valid arguments are passed" do
|
17
|
+
expect do
|
18
|
+
SpannableModel.acts_as_span(
|
19
|
+
start_field: :starting_date,
|
20
|
+
end_field: :ending_date
|
21
|
+
)
|
22
|
+
end.not_to raise_error
|
23
|
+
end
|
24
|
+
|
4
25
|
context "ClassMethods" do
|
5
26
|
it "should have 1 acts_as_span_definition" do
|
6
27
|
expect(SpanModel.acts_as_span_definitions.size).to eq(1)
|
@@ -0,0 +1,319 @@
|
|
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
|
+
include_errors: include_errors
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:base_instance) do
|
12
|
+
Base.create(end_date: initial_end_date)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:include_errors) { true }
|
16
|
+
let(:initial_end_date) { nil }
|
17
|
+
|
18
|
+
let(:other_base_instance) do
|
19
|
+
OtherBase.create(end_date: other_end_date)
|
20
|
+
end
|
21
|
+
let(:other_end_date) { nil }
|
22
|
+
|
23
|
+
let(:skipped_classes) { [] }
|
24
|
+
|
25
|
+
let!(:child_instance) do
|
26
|
+
Child.create(
|
27
|
+
base: base_instance,
|
28
|
+
emancipation_date: child_end_date
|
29
|
+
)
|
30
|
+
end
|
31
|
+
let(:child_end_date) { nil }
|
32
|
+
|
33
|
+
let!(:dog_instance) do
|
34
|
+
Dog.create(
|
35
|
+
base: base_instance,
|
36
|
+
end_date: dog_end_date
|
37
|
+
)
|
38
|
+
end
|
39
|
+
let(:dog_end_date) { nil }
|
40
|
+
|
41
|
+
let!(:bird_instance) do
|
42
|
+
Bird.create(
|
43
|
+
child: child_instance,
|
44
|
+
end_date: child_end_date
|
45
|
+
)
|
46
|
+
end
|
47
|
+
let(:bird_end_date) { nil }
|
48
|
+
|
49
|
+
let(:tale_instance) do
|
50
|
+
base_instance.tales.create(start_date: Date.current, end_date: nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '@errors_cache' do
|
54
|
+
let(:base_start_date) { Date.current - 7 }
|
55
|
+
let(:initial_end_date) { nil }
|
56
|
+
let(:end_date) { Date.current }
|
57
|
+
|
58
|
+
let(:child_start_date) { base_start_date + 1 }
|
59
|
+
let!(:child_instance) do
|
60
|
+
Child.create(
|
61
|
+
base: base_instance,
|
62
|
+
date_of_birth: child_start_date,
|
63
|
+
emancipation_date: child_end_date
|
64
|
+
)
|
65
|
+
end
|
66
|
+
let(:bird_start_date) { child_start_date + 1 }
|
67
|
+
let!(:bird_instance) do
|
68
|
+
Bird.create(
|
69
|
+
child: child_instance,
|
70
|
+
start_date: bird_start_date,
|
71
|
+
end_date: bird_end_date
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
before do
|
76
|
+
base_instance.start_date = base_start_date
|
77
|
+
base_instance.save!
|
78
|
+
base_instance.end_date = end_date
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when all child records are successfully saved' do
|
82
|
+
it 'the parent record does not have any errors' do
|
83
|
+
expect(
|
84
|
+
end_date_propagator.call.errors.full_messages
|
85
|
+
).to be_empty
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'when one grandchild record is not valid' do
|
90
|
+
before do
|
91
|
+
bird_instance.start_date = child_start_date - 1
|
92
|
+
bird_instance.save(validate: false)
|
93
|
+
end
|
94
|
+
it "the parent shows that grandchild's errors" do
|
95
|
+
expect(
|
96
|
+
end_date_propagator.call.errors.full_messages.join
|
97
|
+
).to include(
|
98
|
+
I18n.t(
|
99
|
+
'not_within_parent_date_span',
|
100
|
+
parent: 'Child',
|
101
|
+
scope: %i[activerecord errors messages]
|
102
|
+
)
|
103
|
+
)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'when multiple child records are not valid' do
|
108
|
+
context 'when include_errors = true (default)' do
|
109
|
+
before do
|
110
|
+
child_instance.date_of_birth = base_instance.span.start_date - 1
|
111
|
+
child_instance.save(validate: false)
|
112
|
+
bird_instance.start_date = child_instance.span.start_date - 1
|
113
|
+
bird_instance.save(validate: false)
|
114
|
+
end
|
115
|
+
it "the parent gains all children's errors" do
|
116
|
+
expect(
|
117
|
+
end_date_propagator.call.errors.full_messages.join
|
118
|
+
).to include(
|
119
|
+
I18n.t(
|
120
|
+
'not_within_parent_date_span',
|
121
|
+
parent: 'Child',
|
122
|
+
scope: %i[activerecord errors messages]
|
123
|
+
)
|
124
|
+
).and include(
|
125
|
+
I18n.t(
|
126
|
+
'not_within_parent_date_span',
|
127
|
+
parent: 'Base',
|
128
|
+
scope: %i[activerecord errors messages]
|
129
|
+
)
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'when include_errors = false' do
|
135
|
+
let(:include_errors) { false }
|
136
|
+
|
137
|
+
it 'does not push any child errors' do
|
138
|
+
expect(end_date_propagator.call.errors.full_messages).to be_empty
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe '.call' do
|
145
|
+
subject(:result) do
|
146
|
+
ActsAsSpan::EndDatePropagator.call(obj, call_options)
|
147
|
+
end
|
148
|
+
let(:obj) { base_instance }
|
149
|
+
|
150
|
+
context 'when no skipped classes are passed' do
|
151
|
+
let(:call_options) { {} }
|
152
|
+
|
153
|
+
it 'forwards the correct arguments to :new' do
|
154
|
+
expect(ActsAsSpan::EndDatePropagator)
|
155
|
+
.to receive(:new).with(obj, call_options).and_call_original
|
156
|
+
expect(result).to eq(obj)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context 'when skipped classes are passed' do
|
161
|
+
let(:call_options) { { skipped_classes: ['bungus'] } }
|
162
|
+
|
163
|
+
it 'forwards the correct arguments to :new' do
|
164
|
+
expect(ActsAsSpan::EndDatePropagator)
|
165
|
+
.to receive(:new).with(obj, call_options).and_call_original
|
166
|
+
expect(result).to eq(obj)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe '#call' do
|
172
|
+
context 'without an end_date' do
|
173
|
+
let(:object_instance) { SpannableModel.new }
|
174
|
+
|
175
|
+
it 'does not raise an error' do
|
176
|
+
expect do
|
177
|
+
ActsAsSpan::EndDatePropagator.new(object_instance).call
|
178
|
+
end.not_to raise_error
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
context 'updates children' do
|
183
|
+
before do
|
184
|
+
base_instance
|
185
|
+
base_instance.end_date = end_date
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'base_instance.end_date nil -> !nil' do
|
189
|
+
let(:initial_end_date) { nil }
|
190
|
+
let(:end_date) { Date.current }
|
191
|
+
|
192
|
+
context 'child_end_date == initial_end_date' do
|
193
|
+
let(:child_end_date) { initial_end_date }
|
194
|
+
|
195
|
+
it 'propagates to the child_instance' do
|
196
|
+
expect{ end_date_propagator.call }.to change{
|
197
|
+
child_instance.reload.emancipation_date }
|
198
|
+
.from(child_end_date).to(base_instance.end_date)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context 'child_end_date >= initial_end_date' do
|
203
|
+
let(:child_end_date) { end_date + 3 }
|
204
|
+
|
205
|
+
it 'propagates to the child_instance' do
|
206
|
+
expect{ end_date_propagator.call }.to change{
|
207
|
+
child_instance.reload.emancipation_date}
|
208
|
+
.from(child_end_date).to(base_instance.end_date)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'child_end_date <= initial_end_date' do
|
213
|
+
let(:child_end_date) { end_date - 3 }
|
214
|
+
|
215
|
+
it 'does not propagate to the child_instance' do
|
216
|
+
expect{ end_date_propagator.call }.not_to change{
|
217
|
+
child_instance.reload.emancipation_date}
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'when a child cannot have its end date updated' do
|
222
|
+
before do
|
223
|
+
# add a "within parent date span" error to child
|
224
|
+
base_instance.start_date = Date.current - 1
|
225
|
+
child_instance.date_of_birth = Date.current - 2
|
226
|
+
child_instance.save(validate: false)
|
227
|
+
end
|
228
|
+
|
229
|
+
it "the parent's end date is not updated" do
|
230
|
+
expect{ end_date_propagator.call }.to change{
|
231
|
+
base_instance.errors[:base]
|
232
|
+
}.from([])
|
233
|
+
end
|
234
|
+
|
235
|
+
context 'and the child is the child of a child' do
|
236
|
+
before do
|
237
|
+
end
|
238
|
+
|
239
|
+
it "the parent's end date is not updated" do
|
240
|
+
expect{ end_date_propagator.call }.not_to change{
|
241
|
+
base_instance.reload.end_date
|
242
|
+
}
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'base_instance.end_date !nil -> nil' do
|
249
|
+
let(:initial_end_date) { Date.current }
|
250
|
+
let(:end_date) { nil }
|
251
|
+
let(:child_end_date) { initial_end_date }
|
252
|
+
|
253
|
+
it 'does not propagate to the child_instance' do
|
254
|
+
expect{ end_date_propagator.call }.not_to change{
|
255
|
+
child_instance.reload.emancipation_date }
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
context 'base_instance.end_date not changed' do
|
260
|
+
let(:end_date) { initial_end_date }
|
261
|
+
|
262
|
+
it 'does not propagate to the child_instance' do
|
263
|
+
expect{ end_date_propagator.call }.not_to change{
|
264
|
+
child_instance.reload.emancipation_date }
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
context 'has access to all children via has_many associations' do
|
269
|
+
let(:end_date) { Date.current }
|
270
|
+
|
271
|
+
it 'changes the end_date of all child associations' do
|
272
|
+
expect{ end_date_propagator.call }.to change{
|
273
|
+
child_instance.reload.emancipation_date }.
|
274
|
+
from(child_instance.emancipation_date).to(base_instance.end_date)
|
275
|
+
.and change{ dog_instance.reload.end_date }
|
276
|
+
.from(dog_instance.end_date).to(base_instance.end_date)
|
277
|
+
.and change{ bird_instance.reload.end_date }
|
278
|
+
.from(bird_instance.end_date).to(base_instance.end_date)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
context 'when child record does not have end_date to update' do
|
284
|
+
let!(:cat_owner_instance) do
|
285
|
+
CatOwner.create(end_date: initial_end_date)
|
286
|
+
end
|
287
|
+
let!(:cat_instance) do
|
288
|
+
Cat.create(cat_owner: cat_owner_instance)
|
289
|
+
end
|
290
|
+
let(:cat_end_date) { nil }
|
291
|
+
let(:end_date) { Date.current }
|
292
|
+
|
293
|
+
before do
|
294
|
+
cat_owner_instance.end_date = end_date
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'does not throw an error' do
|
298
|
+
expect(cat_instance).not_to respond_to(:end_date)
|
299
|
+
expect{ end_date_propagator.call }.not_to raise_error
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
context 'when a class is skipped' do
|
304
|
+
let(:end_date) { Date.current }
|
305
|
+
let(:skipped_classes) { [Tale] }
|
306
|
+
|
307
|
+
before do
|
308
|
+
base_instance
|
309
|
+
tale_instance.save!
|
310
|
+
base_instance.end_date = end_date
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'does not propagate to that class' do
|
314
|
+
expect{ end_date_propagator.call }.not_to change{
|
315
|
+
tale_instance.reload.end_date }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|