acts_as_span 0.0.5 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -3
- data/.rspec +2 -0
- data/.tool-versions +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +0 -3
- data/README.rdoc +4 -16
- data/Rakefile +5 -1
- data/acts_as_span.gemspec +31 -14
- data/config/locales/en/acts_as_span.yml +15 -0
- data/lib/acts_as_span.rb +69 -98
- data/lib/acts_as_span/end_date_propagator.rb +198 -0
- data/lib/acts_as_span/no_overlap_validator.rb +86 -0
- data/lib/acts_as_span/span_instance.rb +24 -32
- data/lib/acts_as_span/span_instance/status.rb +12 -27
- data/lib/acts_as_span/span_instance/validations.rb +11 -53
- data/lib/acts_as_span/span_klass.rb +11 -19
- data/lib/acts_as_span/span_klass/status.rb +43 -12
- data/lib/acts_as_span/version.rb +6 -4
- data/lib/acts_as_span/within_parent_date_span_validator.rb +44 -0
- data/spec/lib/acts_as_span_spec.rb +38 -35
- data/spec/lib/delegation_spec.rb +45 -78
- data/spec/lib/end_date_propagator_spec.rb +319 -0
- data/spec/lib/no_overlap_validator_spec.rb +129 -0
- data/spec/lib/span_instance/named_scopes_on_spec.rb +193 -193
- data/spec/lib/span_instance/named_scopes_spec.rb +193 -191
- data/spec/lib/span_instance/overlap_spec.rb +193 -253
- data/spec/lib/span_instance/status_spec.rb +22 -35
- data/spec/lib/span_instance/validations_spec.rb +8 -44
- data/spec/lib/span_instance_spec.rb +17 -30
- data/spec/lib/span_klass/status_spec.rb +38 -0
- data/spec/lib/within_parent_date_span_validator_spec.rb +126 -0
- data/spec/spec_helper.rb +19 -6
- data/spec/spec_models.rb +226 -0
- metadata +167 -61
- data/Gemfile.lock +0 -47
- data/lib/acts_as_span/span_instance/overlap.rb +0 -17
- data/lib/acts_as_span/span_klass/overlap.rb +0 -21
- data/spec/lib/negative_spec.rb +0 -30
- data/spec/spec.opts +0 -1
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 59d6ed6e5f0b02d6d3683a99d62ac5f3b11ccdb5cf510f5bf1a3197e095102ff
|
4
|
+
data.tar.gz: 856975edeadab3d35def0a65e51bb6d65987fc2042cdec822d72b7c76741a9cb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cb1ae3e3c092a528e83d7dcb2fd9ca7d601b985eaa974b6eac1c1eb2634dc4630473dc5bfcafd548d3748a602569185cd803c2f7f6551c486dddcee1905e8205
|
7
|
+
data.tar.gz: 605657a4fb0114f18018b4aab29d9c2f9c63bf87321fe7c04ceaccb7974ad064dc4112ce3b7e5b44c8b541b65680e768e60ef54e8d43e1502ec59f27f46c797d
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.6.5
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
= acts_as_span
|
2
2
|
|
3
|
-
ActiveRecord model w/ a start_date and an end_date
|
3
|
+
ActiveRecord model w/ a start_date and an end_date == ActsAsSpan
|
4
|
+
|
5
|
+
Treat those date spans like the objects they are!
|
4
6
|
|
5
7
|
== Getting Started
|
6
8
|
|
@@ -14,20 +16,6 @@ In your model:
|
|
14
16
|
acts_as_span
|
15
17
|
end
|
16
18
|
|
17
|
-
In your migrations:
|
18
|
-
|
19
|
-
class AddSpanToSpanRecord < ActiveRecord::Migration
|
20
|
-
def self.up
|
21
|
-
add_column :span_records, :start_date, :date
|
22
|
-
add_column :span_records, :end_date, :date
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.down
|
26
|
-
remove_column :span_records, :start_date
|
27
|
-
remove_column :span_records, :end_date
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
19
|
== Copyright
|
32
20
|
|
33
|
-
Copyright (c) 2011 Annkissam. See LICENSE for details.
|
21
|
+
Copyright (c) 2011-2018 Annkissam. See LICENSE for details.
|
data/Rakefile
CHANGED
data/acts_as_span.gemspec
CHANGED
@@ -1,23 +1,40 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_Literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
require 'acts_as_span/version'
|
4
5
|
|
5
6
|
Gem::Specification.new do |s|
|
6
|
-
s.name =
|
7
|
+
s.name = 'acts_as_span'
|
7
8
|
s.version = ActsAsSpan::VERSION::STRING
|
8
|
-
s.authors = [
|
9
|
-
s.email = [
|
10
|
-
s.homepage =
|
9
|
+
s.authors = ['Eric Sullivan']
|
10
|
+
s.email = ['eric.sullivan@annkissam.com']
|
11
|
+
s.homepage = 'https://github.com/annkissam/acts_as_span'
|
11
12
|
s.summary = ActsAsSpan::VERSION::SUMMARY
|
12
|
-
s.description =
|
13
|
+
s.description = 'ActiveRecord model w/ a start_date and an end_date == ActsAsSpan'
|
14
|
+
s.license = 'MIT'
|
13
15
|
|
14
|
-
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
if s.respond_to?(:metadata)
|
19
|
+
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
20
|
+
else
|
21
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
22
|
+
'public gem pushes.'
|
23
|
+
end
|
15
24
|
|
16
25
|
s.files = `git ls-files`.split("\n")
|
17
26
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
-
s.require_paths = [
|
20
|
-
|
21
|
-
s.
|
22
|
-
s.
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
28
|
+
s.require_paths = %w[lib]
|
29
|
+
|
30
|
+
s.add_development_dependency 'bundler', '~> 2.1.4'
|
31
|
+
s.add_development_dependency 'has_siblings', '~> 0.2.7'
|
32
|
+
s.add_development_dependency 'pry-byebug'
|
33
|
+
s.add_development_dependency 'rake', '>= 12.3.3'
|
34
|
+
s.add_development_dependency 'rspec', '~> 3.0'
|
35
|
+
s.add_development_dependency 'sqlite3', '~> 1.4'
|
36
|
+
s.add_development_dependency 'temping'
|
37
|
+
|
38
|
+
s.add_runtime_dependency('activerecord', '>= 5.0.0')
|
39
|
+
s.add_runtime_dependency('activesupport', '>= 5.0.0')
|
23
40
|
end
|
@@ -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
@@ -1,161 +1,132 @@
|
|
1
|
-
require 'active_support'
|
2
1
|
require 'ostruct'
|
3
|
-
require '
|
2
|
+
require 'acts_as_span/version'
|
3
|
+
require 'acts_as_span/span_klass'
|
4
|
+
require 'acts_as_span/span_instance'
|
5
|
+
|
6
|
+
require 'acts_as_span/no_overlap_validator'
|
7
|
+
require 'acts_as_span/within_parent_date_span_validator'
|
4
8
|
|
5
|
-
|
9
|
+
require 'acts_as_span/end_date_propagator'
|
6
10
|
|
7
|
-
require
|
8
|
-
require
|
9
|
-
|
11
|
+
require 'active_support'
|
12
|
+
require 'active_record'
|
13
|
+
|
14
|
+
I18n.load_path += Dir[File.join(File.dirname(__dir__), 'config', 'locales', '**', 'acts_as_span.yml')]
|
10
15
|
|
11
16
|
module ActsAsSpan
|
12
17
|
extend ActiveSupport::Concern
|
13
|
-
|
18
|
+
|
19
|
+
OPTIONS = %i[start_field end_field name].freeze
|
20
|
+
|
14
21
|
class << self
|
15
22
|
def options
|
16
23
|
@options ||= {
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:start_date_field_required => false,
|
20
|
-
:end_date_field_required => false,
|
21
|
-
:span_overlap_scope => nil,
|
22
|
-
:span_overlap_count => nil,
|
24
|
+
:start_field => :start_date,
|
25
|
+
:end_field => :end_date,
|
23
26
|
:name => :default
|
24
27
|
}
|
25
28
|
end
|
26
|
-
|
29
|
+
|
27
30
|
def configure
|
28
31
|
yield(self) if block_given?
|
29
32
|
end
|
30
33
|
end
|
31
|
-
|
32
|
-
#by default, all model classess & their instances will return false w/ acts_as_span?
|
33
|
-
included do
|
34
|
-
self.send(:extend, ActsAsSpan::NegativeMethods)
|
35
|
-
self.send(:include, ActsAsSpan::NegativeMethods)
|
36
|
-
end
|
37
|
-
|
34
|
+
|
38
35
|
module ClassMethods
|
39
36
|
def acts_as_span(*args)
|
40
|
-
#this model & its instances will return true w/ acts_as_span?
|
41
|
-
self.send(:extend, ActsAsSpan::PositiveMethods)
|
42
|
-
self.send(:include, ActsAsSpan::PositiveMethods)
|
43
|
-
|
44
|
-
self.send(:extend, Forwardable)
|
45
|
-
|
46
37
|
self.send(:extend, ActsAsSpan::ExtendedClassMethods)
|
47
38
|
self.send(:include, ActsAsSpan::IncludedInstanceMethods)
|
48
|
-
|
39
|
+
|
40
|
+
# TODO: There's some refactoring that could be done here using keyword args (or the more standard old hash arg pattern)
|
49
41
|
options = OpenStruct.new(args.last.is_a?(Hash) ? ActsAsSpan.options.merge(args.pop) : ActsAsSpan.options)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
42
|
+
|
43
|
+
unsupported_options =
|
44
|
+
options.to_h.keys.reject { |opt| OPTIONS.include? opt }
|
45
|
+
unless unsupported_options.empty?
|
46
|
+
raise ArgumentError,
|
47
|
+
'Unsupported option(s): ' <<
|
48
|
+
unsupported_options.map { |o| "'#{o}'" }.join(', ')
|
49
|
+
end
|
50
|
+
|
54
51
|
acts_as_span_definitions[options.name] = options
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
52
|
+
|
53
|
+
# TODO add tests that check delegation of all methos in span
|
54
|
+
delegate :span_status,
|
55
|
+
:span_status_on,
|
56
|
+
:current?,
|
57
|
+
:current_on?,
|
58
|
+
:future?,
|
59
|
+
:future_on?,
|
60
|
+
:expired?,
|
61
|
+
:expired_on?,
|
62
|
+
:past?,
|
63
|
+
:past_on?, to: :span
|
64
|
+
|
65
|
+
delegate :acts_as_span_definitions, to: :class
|
66
|
+
|
67
|
+
# TODO idem above
|
71
68
|
class << self
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
69
|
+
delegate :current,
|
70
|
+
:current_on,
|
71
|
+
:future,
|
72
|
+
:future_on,
|
73
|
+
:expired,
|
74
|
+
:expired_on,
|
75
|
+
:past_on,
|
76
|
+
:past,
|
77
|
+
:current_or_future_on,
|
78
|
+
:current_or_future, to: :span
|
80
79
|
end
|
81
|
-
|
80
|
+
|
82
81
|
validate :validate_spans
|
83
82
|
end
|
84
|
-
|
83
|
+
|
85
84
|
def acts_as_span_definitions
|
86
85
|
@_acts_as_span_definitions ||= {}
|
87
86
|
end
|
88
87
|
end
|
89
|
-
|
88
|
+
|
90
89
|
module ExtendedClassMethods
|
91
|
-
def overlap(test_record)
|
92
|
-
overlap_for(test_record, :default, :default)
|
93
|
-
end
|
94
|
-
|
95
|
-
def overlap_for(test_record, test_record_span_name = :default, this_span_name = :default)
|
96
|
-
span_for(this_span_name).overlap(test_record.span_for(test_record_span_name))
|
97
|
-
end
|
98
|
-
|
99
90
|
def spans
|
100
91
|
acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
|
101
92
|
end
|
102
|
-
|
93
|
+
|
103
94
|
def span
|
104
95
|
span_for(:default)
|
105
96
|
end
|
106
|
-
|
97
|
+
|
107
98
|
def span_for(name = :default)
|
108
99
|
acts_as_span_klasses[name] ||= SpanKlass.new(name, self, acts_as_span_definitions[name])
|
109
100
|
end
|
110
|
-
|
101
|
+
|
111
102
|
def acts_as_span_klasses
|
112
103
|
@_acts_as_span_klasses ||= {}
|
113
104
|
end
|
114
105
|
end
|
115
|
-
|
106
|
+
|
116
107
|
module IncludedInstanceMethods
|
117
108
|
def spans
|
118
109
|
acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
|
119
110
|
end
|
120
|
-
|
111
|
+
|
121
112
|
def span
|
122
113
|
span_for(:default)
|
123
114
|
end
|
124
|
-
|
115
|
+
|
125
116
|
def span_for(name = :default)
|
126
117
|
acts_as_span_instances[name] ||= SpanInstance.new(name, self, acts_as_span_definitions[name])
|
127
118
|
end
|
128
|
-
|
119
|
+
|
129
120
|
def acts_as_span_instances
|
130
121
|
@_acts_as_span_instances ||= {}
|
131
122
|
end
|
132
|
-
|
123
|
+
|
133
124
|
def validate_spans
|
134
125
|
spans.each(&:validate)
|
135
126
|
end
|
136
|
-
|
137
|
-
#This syntax assumes :default span
|
138
|
-
def overlap?(other_record)
|
139
|
-
overlap_for?(other_record, :default, :default)
|
140
|
-
end
|
141
|
-
|
142
|
-
#record.span_for(:this_span_name).overlap?(other_record.span_for(:other_record_span_name))
|
143
|
-
def overlap_for?(other_record, this_span_name = :default, other_record_span_name = :default)
|
144
|
-
span_for(this_span_name).overlap?(other_record.span_for(other_record_span_name))
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
module PositiveMethods
|
149
|
-
send(:define_method, "acts_as_span?") do
|
150
|
-
true
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
module NegativeMethods
|
155
|
-
send(:define_method, "acts_as_span?") do
|
156
|
-
false
|
157
|
-
end
|
158
127
|
end
|
159
128
|
end
|
160
129
|
|
161
|
-
|
130
|
+
if Object.const_defined?("ActiveRecord")
|
131
|
+
ActiveRecord::Base.send(:include, ActsAsSpan)
|
132
|
+
end
|
@@ -0,0 +1,198 @@
|
|
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
|
+
:include_errors
|
75
|
+
|
76
|
+
def initialize(object, errors_cache: [], skipped_classes: [], include_errors: true)
|
77
|
+
@object = object
|
78
|
+
@errors_cache = errors_cache
|
79
|
+
@skipped_classes = skipped_classes
|
80
|
+
@include_errors = include_errors
|
81
|
+
end
|
82
|
+
|
83
|
+
# class-level call: enable the usage of ActsAsSpan::EndDatePropagator.call
|
84
|
+
def self.call(object, **opts)
|
85
|
+
new(object, opts).call
|
86
|
+
end
|
87
|
+
|
88
|
+
def call
|
89
|
+
result = propagate
|
90
|
+
# only add new errors to the object
|
91
|
+
result.errors.each do |error, message|
|
92
|
+
unless object.errors[error].include? message
|
93
|
+
object.errors.add(error, message: message)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
object
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def propagate
|
102
|
+
# return if there is nothing to propagate
|
103
|
+
return object unless should_propagate_from? object
|
104
|
+
|
105
|
+
children(object).each do |child|
|
106
|
+
# End the record, its children too. And their children, forever, true.
|
107
|
+
propagated_child = assign_end_date(child, object.span.end_date)
|
108
|
+
|
109
|
+
# save child and add errors to cache
|
110
|
+
save_with_errors(object, child, propagated_child)
|
111
|
+
end
|
112
|
+
|
113
|
+
if errors_cache.present?
|
114
|
+
errors_cache.each do |message|
|
115
|
+
skip if object.errors.added?(:base, message)
|
116
|
+
|
117
|
+
object.errors.add(:base, message: message)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# return the object, with any newly-added errors
|
122
|
+
object
|
123
|
+
end
|
124
|
+
|
125
|
+
# returns the given child, but possibly with errors
|
126
|
+
def assign_end_date(child, new_end_date)
|
127
|
+
child.assign_attributes({ child.span.end_field => new_end_date })
|
128
|
+
ActsAsSpan::EndDatePropagator.call(
|
129
|
+
child,
|
130
|
+
errors_cache: errors_cache,
|
131
|
+
skipped_classes: skipped_classes,
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
# save the child record, add errors.
|
136
|
+
def save_with_errors(object, child, propagated_child)
|
137
|
+
if object_has_errors?(propagated_child) && include_errors
|
138
|
+
errors_cache << propagation_error_message(object, child)
|
139
|
+
end
|
140
|
+
child.save
|
141
|
+
end
|
142
|
+
|
143
|
+
def propagation_error_message(object, child)
|
144
|
+
I18n.t(
|
145
|
+
'propagation_failure',
|
146
|
+
scope: %i[activerecord errors messages end_date_propagator],
|
147
|
+
end_date_field_name: child.class.human_attribute_name(
|
148
|
+
child.span.end_field,
|
149
|
+
),
|
150
|
+
parent: object.model_name.human,
|
151
|
+
child: child.model_name.human,
|
152
|
+
reason: child.errors.full_messages.join('; '),
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
def object_has_errors?(object)
|
157
|
+
!object.valid? ||
|
158
|
+
(object.errors.present? && object.errors.messages.values.flatten.any?)
|
159
|
+
end
|
160
|
+
|
161
|
+
# check if the end_date analog is dirtied
|
162
|
+
def end_date_changed?(object)
|
163
|
+
end_date_field = object.span.end_field.to_s
|
164
|
+
object.changed.include? end_date_field
|
165
|
+
end
|
166
|
+
|
167
|
+
def should_propagate_from?(object)
|
168
|
+
object.respond_to?(:span) &&
|
169
|
+
end_date_changed?(object) &&
|
170
|
+
!object.span.end_date.nil?
|
171
|
+
end
|
172
|
+
|
173
|
+
# Use acts_as_span to determine whether a record has an end date
|
174
|
+
def should_propagate_to?(klass)
|
175
|
+
klass.respond_to?(:span) && @skipped_classes.exclude?(klass)
|
176
|
+
end
|
177
|
+
|
178
|
+
def child_associations(object)
|
179
|
+
object.class.reflect_on_all_associations(:has_many).select do |reflection|
|
180
|
+
%i[delete destroy].include?(reflection.options[:dependent]) &&
|
181
|
+
should_propagate_to?(reflection.klass)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def children(object)
|
186
|
+
child_objects = child_associations(object).flat_map do |reflection|
|
187
|
+
object.send(reflection.name)
|
188
|
+
end
|
189
|
+
|
190
|
+
# skip previously-ended children
|
191
|
+
child_objects.reject do |child|
|
192
|
+
child.span.end_date && child.span.end_date < object.span.end_date
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
attr_writer :object, :errors_cache
|
197
|
+
end
|
198
|
+
end
|