acts_as_span 0.0.5 → 1.2.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -3
  3. data/.rspec +2 -0
  4. data/.tool-versions +1 -0
  5. data/.travis.yml +12 -0
  6. data/Gemfile +0 -3
  7. data/README.rdoc +4 -16
  8. data/Rakefile +5 -1
  9. data/acts_as_span.gemspec +31 -14
  10. data/config/locales/en/acts_as_span.yml +15 -0
  11. data/lib/acts_as_span.rb +69 -98
  12. data/lib/acts_as_span/end_date_propagator.rb +198 -0
  13. data/lib/acts_as_span/no_overlap_validator.rb +86 -0
  14. data/lib/acts_as_span/span_instance.rb +24 -32
  15. data/lib/acts_as_span/span_instance/status.rb +12 -27
  16. data/lib/acts_as_span/span_instance/validations.rb +11 -53
  17. data/lib/acts_as_span/span_klass.rb +11 -19
  18. data/lib/acts_as_span/span_klass/status.rb +43 -12
  19. data/lib/acts_as_span/version.rb +6 -4
  20. data/lib/acts_as_span/within_parent_date_span_validator.rb +44 -0
  21. data/spec/lib/acts_as_span_spec.rb +38 -35
  22. data/spec/lib/delegation_spec.rb +45 -78
  23. data/spec/lib/end_date_propagator_spec.rb +319 -0
  24. data/spec/lib/no_overlap_validator_spec.rb +129 -0
  25. data/spec/lib/span_instance/named_scopes_on_spec.rb +193 -193
  26. data/spec/lib/span_instance/named_scopes_spec.rb +193 -191
  27. data/spec/lib/span_instance/overlap_spec.rb +193 -253
  28. data/spec/lib/span_instance/status_spec.rb +22 -35
  29. data/spec/lib/span_instance/validations_spec.rb +8 -44
  30. data/spec/lib/span_instance_spec.rb +17 -30
  31. data/spec/lib/span_klass/status_spec.rb +38 -0
  32. data/spec/lib/within_parent_date_span_validator_spec.rb +126 -0
  33. data/spec/spec_helper.rb +19 -6
  34. data/spec/spec_models.rb +226 -0
  35. metadata +167 -61
  36. data/Gemfile.lock +0 -47
  37. data/lib/acts_as_span/span_instance/overlap.rb +0 -17
  38. data/lib/acts_as_span/span_klass/overlap.rb +0 -21
  39. data/spec/lib/negative_spec.rb +0 -30
  40. data/spec/spec.opts +0 -1
@@ -1,64 +1,67 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "acts_as_span" do
4
- before(:each) do
5
- build_model :span_model do
6
- acts_as_span
7
- end
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
8
23
  end
9
-
24
+
10
25
  context "ClassMethods" do
11
- it "should return true for acts_as_span?" do
12
- SpanModel.acts_as_span?.should be_true
13
- end
14
-
15
26
  it "should have 1 acts_as_span_definition" do
16
- SpanModel.should have(1).acts_as_span_definitions
27
+ expect(SpanModel.acts_as_span_definitions.size).to eq(1)
17
28
  end
18
-
29
+
19
30
  it "should set default options for acts_as_span_definition" do
20
31
  span_definition = SpanModel.acts_as_span_definitions[:default]
21
-
22
- span_definition.start_date_field.should == :start_date
23
- span_definition.end_date_field.should == :end_date
24
- span_definition.start_date_field_required.should be_false
25
- span_definition.end_date_field_required.should be_false
26
- span_definition.exclude_end.should be_false
27
- span_definition.span_overlap_scope.should be_nil
28
- span_definition.span_overlap_count.should be_nil
29
- span_definition.name.should == :default
32
+
33
+ expect(span_definition.start_field).to eq(:start_date)
34
+ expect(span_definition.end_field).to eq(:end_date)
35
+ expect(span_definition.exclude_end).to be_falsey
36
+ expect(span_definition.name).to eq(:default)
30
37
  end
31
-
38
+
32
39
  it "should return a SpanKlass w/ span" do
33
- SpanModel.span.should be_instance_of(ActsAsSpan::SpanKlass)
40
+ expect(SpanModel.span).to be_instance_of(ActsAsSpan::SpanKlass)
34
41
  end
35
42
 
36
43
  it "should return a SpanKlass w/ span_for(:default)" do
37
- SpanModel.span_for(:default).should be_instance_of(ActsAsSpan::SpanKlass)
44
+ expect(SpanModel.span_for(:default)).to be_instance_of(ActsAsSpan::SpanKlass)
38
45
  end
39
-
46
+
40
47
  it "should have (1) spans" do
41
- SpanModel.spans.should have(1).span
48
+ expect(SpanModel.spans.size).to eq(1)
42
49
  end
43
50
  end
44
-
51
+
45
52
  context "InstanceMethods" do
46
53
  let(:span_model) { SpanModel.new }
47
-
48
- it "should return true for acts_as_span?" do
49
- span_model.acts_as_span?.should be_true
50
- end
51
54
 
52
55
  it "should return a SpanInstance w/ span" do
53
- span_model.span.should be_instance_of(ActsAsSpan::SpanInstance)
56
+ expect(span_model.span).to be_instance_of(ActsAsSpan::SpanInstance)
54
57
  end
55
58
 
56
59
  it "should return a SpanInstance w/ span_for(:default)" do
57
- span_model.span_for(:default).should be_instance_of(ActsAsSpan::SpanInstance)
60
+ expect(span_model.span_for(:default)).to be_instance_of(ActsAsSpan::SpanInstance)
58
61
  end
59
-
62
+
60
63
  it "should have (1) spans" do
61
- span_model.spans.should have(1).span
64
+ expect(span_model.spans.size).to eq(1)
62
65
  end
63
66
  end
64
67
  end
@@ -1,128 +1,95 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "Span" do
4
- before(:each) do
5
- build_model :span_model do
6
- date :start_date
7
- date :end_date
8
-
9
- acts_as_span
10
- end
11
- end
12
-
13
- let(:span_model) { SpanModel.new(:start_date => Date.today, :end_date => nil) }
3
+ RSpec.describe "Span" do
4
+ let(:span_model) { SpanModel.new(:start_date => Date.current, :end_date => nil) }
14
5
  let(:span_klass) { SpanModel.span }
15
6
  let(:span_instance) { span_model.span }
16
-
7
+
17
8
  context "ClassMethods" do
18
9
  it "should delegate current" do
19
- span_klass.should_receive(:current).and_return(true)
20
-
10
+ expect(span_klass).to receive(:current).and_return(true)
11
+
21
12
  SpanModel.current
22
13
  end
23
-
14
+
24
15
  it "should delegate current_on" do
25
- span_klass.should_receive(:current_on).and_return(true)
26
-
16
+ expect(span_klass).to receive(:current_on).and_return(true)
17
+
27
18
  SpanModel.current_on
28
19
  end
29
-
20
+
30
21
  it "should delegate future" do
31
- span_klass.should_receive(:future).and_return(true)
32
-
22
+ expect(span_klass).to receive(:future).and_return(true)
23
+
33
24
  SpanModel.future
34
25
  end
35
-
26
+
36
27
  it "should delegate future_on" do
37
- span_klass.should_receive(:future_on).and_return(true)
38
-
28
+ expect(span_klass).to receive(:future_on).and_return(true)
29
+
39
30
  SpanModel.future_on
40
31
  end
41
-
32
+
42
33
  it "should delegate expired" do
43
- span_klass.should_receive(:expired).and_return(true)
44
-
34
+ expect(span_klass).to receive(:expired).and_return(true)
35
+
45
36
  SpanModel.expired
46
37
  end
47
-
38
+
48
39
  it "should delegate expired_on" do
49
- span_klass.should_receive(:expired_on).and_return(true)
50
-
40
+ expect(span_klass).to receive(:expired_on).and_return(true)
41
+
51
42
  SpanModel.expired_on
52
43
  end
53
44
  end
54
-
45
+
55
46
  context "InstanceMethods" do
56
- it "should delegate close!" do
57
- span_instance.should_receive(:close!).and_return(true)
58
-
59
- span_model.close!
60
- end
61
-
62
- it "should delegate close_on!" do
63
- span_instance.should_receive(:close_on!).and_return(true)
64
-
65
- span_model.close_on!
66
- end
67
-
68
47
  it "should delegate span_status" do
69
- span_instance.should_receive(:span_status).and_return(true)
70
-
48
+ expect(span_instance).to receive(:span_status).and_return(true)
49
+
71
50
  span_model.span_status
72
51
  end
73
-
52
+
74
53
  it "should delegate span_status_on" do
75
- span_instance.should_receive(:span_status_on).and_return(true)
76
-
54
+ expect(span_instance).to receive(:span_status_on).and_return(true)
55
+
77
56
  span_model.span_status_on
78
57
  end
79
-
80
- it "should delegate span_status_to_s" do
81
- span_instance.should_receive(:span_status_to_s).and_return(true)
82
-
83
- span_model.span_status_to_s
84
- end
85
-
86
- it "should delegate span_status_to_s_on" do
87
- span_instance.should_receive(:span_status_to_s_on).and_return(true)
88
-
89
- span_model.span_status_to_s_on
90
- end
91
-
58
+
92
59
  it "should delegate current?" do
93
- span_instance.should_receive(:current?).and_return(true)
94
-
60
+ expect(span_instance).to receive(:current?).and_return(true)
61
+
95
62
  span_model.current?
96
63
  end
97
-
64
+
98
65
  it "should delegate current_on?" do
99
- span_instance.should_receive(:current_on?).and_return(true)
100
-
66
+ expect(span_instance).to receive(:current_on?).and_return(true)
67
+
101
68
  span_model.current_on?
102
69
  end
103
-
70
+
104
71
  it "should delegate future?" do
105
- span_instance.should_receive(:future?).and_return(true)
106
-
72
+ expect(span_instance).to receive(:future?).and_return(true)
73
+
107
74
  span_model.future?
108
75
  end
109
-
76
+
110
77
  it "should delegate future_on?" do
111
- span_instance.should_receive(:future_on?).and_return(true)
112
-
78
+ expect(span_instance).to receive(:future_on?).and_return(true)
79
+
113
80
  span_model.future_on?
114
81
  end
115
-
82
+
116
83
  it "should delegate expired?" do
117
- span_instance.should_receive(:expired?).and_return(true)
118
-
84
+ expect(span_instance).to receive(:expired?).and_return(true)
85
+
119
86
  span_model.expired?
120
87
  end
121
-
88
+
122
89
  it "should delegate expired_on?" do
123
- span_instance.should_receive(:expired_on?).and_return(true)
124
-
90
+ expect(span_instance).to receive(:expired_on?).and_return(true)
91
+
125
92
  span_model.expired_on?
126
93
  end
127
94
  end
128
- end
95
+ end
@@ -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.values.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.values.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.values).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