acts_as_span 0.0.6 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(end_field, "Must be on or after #{start_field}")
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
- (arel_table[start_field].lteq(query_date).or(arel_table[start_field].eq(nil))).
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
@@ -1,8 +1,10 @@
1
+ # frozen_string_Literal: true
2
+
1
3
  module ActsAsSpan
2
4
  module VERSION
3
- MAJOR = 0
4
- MINOR = 0
5
- TINY = 6
5
+ MAJOR = 1
6
+ MINOR = 2
7
+ TINY = 1
6
8
  PRE = nil
7
9
 
8
10
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -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, :not_within_parent_date_span, parent: record.class.human_attribute_name(parent)) if outside_of_parent_date_span?(record, parent)
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