acts_as_span 1.0.0 → 1.2.2

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.
@@ -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
5
  MAJOR = 1
4
- MINOR = 0
5
- TINY = 0
6
+ MINOR = 2
7
+ TINY = 2
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
@@ -0,0 +1,344 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActsAsSpan::EndDatePropagator do
4
+ let(:end_date_propagator) do
5
+ ActsAsSpan::EndDatePropagator.new(
6
+ base_instance,
7
+ skipped_classes: skipped_classes,
8
+ include_errors: include_errors,
9
+ )
10
+ end
11
+
12
+ let(:base_instance) do
13
+ Base.create(end_date: initial_end_date)
14
+ end
15
+
16
+ let(:include_errors) { true }
17
+ let(:initial_end_date) { nil }
18
+
19
+ let(:other_base_instance) do
20
+ OtherBase.create(end_date: other_end_date)
21
+ end
22
+ let(:other_end_date) { nil }
23
+
24
+ let(:skipped_classes) { [] }
25
+
26
+ let!(:child_instance) do
27
+ Child.create(
28
+ base: base_instance,
29
+ emancipation_date: child_end_date
30
+ )
31
+ end
32
+ let(:child_end_date) { nil }
33
+
34
+ let!(:dog_instance) do
35
+ Dog.create(
36
+ base: base_instance,
37
+ end_date: dog_end_date
38
+ )
39
+ end
40
+ let(:dog_end_date) { nil }
41
+
42
+ let!(:bird_instance) do
43
+ Bird.create(
44
+ child: child_instance,
45
+ end_date: child_end_date
46
+ )
47
+ end
48
+ let(:bird_end_date) { nil }
49
+
50
+ let(:tale_instance) do
51
+ base_instance.tales.create(start_date: Date.current, end_date: nil)
52
+ end
53
+
54
+ describe '@errors_cache' do
55
+ let(:base_start_date) { Date.current - 7 }
56
+ let(:initial_end_date) { nil }
57
+ let(:end_date) { Date.current }
58
+
59
+ let(:child_start_date) { base_start_date + 1 }
60
+ let!(:child_instance) do
61
+ Child.create(
62
+ base: base_instance,
63
+ date_of_birth: child_start_date,
64
+ emancipation_date: child_end_date
65
+ )
66
+ end
67
+ let(:bird_start_date) { child_start_date + 1 }
68
+ let!(:bird_instance) do
69
+ Bird.create(
70
+ child: child_instance,
71
+ start_date: bird_start_date,
72
+ end_date: bird_end_date
73
+ )
74
+ end
75
+
76
+ before do
77
+ base_instance.start_date = base_start_date
78
+ base_instance.save!
79
+ base_instance.end_date = end_date
80
+ end
81
+
82
+ context 'when all child records are successfully saved' do
83
+ it 'the parent record does not have any errors' do
84
+ expect(
85
+ end_date_propagator.call.errors.full_messages
86
+ ).to be_empty
87
+ end
88
+ end
89
+
90
+ context 'when one grandchild record is not valid' do
91
+ before do
92
+ bird_instance.start_date = child_start_date - 1
93
+ bird_instance.save(validate: false)
94
+ end
95
+ it "the parent shows that grandchild's errors" do
96
+ expect(
97
+ end_date_propagator.call.errors.full_messages.join
98
+ ).to include(
99
+ I18n.t(
100
+ 'not_within_parent_date_span',
101
+ parent: 'Child',
102
+ scope: %i[activerecord errors messages]
103
+ )
104
+ )
105
+ end
106
+ end
107
+
108
+ context 'when multiple child records are not valid' do
109
+ context 'when include_errors = true (default)' do
110
+ before do
111
+ child_instance.date_of_birth = base_instance.span.start_date - 1
112
+ child_instance.save(validate: false)
113
+ bird_instance.start_date = child_instance.span.start_date - 1
114
+ bird_instance.save(validate: false)
115
+ end
116
+ it "the parent gains all children's errors" do
117
+ expect(
118
+ end_date_propagator.call.errors.full_messages.join
119
+ ).to include(
120
+ I18n.t(
121
+ 'not_within_parent_date_span',
122
+ parent: 'Child',
123
+ scope: %i[activerecord errors messages]
124
+ )
125
+ ).and include(
126
+ I18n.t(
127
+ 'not_within_parent_date_span',
128
+ parent: 'Base',
129
+ scope: %i[activerecord errors messages]
130
+ )
131
+ )
132
+ end
133
+ end
134
+
135
+ context 'when include_errors = false' do
136
+ let(:include_errors) { false }
137
+
138
+ it 'does not push any child errors' do
139
+ expect(end_date_propagator.call.errors.full_messages).to be_empty
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ describe '.call' do
146
+ subject(:result) do
147
+ ActsAsSpan::EndDatePropagator.call(obj, call_options)
148
+ end
149
+ let(:obj) { base_instance }
150
+
151
+ context 'when no skipped classes are passed' do
152
+ let(:call_options) { {} }
153
+
154
+ it 'forwards the correct arguments to :new' do
155
+ expect(ActsAsSpan::EndDatePropagator)
156
+ .to receive(:new).with(obj, call_options).and_call_original
157
+ expect(result).to eq(obj)
158
+ end
159
+ end
160
+
161
+ context 'when skipped classes are passed' do
162
+ let(:call_options) { { skipped_classes: ['bungus'] } }
163
+
164
+ it 'forwards the correct arguments to :new' do
165
+ expect(ActsAsSpan::EndDatePropagator)
166
+ .to receive(:new).with(obj, call_options).and_call_original
167
+ expect(result).to eq(obj)
168
+ end
169
+ end
170
+ end
171
+
172
+ describe '#call' do
173
+ context 'without an end_date' do
174
+ let(:object_instance) { SpannableModel.new }
175
+
176
+ it 'does not raise an error' do
177
+ expect do
178
+ ActsAsSpan::EndDatePropagator.new(object_instance).call
179
+ end.not_to raise_error
180
+ end
181
+ end
182
+
183
+ context 'updates children' do
184
+ before do
185
+ base_instance
186
+ base_instance.end_date = end_date
187
+ end
188
+
189
+ context 'base_instance.end_date nil -> !nil' do
190
+ let(:initial_end_date) { nil }
191
+ let(:end_date) { Date.current }
192
+
193
+ context 'child_end_date == initial_end_date' do
194
+ let(:child_end_date) { initial_end_date }
195
+
196
+ it 'propagates to the child_instance' do
197
+ expect{ end_date_propagator.call }.to change{
198
+ child_instance.reload.emancipation_date }
199
+ .from(child_end_date).to(base_instance.end_date)
200
+ end
201
+ end
202
+
203
+ context 'child_end_date >= initial_end_date' do
204
+ let(:child_end_date) { end_date + 3 }
205
+
206
+ it 'propagates to the child_instance' do
207
+ expect{ end_date_propagator.call }.to change{
208
+ child_instance.reload.emancipation_date}
209
+ .from(child_end_date).to(base_instance.end_date)
210
+ end
211
+ end
212
+
213
+ context 'child_end_date <= initial_end_date' do
214
+ let(:child_end_date) { end_date - 3 }
215
+
216
+ it 'does not propagate to the child_instance' do
217
+ expect{ end_date_propagator.call }.not_to change{
218
+ child_instance.reload.emancipation_date}
219
+ end
220
+ end
221
+
222
+ context 'when a child cannot have its end date updated' do
223
+ before do
224
+ # add a "within parent date span" error to child
225
+ base_instance.start_date = Date.current - 1
226
+ child_instance.date_of_birth = Date.current - 2
227
+ child_instance.save(validate: false)
228
+ end
229
+
230
+ it "the parent's end date is not updated" do
231
+ expect{ end_date_propagator.call }.to change{
232
+ base_instance.errors[:base]
233
+ }.from([])
234
+ end
235
+
236
+ context 'and the child is the child of a child' do
237
+ before do
238
+ end
239
+
240
+ it "the parent's end date is not updated" do
241
+ expect{ end_date_propagator.call }.not_to change{
242
+ base_instance.reload.end_date
243
+ }
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ context 'base_instance.end_date !nil -> nil' do
250
+ let(:initial_end_date) { Date.current }
251
+ let(:end_date) { nil }
252
+ let(:child_end_date) { initial_end_date }
253
+
254
+ it 'does not propagate to the child_instance' do
255
+ expect{ end_date_propagator.call }.not_to change{
256
+ child_instance.reload.emancipation_date }
257
+ end
258
+ end
259
+
260
+ context 'base_instance.end_date not changed' do
261
+ let(:end_date) { initial_end_date }
262
+
263
+ it 'does not propagate to the child_instance' do
264
+ expect{ end_date_propagator.call }.not_to change{
265
+ child_instance.reload.emancipation_date }
266
+ end
267
+ end
268
+
269
+ context 'has access to all children via has_many associations' do
270
+ let(:end_date) { Date.current }
271
+
272
+ it 'changes the end_date of all child associations' do
273
+ expect{ end_date_propagator.call }.to change{
274
+ child_instance.reload.emancipation_date }.
275
+ from(child_instance.emancipation_date).to(base_instance.end_date)
276
+ .and change{ dog_instance.reload.end_date }
277
+ .from(dog_instance.end_date).to(base_instance.end_date)
278
+ .and change{ bird_instance.reload.end_date }
279
+ .from(bird_instance.end_date).to(base_instance.end_date)
280
+ end
281
+ end
282
+ end
283
+
284
+ context 'when child record does not have end_date to update' do
285
+ let!(:cat_owner_instance) do
286
+ CatOwner.create(end_date: initial_end_date)
287
+ end
288
+ let!(:cat_instance) do
289
+ Cat.create(cat_owner: cat_owner_instance)
290
+ end
291
+ let(:cat_end_date) { nil }
292
+ let(:end_date) { Date.current }
293
+
294
+ before do
295
+ cat_owner_instance.end_date = end_date
296
+ end
297
+
298
+ it 'does not throw an error' do
299
+ expect(cat_instance).not_to respond_to(:end_date)
300
+ expect{ end_date_propagator.call }.not_to raise_error
301
+ end
302
+ end
303
+
304
+ context 'when the record already has a validation that would be added' do
305
+ let(:end_date) { Date.current }
306
+
307
+ before do
308
+ base_instance.save!
309
+ base_instance.end_date = end_date
310
+ base_instance.errors.add(:base, 'Child is bad')
311
+
312
+ child_instance.manual_invalidation = true
313
+ child_instance.save(validate: false)
314
+
315
+ # add the error that would be added by the child's propagation
316
+ base_instance.errors.add(
317
+ :base,
318
+ "Base could not propagate Emancipation date to Child:\nChild is bad",
319
+ )
320
+ end
321
+
322
+ it 'does not add the duplicate error' do
323
+ expect { end_date_propagator.call }
324
+ .not_to(change(base_instance, :errors))
325
+ end
326
+ end
327
+
328
+ context 'when a class is skipped' do
329
+ let(:end_date) { Date.current }
330
+ let(:skipped_classes) { [Tale] }
331
+
332
+ before do
333
+ base_instance
334
+ tale_instance.save!
335
+ base_instance.end_date = end_date
336
+ end
337
+
338
+ it 'does not propagate to that class' do
339
+ expect{ end_date_propagator.call }.not_to change{
340
+ tale_instance.reload.end_date }
341
+ end
342
+ end
343
+ end
344
+ end