acts_as_span 0.0.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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