acts_as_span 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1b2bd958d53e10e1816b39bbb073c8b9d21b105ab20b7bdd1d0f11048f2adf7
4
- data.tar.gz: 9f211ee95efdcbad216591259729983fbf31fc26f6ce91d39fdd03be258d0c0e
3
+ metadata.gz: 57404b0d110113b5f9c52537aa90970a560d69f4a5b7d5d318caacc229157679
4
+ data.tar.gz: 5702fafe912d1bc56b633aec32dd2ae76da9f886608c182eed29c0ca7d2ed509
5
5
  SHA512:
6
- metadata.gz: aa5735e3051308f569cee93a81b13b833c33c1611feaacc0cfc2b985f1938dbaac923676bcd5e0fc75ed91f428651a3b2e10ff2a4ed48981e6191acc60494bd1
7
- data.tar.gz: 2fc6f890b396e9829559aa063f6bdd6dfb21b1afc0806a83dc1114acbc736af6ce0cf6dec2c319a259ad1adfdbd52657f81c2adc439b32a3d05c4844f179b7f5
6
+ metadata.gz: b1e7265d3cfc7433b331f407fe991eb5549091e10c5dcef0298468099f1769dfaca71a7355363af8f6f88c381362d518fa494366d3f1dead8de8072458d95b2c
7
+ data.tar.gz: 6304cb8600c553e60bc7e470ed4f87e6cc145a4239243f5109556278f76d88b24d1624bbc904e499d7ce95d7196967d4d8822c962571c9184c875dbd20d6d57f
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 2.6.3
data/acts_as_span.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.add_development_dependency "bundler", "~> 2.0.1"
30
30
  s.add_development_dependency "rake", "~> 10.0"
31
31
  s.add_development_dependency "rspec", "~> 3.0"
32
- s.add_development_dependency "sqlite3", "~> 1.3.6"
32
+ s.add_development_dependency "sqlite3", "~> 1.4"
33
33
  s.add_development_dependency "has_siblings", "~> 0.2.7"
34
34
  s.add_development_dependency "temping"
35
35
  s.add_development_dependency "pry-byebug"
@@ -0,0 +1,15 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ end_date_propagator:
6
+ propagation_failure: "%{parent} could not propagate
7
+ %{end_date_field_name} to %{child}:\n%{reason}"
8
+ # TODO: let pluralize handle pluralization
9
+ no_overlap:
10
+ one: "A %{model_name} already exists between
11
+ %{start_date} - %{end_date}: %{overlapping_records_s}"
12
+ other: "%{count} %{model_name_plural} already exist between
13
+ %{start_date} - %{end_date}: %{overlapping_records_s}"
14
+ not_within_parent_date_span: Must exist within the %{parent} date span
15
+ start_date_after_end_date: Must be on or after %{start_field}
data/lib/acts_as_span.rb CHANGED
@@ -6,10 +6,13 @@ require 'acts_as_span/span_instance'
6
6
  require 'acts_as_span/no_overlap_validator'
7
7
  require 'acts_as_span/within_parent_date_span_validator'
8
8
 
9
+ require 'acts_as_span/end_date_propagator'
9
10
 
10
11
  require 'active_support'
11
12
  require 'active_record'
12
13
 
14
+ I18n.load_path += Dir[File.join(File.dirname(__dir__), 'config', 'locales', '**', 'acts_as_span.yml')]
15
+
13
16
  module ActsAsSpan
14
17
  extend ActiveSupport::Concern
15
18
 
@@ -0,0 +1,190 @@
1
+ # frozen_string_Literal: true
2
+
3
+ module ActsAsSpan
4
+ # # End Date Propagator
5
+ #
6
+ # When editing the `end_date` of a record, the record's children often also
7
+ # need to be updated. This propagator takes care of that.
8
+ # For each of the child records (defined below in the function `children`),
9
+ # the child record's `end_date` is updated to match that of the original
10
+ # object. The function `propagate` is recursive, propagating to
11
+ # children of children and so on.
12
+ # Records that should not have their end dates propagated in this manner
13
+ # (e.g. StatusRecords) are manually excluded in `skipped_classes`.
14
+ # If there is some error preventing propagation, the child record is NOT saved
15
+ # and that error message is added to the object's `errors`. These errors
16
+ # propagate upwards into a flattened array of error messages.
17
+ #
18
+ # This class uses its own definition of 'child' for an object. For a given
19
+ # object, the objects the propagator considers its children are:
20
+ # * Associated via `has_many` association
21
+ # * Association `:dependent` option is `:delete` or `:destroy`
22
+ # * acts_as_span (checked via `respond_to?(:span)`)
23
+ # * Not blacklisted via `skipped_classes` array
24
+ #
25
+ # The return value for `call` is the given object, updated to have children's
26
+ # errors added to its `:base` errors if any children had errors.
27
+ #
28
+ # ## Usage:
29
+ #
30
+ # Propagate end dates for an object that acts_as_span and has propagatable
31
+ # children to all propagatable children:
32
+ # ```
33
+ # ActsAsSpan::EndDatePropagator.call(object)
34
+ # ```
35
+ #
36
+ # To propagate to a subset of its propagatable children:
37
+ # ```
38
+ # ActsAsSpan::EndDatePropagator.call(
39
+ # object, skipped_classes: [ClassOne, ClassTwo]
40
+ # )
41
+ # ```
42
+ # ... where ClassOne and ClassTwo are the classes to be excluded.
43
+ #
44
+ # The EndDatePropagator does not use transactions. If the propagation should
45
+ # be run in a transaction, wrap the call in one like so:
46
+ # ```
47
+ # ActiveRecord::Base.transaction do
48
+ # ActsAsSpan::EndDatePropagator.call(
49
+ # obj, skipped_classes: [ClassOne, ClassTwo]
50
+ # )
51
+ # end
52
+ # ```
53
+ #
54
+ # One use case for the transaction wrapper would be to not follow through
55
+ # with propagation if the object has errors:
56
+ # ```
57
+ # ActiveRecord::Base.transaction do
58
+ # result = ActsAsSpan::EndDatePropagator.call(obj)
59
+ # if result.errors.present?
60
+ # fail OhNoMyObjetHasErrorsError, "Oh, no! My object has errors!"
61
+ # end
62
+ # end
63
+ # ```
64
+ #
65
+ # Currently only propagates "default" span. The approach to implementing such
66
+ # a feature is ambiguous - would all children have the same span propagated?
67
+ # Would each acts_as_span model need a method to tell which span to
68
+ # propagate to? Once there is a solid use case for using this object on
69
+ # models with multiple spans, that will inform the implementation strategy.
70
+ class EndDatePropagator
71
+ attr_reader :object,
72
+ :errors_cache,
73
+ :skipped_classes
74
+
75
+ def initialize(object, errors_cache: [], skipped_classes: [])
76
+ @object = object
77
+ @errors_cache = errors_cache
78
+ @skipped_classes = skipped_classes
79
+ end
80
+
81
+ # class-level call: enable the usage of ActsAsSpan::EndDatePropagator.call
82
+ def self.call(object, **opts)
83
+ new(object, opts).call
84
+ end
85
+
86
+ def call
87
+ result = propagate
88
+ # only add new errors to the object
89
+ result.errors.each do |error, message|
90
+ unless object.errors[error].include? message
91
+ object.errors[error] << message
92
+ end
93
+ end
94
+ object
95
+ end
96
+
97
+ private
98
+
99
+ def propagate
100
+ # return if there is nothing to propagate
101
+ return object unless should_propagate_from? object
102
+
103
+ children(object).each do |child|
104
+ # End the record, its children too. And their children, forever, true.
105
+ propagated_child = assign_end_date(child, object.span.end_date)
106
+
107
+ # save child and add errors to cache
108
+ save_with_errors(object, child, propagated_child)
109
+ end
110
+
111
+ # add just the strings, prevent ugly nested arrays in the view
112
+ object.errors[:base].push(*errors_cache.flatten)
113
+
114
+ # return the object, with any newly-added errors
115
+ object
116
+ end
117
+
118
+ # returns the given child, but possibly with errors
119
+ def assign_end_date(child, new_end_date)
120
+ child.assign_attributes({ child.span.end_field => new_end_date })
121
+ ActsAsSpan::EndDatePropagator.call(
122
+ child, errors_cache: errors_cache, skipped_classes: skipped_classes
123
+ )
124
+ end
125
+
126
+ # save the child record, add errors.
127
+ def save_with_errors(object, child, propagated_child)
128
+ if object_has_errors?(propagated_child)
129
+ errors_cache << propagation_error_message(object, child)
130
+ end
131
+ child.save
132
+ end
133
+
134
+ def propagation_error_message(object, child)
135
+ I18n.t(
136
+ 'propagation_failure',
137
+ scope: %i[activerecord errors messages end_date_propagator],
138
+ end_date_field_name: child.class.human_attribute_name(
139
+ child.span.end_field
140
+ ),
141
+ parent: object.model_name.human,
142
+ child: child.model_name.human,
143
+ reason: child.errors.full_messages.join('; ')
144
+ )
145
+ end
146
+
147
+ def object_has_errors?(object)
148
+ !object.valid? ||
149
+ (object.errors.present? && object.errors.messages.values.flatten.any?)
150
+ end
151
+
152
+ # check if the end_date analog is dirtied
153
+ def end_date_changed?(object)
154
+ end_date_field = object.span.end_field.to_s
155
+ object.changed.include? end_date_field
156
+ end
157
+
158
+ def should_propagate_from?(object)
159
+ object.respond_to?(:span) &&
160
+ end_date_changed?(object) &&
161
+ !object.span.end_date.nil?
162
+ end
163
+
164
+ # Use acts_as_span to determine whether a record has an end date
165
+ def should_propagate_to?(klass)
166
+ klass.respond_to?(:span) &&
167
+ @skipped_classes.exclude?(klass)
168
+ end
169
+
170
+ def child_associations(object)
171
+ object.class.reflect_on_all_associations(:has_many).select do |reflection|
172
+ %i[delete destroy].include?(reflection.options[:dependent]) &&
173
+ should_propagate_to?(reflection.klass)
174
+ end
175
+ end
176
+
177
+ def children(object)
178
+ child_objects = child_associations(object).flat_map do |reflection|
179
+ object.send(reflection.name)
180
+ end
181
+
182
+ # skip previously-ended children
183
+ child_objects.reject do |child|
184
+ child.span.end_date && child.span.end_date < object.span.end_date
185
+ end
186
+ end
187
+
188
+ attr_writer :object, :errors_cache
189
+ end
190
+ end
@@ -4,19 +4,21 @@ module ActsAsSpan
4
4
  class NoOverlapValidator < ActiveModel::Validator
5
5
  def validate(record)
6
6
  overlapping_records = temporally_overlapping_for(record)
7
- instance_scope = options[:instance_scope].is_a?(Proc) ? record.instance_eval(&options[:instance_scope]) : true
7
+ instance_scope = if options[:instance_scope].is_a? Proc
8
+ record.instance_eval&options[:instance_scope]
9
+ else
10
+ true
11
+ end
8
12
 
9
13
  if overlapping_records.any? && instance_scope
10
14
 
11
- error_type = overlapping_records.size == 1 ? "no_overlap.one" : "no_overlap.other"
12
-
13
15
  record.errors.add(
14
16
  :base,
15
- error_type.to_sym,
17
+ :no_overlap,
16
18
  model_name: record.class.model_name.human,
17
19
  model_name_plural: record.class.model_name.plural.humanize,
18
- start_date: record.start_date,
19
- end_date: record.end_date,
20
+ start_date: record.span.start_date,
21
+ end_date: record.span.end_date,
20
22
  count: overlapping_records.size,
21
23
  overlapping_records_s: overlapping_records.join(",")
22
24
  )
@@ -24,26 +26,26 @@ module ActsAsSpan
24
26
  end
25
27
 
26
28
  #TODO add back condition for start_date nil
27
- #TODO add configuration for span configuration
29
+ #TODO add support for multiple spans (currently only checks :default)
28
30
  def temporally_overlapping_for(record)
29
31
  scope = record.instance_eval(&options[:scope])
30
32
 
31
- start_date = record.start_date || Date.current
32
- end_date = record.end_date
33
+ start_date = record.span.start_date || Date.current
34
+ end_date = record.span.end_date
33
35
  arel_table = record.class.arel_table
34
36
 
35
37
  if end_date
36
38
  scope.where(
37
- arel_table[:start_date].lteq(end_date).
38
- and(
39
- arel_table[:end_date].gteq(start_date).
40
- or(arel_table[:end_date].eq(nil))
39
+ arel_table[record.span.start_field].lteq(end_date)
40
+ .and(
41
+ arel_table[record.span.end_field].gteq(start_date)
42
+ .or(arel_table[record.span.end_field].eq(nil))
41
43
  )
42
44
  )
43
45
  else
44
46
  scope.where(
45
- arel_table[:end_date].gteq(start_date).
46
- or(arel_table[:end_date].eq(nil))
47
+ arel_table[record.span.end_field].gteq(start_date)
48
+ .or(arel_table[record.span.end_field].eq(nil))
47
49
  )
48
50
  end
49
51
  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
@@ -1,7 +1,7 @@
1
1
  module ActsAsSpan
2
2
  module VERSION
3
3
  MAJOR = 1
4
- MINOR = 0
4
+ MINOR = 1
5
5
  TINY = 0
6
6
  PRE = nil
7
7
 
@@ -22,21 +22,21 @@ module ActsAsSpan
22
22
  private
23
23
 
24
24
  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
25
+ record.span.start_date.present? && parent.span.start_date.present? &&
26
+ record.span.start_date < parent.span.start_date
27
27
  end
28
28
 
29
29
  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
30
+ record.span.end_date.present? && parent.span.end_date.present? &&
31
+ record.span.end_date > parent.span.end_date
32
32
  end
33
33
 
34
34
  def child_record_without_start_date(record, parent)
35
- record.start_date.nil? && parent.start_date.present?
35
+ record.span.start_date.nil? && parent.span.start_date.present?
36
36
  end
37
37
 
38
38
  def child_record_without_end_date(record, parent)
39
- record.end_date.nil? && parent.end_date.present?
39
+ record.span.end_date.nil? && parent.span.end_date.present?
40
40
  end
41
41
  end
42
42
  end
@@ -0,0 +1,306 @@
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
+ )
8
+ end
9
+
10
+ let(:base_instance) do
11
+ Base.create(end_date: initial_end_date)
12
+ end
13
+ let(:initial_end_date) { nil }
14
+
15
+ let(:other_base_instance) do
16
+ OtherBase.create(end_date: other_end_date)
17
+ end
18
+ let(:other_end_date) { nil }
19
+
20
+ let(:skipped_classes) { [] }
21
+
22
+ let!(:child_instance) do
23
+ Child.create(
24
+ base: base_instance,
25
+ emancipation_date: child_end_date
26
+ )
27
+ end
28
+ let(:child_end_date) { nil }
29
+
30
+ let!(:dog_instance) do
31
+ Dog.create(
32
+ base: base_instance,
33
+ end_date: dog_end_date
34
+ )
35
+ end
36
+ let(:dog_end_date) { nil }
37
+
38
+ let!(:bird_instance) do
39
+ Bird.create(
40
+ child: child_instance,
41
+ end_date: child_end_date
42
+ )
43
+ end
44
+ let(:bird_end_date) { nil }
45
+
46
+ let(:tale_instance) do
47
+ base_instance.tales.create(start_date: Date.current, end_date: nil)
48
+ end
49
+
50
+ describe '@errors_cache' do
51
+ let(:base_start_date) { Date.current - 7 }
52
+ let(:initial_end_date) { nil }
53
+ let(:end_date) { Date.current }
54
+
55
+ let(:child_start_date) { base_start_date + 1 }
56
+ let!(:child_instance) do
57
+ Child.create(
58
+ base: base_instance,
59
+ date_of_birth: child_start_date,
60
+ emancipation_date: child_end_date
61
+ )
62
+ end
63
+ let(:bird_start_date) { child_start_date + 1 }
64
+ let!(:bird_instance) do
65
+ Bird.create(
66
+ child: child_instance,
67
+ start_date: bird_start_date,
68
+ end_date: bird_end_date
69
+ )
70
+ end
71
+
72
+ before do
73
+ base_instance.start_date = base_start_date
74
+ base_instance.save!
75
+ base_instance.end_date = end_date
76
+ end
77
+
78
+ context 'when all child records are successfully saved' do
79
+ it 'the parent record does not have any errors' do
80
+ expect(
81
+ end_date_propagator.call.errors.full_messages
82
+ ).to be_empty
83
+ end
84
+ end
85
+
86
+ context 'when one grandchild record is not valid' do
87
+ before do
88
+ bird_instance.start_date = child_start_date - 1
89
+ bird_instance.save(validate: false)
90
+ end
91
+ it "the parent shows that grandchild's errors" do
92
+ expect(
93
+ end_date_propagator.call.errors.values.join
94
+ ).to include(
95
+ I18n.t(
96
+ 'not_within_parent_date_span',
97
+ parent: 'Child',
98
+ scope: %i[activerecord errors messages]
99
+ )
100
+ )
101
+ end
102
+ end
103
+
104
+ context 'when multiple child records are not valid' do
105
+ before do
106
+ child_instance.date_of_birth = base_instance.span.start_date - 1
107
+ child_instance.save(validate: false)
108
+ bird_instance.start_date = child_instance.span.start_date - 1
109
+ bird_instance.save(validate: false)
110
+ end
111
+ it "the parent gains all children's errors" do
112
+ expect(
113
+ end_date_propagator.call.errors.values.join
114
+ ).to include(
115
+ I18n.t(
116
+ 'not_within_parent_date_span',
117
+ parent: 'Child',
118
+ scope: %i[activerecord errors messages]
119
+ )
120
+ ).and include(
121
+ I18n.t(
122
+ 'not_within_parent_date_span',
123
+ parent: 'Base',
124
+ scope: %i[activerecord errors messages]
125
+ )
126
+ )
127
+ end
128
+ end
129
+ end
130
+
131
+ describe '.call' do
132
+ subject(:result) do
133
+ ActsAsSpan::EndDatePropagator.call(obj, call_options)
134
+ end
135
+ let(:obj) { base_instance }
136
+
137
+ context 'when no skipped classes are passed' do
138
+ let(:call_options) { {} }
139
+
140
+ it 'forwards the correct arguments to :new' do
141
+ expect(ActsAsSpan::EndDatePropagator)
142
+ .to receive(:new).with(obj, call_options).and_call_original
143
+ expect(result).to eq(obj)
144
+ end
145
+ end
146
+
147
+ context 'when skipped classes are passed' do
148
+ let(:call_options) { { skipped_classes: ['bungus'] } }
149
+
150
+ it 'forwards the correct arguments to :new' do
151
+ expect(ActsAsSpan::EndDatePropagator)
152
+ .to receive(:new).with(obj, call_options).and_call_original
153
+ expect(result).to eq(obj)
154
+ end
155
+ end
156
+ end
157
+
158
+ describe '#call' do
159
+ context 'without an end_date' do
160
+ let(:object_instance) { SpannableModel.new }
161
+
162
+ it 'does not raise an error' do
163
+ expect do
164
+ ActsAsSpan::EndDatePropagator.new(object_instance).call
165
+ end.not_to raise_error
166
+ end
167
+ end
168
+
169
+ context 'updates children' do
170
+ before do
171
+ base_instance
172
+ base_instance.end_date = end_date
173
+ end
174
+
175
+ context 'base_instance.end_date nil -> !nil' do
176
+ let(:initial_end_date) { nil }
177
+ let(:end_date) { Date.current }
178
+
179
+ context 'child_end_date == initial_end_date' do
180
+ let(:child_end_date) { initial_end_date }
181
+
182
+ it 'propagates to the child_instance' do
183
+ expect{ end_date_propagator.call }.to change{
184
+ child_instance.reload.emancipation_date }
185
+ .from(child_end_date).to(base_instance.end_date)
186
+ end
187
+ end
188
+
189
+ context 'child_end_date >= initial_end_date' do
190
+ let(:child_end_date) { end_date + 3 }
191
+
192
+ it 'propagates to the child_instance' do
193
+ expect{ end_date_propagator.call }.to change{
194
+ child_instance.reload.emancipation_date}
195
+ .from(child_end_date).to(base_instance.end_date)
196
+ end
197
+ end
198
+
199
+ context 'child_end_date <= initial_end_date' do
200
+ let(:child_end_date) { end_date - 3 }
201
+
202
+ it 'does not propagate to the child_instance' do
203
+ expect{ end_date_propagator.call }.not_to change{
204
+ child_instance.reload.emancipation_date}
205
+ end
206
+ end
207
+
208
+ context 'when a child cannot have its end date updated' do
209
+ before do
210
+ # add a "within parent date span" error to child
211
+ base_instance.start_date = Date.current - 1
212
+ child_instance.date_of_birth = Date.current - 2
213
+ child_instance.save(validate: false)
214
+ end
215
+
216
+ it "the parent's end date is not updated" do
217
+ expect{ end_date_propagator.call }.to change{
218
+ base_instance.errors[:base]
219
+ }.from([])
220
+ end
221
+
222
+ context 'and the child is the child of a child' do
223
+ before do
224
+ end
225
+
226
+ it "the parent's end date is not updated" do
227
+ expect{ end_date_propagator.call }.not_to change{
228
+ base_instance.reload.end_date
229
+ }
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ context 'base_instance.end_date !nil -> nil' do
236
+ let(:initial_end_date) { Date.current }
237
+ let(:end_date) { nil }
238
+ let(:child_end_date) { initial_end_date }
239
+
240
+ it 'does not propagate to the child_instance' do
241
+ expect{ end_date_propagator.call }.not_to change{
242
+ child_instance.reload.emancipation_date }
243
+ end
244
+ end
245
+
246
+ context 'base_instance.end_date not changed' do
247
+ let(:end_date) { initial_end_date }
248
+
249
+ it 'does not propagate to the child_instance' do
250
+ expect{ end_date_propagator.call }.not_to change{
251
+ child_instance.reload.emancipation_date }
252
+ end
253
+ end
254
+
255
+ context 'has access to all children via has_many associations' do
256
+ let(:end_date) { Date.current }
257
+
258
+ it 'changes the end_date of all child associations' do
259
+ expect{ end_date_propagator.call }.to change{
260
+ child_instance.reload.emancipation_date }.
261
+ from(child_instance.emancipation_date).to(base_instance.end_date)
262
+ .and change{ dog_instance.reload.end_date }
263
+ .from(dog_instance.end_date).to(base_instance.end_date)
264
+ .and change{ bird_instance.reload.end_date }
265
+ .from(bird_instance.end_date).to(base_instance.end_date)
266
+ end
267
+ end
268
+ end
269
+
270
+ context 'when child record does not have end_date to update' do
271
+ let!(:cat_owner_instance) do
272
+ CatOwner.create(end_date: initial_end_date)
273
+ end
274
+ let!(:cat_instance) do
275
+ Cat.create(cat_owner: cat_owner_instance)
276
+ end
277
+ let(:cat_end_date) { nil }
278
+ let(:end_date) { Date.current }
279
+
280
+ before do
281
+ cat_owner_instance.end_date = end_date
282
+ end
283
+
284
+ it 'does not throw an error' do
285
+ expect(cat_instance).not_to respond_to(:end_date)
286
+ expect{ end_date_propagator.call }.not_to raise_error
287
+ end
288
+ end
289
+
290
+ context 'when a class is skipped' do
291
+ let(:end_date) { Date.current }
292
+ let(:skipped_classes) { [Tale] }
293
+
294
+ before do
295
+ base_instance
296
+ tale_instance.save!
297
+ base_instance.end_date = end_date
298
+ end
299
+
300
+ it 'does not propagate to that class' do
301
+ expect{ end_date_propagator.call }.not_to change{
302
+ tale_instance.reload.end_date }
303
+ end
304
+ end
305
+ end
306
+ end
@@ -69,7 +69,7 @@ RSpec.describe ActsAsSpan::NoOverlapValidator do
69
69
 
70
70
  before do
71
71
  all_siblings.each do |sibling|
72
- sibling.update_attributes(papa: papa)
72
+ sibling.update(papa: papa)
73
73
  end
74
74
 
75
75
  new_child.papa = papa
data/spec/spec_models.rb CHANGED
@@ -64,6 +64,8 @@ Temping.create :mama do
64
64
  t.date :end_date
65
65
  end
66
66
 
67
+ acts_as_span
68
+
67
69
  has_many :one_parent_children
68
70
  has_many :two_parent_children
69
71
  end
@@ -74,5 +76,116 @@ Temping.create :papa do
74
76
  t.date :end_date
75
77
  end
76
78
 
79
+ acts_as_span
80
+
77
81
  has_many :one_parent_children
78
82
  end
83
+
84
+ # fulfill association requirements for EndDatePropagator
85
+ Temping.create :base do
86
+ has_many :children, dependent: :destroy
87
+ has_many :dogs, dependent: :destroy
88
+ has_many :birds, through: :children
89
+ has_many :tales, dependent: :destroy
90
+
91
+ acts_as_span
92
+
93
+ with_columns do |t|
94
+ t.date :end_date
95
+ t.date :start_date
96
+ end
97
+ end
98
+
99
+ Temping.create :cat_owner do
100
+ has_many :cats, dependent: :destroy
101
+
102
+ acts_as_span
103
+
104
+ with_columns do |t|
105
+ t.date :start_date
106
+ t.date :end_date
107
+ end
108
+ end
109
+
110
+ Temping.create :cat do
111
+ belongs_to :cat_owner
112
+
113
+ with_columns do |t|
114
+ t.belongs_to :cat_owner
115
+ end
116
+ end
117
+
118
+ Temping.create :other_base do
119
+ has_many :children, dependent: :destroy
120
+
121
+ acts_as_span
122
+
123
+ with_columns do |t|
124
+ t.date :end_date
125
+ t.date :start_date
126
+ end
127
+ end
128
+
129
+ # has non-standard start_ and end_field names
130
+ Temping.create :child do
131
+ belongs_to :base
132
+ belongs_to :other_base
133
+ has_many :birds, dependent: :destroy
134
+
135
+ validates_with ActsAsSpan::WithinParentDateSpanValidator,
136
+ parents: [:base]
137
+
138
+ acts_as_span(
139
+ start_field: :date_of_birth,
140
+ end_field: :emancipation_date
141
+ )
142
+
143
+ with_columns do |t|
144
+ t.date :date_of_birth
145
+ t.date :emancipation_date
146
+ t.string :manual_invalidation
147
+ t.belongs_to :base
148
+ end
149
+ end
150
+
151
+ Temping.create :dog do
152
+ belongs_to :base
153
+
154
+ acts_as_span
155
+
156
+ with_columns do |t|
157
+ t.date :start_date
158
+ t.date :end_date
159
+ t.belongs_to :base
160
+ end
161
+ end
162
+
163
+
164
+ Temping.create :bird do
165
+ belongs_to :child
166
+
167
+ validates_with ActsAsSpan::WithinParentDateSpanValidator,
168
+ parents: [:child]
169
+
170
+ acts_as_span
171
+
172
+ with_columns do |t|
173
+ t.date :end_date
174
+ t.date :start_date
175
+ t.belongs_to :child
176
+ end
177
+ end
178
+
179
+ Temping.create :tale do
180
+ belongs_to :base
181
+
182
+ acts_as_span
183
+
184
+ with_columns do |t|
185
+ t.date :end_date
186
+ t.date :start_date
187
+ t.belongs_to :base
188
+ end
189
+ end
190
+
191
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_span
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Sullivan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-12 00:00:00.000000000 Z
11
+ date: 2020-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 1.3.6
61
+ version: '1.4'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 1.3.6
68
+ version: '1.4'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: has_siblings
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -145,13 +145,16 @@ extra_rdoc_files: []
145
145
  files:
146
146
  - ".gitignore"
147
147
  - ".rspec"
148
+ - ".tool-versions"
148
149
  - ".travis.yml"
149
150
  - Gemfile
150
151
  - LICENSE
151
152
  - README.rdoc
152
153
  - Rakefile
153
154
  - acts_as_span.gemspec
155
+ - config/locales/en/acts_as_span.yml
154
156
  - lib/acts_as_span.rb
157
+ - lib/acts_as_span/end_date_propagator.rb
155
158
  - lib/acts_as_span/no_overlap_validator.rb
156
159
  - lib/acts_as_span/span_instance.rb
157
160
  - lib/acts_as_span/span_instance/status.rb
@@ -162,6 +165,7 @@ files:
162
165
  - lib/acts_as_span/within_parent_date_span_validator.rb
163
166
  - spec/lib/acts_as_span_spec.rb
164
167
  - spec/lib/delegation_spec.rb
168
+ - spec/lib/end_date_propagator_spec.rb
165
169
  - spec/lib/no_overlap_validator_spec.rb
166
170
  - spec/lib/span_instance/named_scopes_on_spec.rb
167
171
  - spec/lib/span_instance/named_scopes_spec.rb
@@ -192,9 +196,21 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
196
  - !ruby/object:Gem::Version
193
197
  version: '0'
194
198
  requirements: []
195
- rubyforge_project:
196
- rubygems_version: 2.7.6
199
+ rubygems_version: 3.1.2
197
200
  signing_key:
198
201
  specification_version: 4
199
- summary: acts_as_span 1.0.0
200
- test_files: []
202
+ summary: acts_as_span 1.1.0
203
+ test_files:
204
+ - spec/lib/acts_as_span_spec.rb
205
+ - spec/lib/delegation_spec.rb
206
+ - spec/lib/end_date_propagator_spec.rb
207
+ - spec/lib/no_overlap_validator_spec.rb
208
+ - spec/lib/span_instance/named_scopes_on_spec.rb
209
+ - spec/lib/span_instance/named_scopes_spec.rb
210
+ - spec/lib/span_instance/overlap_spec.rb
211
+ - spec/lib/span_instance/status_spec.rb
212
+ - spec/lib/span_instance/validations_spec.rb
213
+ - spec/lib/span_instance_spec.rb
214
+ - spec/lib/within_parent_date_span_validator_spec.rb
215
+ - spec/spec_helper.rb
216
+ - spec/spec_models.rb