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.
@@ -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