acts_as_span 1.0.0 → 1.1.0

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