rails_ops 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +84 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1216 -0
- data/RUBY_VERSION +1 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/rails_ops.rb +96 -0
- data/lib/rails_ops/authorization_backend/abstract.rb +7 -0
- data/lib/rails_ops/authorization_backend/can_can_can.rb +14 -0
- data/lib/rails_ops/configuration.rb +4 -0
- data/lib/rails_ops/context.rb +35 -0
- data/lib/rails_ops/controller_mixin.rb +105 -0
- data/lib/rails_ops/exceptions.rb +19 -0
- data/lib/rails_ops/hooked_job.rb +25 -0
- data/lib/rails_ops/hookup.rb +80 -0
- data/lib/rails_ops/hookup/dsl.rb +29 -0
- data/lib/rails_ops/hookup/dsl_validator.rb +45 -0
- data/lib/rails_ops/hookup/hook.rb +11 -0
- data/lib/rails_ops/log_subscriber.rb +24 -0
- data/lib/rails_ops/mixins.rb +2 -0
- data/lib/rails_ops/mixins/authorization.rb +83 -0
- data/lib/rails_ops/mixins/log_settings.rb +20 -0
- data/lib/rails_ops/mixins/model.rb +4 -0
- data/lib/rails_ops/mixins/model/authorization.rb +64 -0
- data/lib/rails_ops/mixins/model/nesting.rb +180 -0
- data/lib/rails_ops/mixins/policies.rb +42 -0
- data/lib/rails_ops/mixins/require_context.rb +33 -0
- data/lib/rails_ops/mixins/routes.rb +35 -0
- data/lib/rails_ops/mixins/schema_validation.rb +25 -0
- data/lib/rails_ops/mixins/sub_ops.rb +35 -0
- data/lib/rails_ops/model_casting.rb +17 -0
- data/lib/rails_ops/model_mixins.rb +12 -0
- data/lib/rails_ops/model_mixins/ar_extension.rb +20 -0
- data/lib/rails_ops/model_mixins/parent_op.rb +10 -0
- data/lib/rails_ops/model_mixins/protected_attributes.rb +78 -0
- data/lib/rails_ops/model_mixins/virtual_attributes.rb +24 -0
- data/lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb +9 -0
- data/lib/rails_ops/model_mixins/virtual_has_one.rb +32 -0
- data/lib/rails_ops/operation.rb +215 -0
- data/lib/rails_ops/operation/model.rb +168 -0
- data/lib/rails_ops/operation/model/create.rb +35 -0
- data/lib/rails_ops/operation/model/destroy.rb +26 -0
- data/lib/rails_ops/operation/model/load.rb +72 -0
- data/lib/rails_ops/operation/model/update.rb +31 -0
- data/lib/rails_ops/patches/active_type_patch.rb +52 -0
- data/lib/rails_ops/profiler.rb +47 -0
- data/lib/rails_ops/profiler/node.rb +64 -0
- data/lib/rails_ops/railtie.rb +19 -0
- data/lib/rails_ops/scoped_env.rb +20 -0
- data/lib/rails_ops/virtual_model.rb +19 -0
- data/rails_ops.gemspec +58 -0
- data/test/test_helper.rb +3 -0
- metadata +252 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
class RailsOps::Hookup::DSL
|
2
|
+
attr_reader :hooks
|
3
|
+
|
4
|
+
def initialize(&block)
|
5
|
+
@current_target = nil
|
6
|
+
@validator = nil
|
7
|
+
@hooks = ActiveSupport::OrderedHash.new
|
8
|
+
|
9
|
+
RailsOps::ScopedEnv.new(self, [:run]).instance_exec(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(target, &block)
|
13
|
+
@current_target = target
|
14
|
+
RailsOps::ScopedEnv.new(self, [:on]).instance_exec(&block)
|
15
|
+
@current_target = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def on(source, event = :after_run)
|
19
|
+
@hooks[source] ||= ActiveSupport::OrderedHash.new
|
20
|
+
@hooks[source][event] ||= []
|
21
|
+
@hooks[source][event] << RailsOps::Hookup::Hook.new(source, event, @current_target)
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate!
|
25
|
+
@validator = RailsOps::Hookup::DSLValidator.new @hooks
|
26
|
+
@validator.validate!
|
27
|
+
@validator
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class RailsOps::Hookup::DSLValidator
|
2
|
+
attr_reader :error, :trace
|
3
|
+
|
4
|
+
def initialize(hooks)
|
5
|
+
@hooks = hooks
|
6
|
+
@trace = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate!
|
10
|
+
# Check infinity loop
|
11
|
+
if target_hooks.any? { |name, targets| recursion?(targets, name) }
|
12
|
+
fail StandardError::SystemStackError.new, "Infinite loop detected in hooks configuration: #{inspect_trace}."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def target_hooks
|
17
|
+
@target_hooks ||= @hooks.map do |name, hash|
|
18
|
+
[name, drilldown(hash)]
|
19
|
+
end.to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def inspect_trace
|
25
|
+
@trace.map(&:to_s).join(' ~> ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def recursion?(targets, name)
|
29
|
+
if targets.include? name
|
30
|
+
@trace << name.to_s
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
|
34
|
+
return targets.any? do |target|
|
35
|
+
if @hooks[target]
|
36
|
+
@trace |= [name, target]
|
37
|
+
recursion? drilldown(@hooks[target]), name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def drilldown(hash)
|
43
|
+
hash.values.flatten.map(&:target_operation)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class RailsOps::Hookup::Hook
|
2
|
+
attr_reader :on_operation
|
3
|
+
attr_reader :on_event
|
4
|
+
attr_reader :target_operation
|
5
|
+
|
6
|
+
def initialize(on_operation, on_event, target_operation)
|
7
|
+
@on_operation = on_operation
|
8
|
+
@on_event = on_event
|
9
|
+
@target_operation = target_operation
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RailsOps
|
2
|
+
# This class subscribes to Rails Ops events via the ActiveSupport
|
3
|
+
# instrumentation API.
|
4
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
# This gets called whenever an operation has been performed and logs the
|
6
|
+
# operation via Rails' `debug` logging method.
|
7
|
+
def run(event)
|
8
|
+
op = event.payload[:operation]
|
9
|
+
|
10
|
+
return if op.class.logging_skipped?
|
11
|
+
|
12
|
+
message = 'OP'
|
13
|
+
|
14
|
+
profile = ::RailsOps::Profiler.node(op.object_id)
|
15
|
+
message += " (#{profile.t_self_ms.round(1)}ms / #{profile.t_kids_ms.round(1)}ms)"
|
16
|
+
profile.free
|
17
|
+
|
18
|
+
message = color(message, YELLOW, true)
|
19
|
+
message += color(" #{op.class.name}", YELLOW, false)
|
20
|
+
|
21
|
+
debug " #{message}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# Mixin for the {RailsOps::Operation} class that provides basic authorization
|
2
|
+
# methods based on the cancan(can) ability that can be provided using an
|
3
|
+
# operation's context.
|
4
|
+
module RailsOps::Mixins::Authorization
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_authorization_disabled
|
9
|
+
class_attribute :_authorization_disabled_for_sub_ops
|
10
|
+
|
11
|
+
policy :after_perform do
|
12
|
+
ensure_authorize_called!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Disables authorization (and authorization check) for this
|
18
|
+
# operation. If `include_sub_ops` is `true` (default `false`),
|
19
|
+
# this setting also applies to sub operations called by this
|
20
|
+
# operation (either via run_sub(!) or via a hook).
|
21
|
+
def without_authorization(include_sub_ops: false)
|
22
|
+
fail 'Option include_sub_ops is not yet supported.' if include_sub_ops
|
23
|
+
|
24
|
+
self._authorization_disabled = true
|
25
|
+
self._authorization_disabled_for_sub_ops = include_sub_ops
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Checks whether authorization is currently enabled and possible
|
30
|
+
# (an ability is present).
|
31
|
+
def authorization_enabled?
|
32
|
+
# Do not perform authorization if it is disabled globally
|
33
|
+
return false unless RailsOps.authorization_enabled?
|
34
|
+
|
35
|
+
# Do not perform authorization if it is disabled for this operation
|
36
|
+
return false if self.class._authorization_disabled
|
37
|
+
|
38
|
+
# Do not perform authorization if no ability is present
|
39
|
+
return false unless context.ability
|
40
|
+
|
41
|
+
# Perform authorization
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
|
45
|
+
def authorization_enabled_for_sub_ops?
|
46
|
+
!@_authorization_disabled_for_sub_ops
|
47
|
+
end
|
48
|
+
|
49
|
+
def authorize_called?
|
50
|
+
@_authorize_called ||= false
|
51
|
+
end
|
52
|
+
|
53
|
+
# Operations within the given block will have disabled authorization.
|
54
|
+
# This only applies to the current thread.
|
55
|
+
def without_authorization(&block)
|
56
|
+
RailsOps.without_authorization(&block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks authorization against the configured authentication backend. Fails if
|
60
|
+
# authentication is not successfull or could not be performed. Does not
|
61
|
+
# perform anything if authorization is disabled.
|
62
|
+
def authorize!(action, *args)
|
63
|
+
authorize_only!(action, *args)
|
64
|
+
@_authorize_called = true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks authorization against the configured authentication backend. Fails if
|
68
|
+
# authentication is not successfull or could not be performed. Does not
|
69
|
+
# perform anything if authorization is disabled. Calling authorize_only! does
|
70
|
+
# not count as authorized concerning ensure_authorize_called!.
|
71
|
+
def authorize_only!(*args)
|
72
|
+
return unless authorization_enabled?
|
73
|
+
RailsOps.authorization_backend.authorize!(self, *args)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks if an authentication check has been performed if authorization is
|
77
|
+
# enabled.
|
78
|
+
def ensure_authorize_called!
|
79
|
+
return unless authorization_enabled?
|
80
|
+
return if authorize_called?
|
81
|
+
fail RailsOps::Exceptions::NoAuthorizationPerformed, "Operation #{self.class.name} has been performed without authorization."
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RailsOps::Mixins::LogSettings
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
class_attribute :_skip_logging
|
6
|
+
self._skip_logging = false
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Allows to skip logging for this operation via the
|
11
|
+
# {RailsOps::LogSubscriber}.
|
12
|
+
def skip_logging(skip = true)
|
13
|
+
self._skip_logging = skip
|
14
|
+
end
|
15
|
+
|
16
|
+
def logging_skipped?
|
17
|
+
_skip_logging
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Mixin for the {RailsOps::Operation::Model} class that provides the method
|
2
|
+
# {authorize_model!} which authorizes the operation's model class or instance
|
3
|
+
# against the authorization methods of {RailsOps::Mixins::Authorization}.
|
4
|
+
module RailsOps::Mixins::Model::Authorization
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_model_authorization_action
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# Gets or sets the action verb used for authorizing models.
|
13
|
+
def model_authorization_action(*action)
|
14
|
+
if action.size == 1
|
15
|
+
self._model_authorization_action = action.first
|
16
|
+
elsif action.size > 1
|
17
|
+
fail ArgumentError, 'Too many arguments'
|
18
|
+
end
|
19
|
+
|
20
|
+
return _model_authorization_action
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def model_authorization_action
|
25
|
+
self.class.model_authorization_action
|
26
|
+
end
|
27
|
+
|
28
|
+
# Performs authorization on the given model using the {authorize?} method.
|
29
|
+
# Models are casted using {cast_model_for_authorization} so that they can be
|
30
|
+
# used for authorization. If no `model_class_or_instance` is given, the
|
31
|
+
# {model} instance method will be used.
|
32
|
+
def authorize_model!(action, model_class_or_instance = model, *extra_args)
|
33
|
+
authorize! action, cast_model_for_authorization(model_class_or_instance), *extra_args
|
34
|
+
end
|
35
|
+
|
36
|
+
# Performs an authorization check like {authorize_model!}, but does not mark
|
37
|
+
# this operation instance as checked. This means that there must be at least
|
38
|
+
# one other authorization call within execution of this operation for the
|
39
|
+
# operation not to fail.
|
40
|
+
def authorize_model_with_authorize_only!(action, model_class_or_instance = model, *extra_args)
|
41
|
+
authorize_only! action, cast_model_for_authorization(model_class_or_instance), *extra_args
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Cast {ActiveType::Record} classes or instances to regular AR models in order
|
47
|
+
# for cancan(can) to work properly. Classes and instances that are no active
|
48
|
+
# type records will be returned as-is.
|
49
|
+
#
|
50
|
+
# TODO: Use ModelCasting module instead?
|
51
|
+
def cast_model_for_authorization(model_class_or_instance)
|
52
|
+
if model_class_or_instance.is_a?(Class)
|
53
|
+
if model_class_or_instance.respond_to?(:extended_record_base_class)
|
54
|
+
return model_class_or_instance.extended_record_base_class
|
55
|
+
else
|
56
|
+
return model_class_or_instance
|
57
|
+
end
|
58
|
+
elsif model_class_or_instance.class.respond_to?(:extended_record_base_class)
|
59
|
+
return ActiveType.cast(model_class_or_instance, model_class_or_instance.class.extended_record_base_class)
|
60
|
+
else
|
61
|
+
model_class_or_instance
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module RailsOps::Mixins::Model::Nesting
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
class_attribute :_nested_model_ops
|
6
|
+
self._nested_model_ops = {}.freeze
|
7
|
+
|
8
|
+
attr_reader :nested_model_ops
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def nest_model_op(attribute, klass, lookup_via_id_on_update: true, ¶ms_block)
|
13
|
+
# ---------------------------------------------------------------
|
14
|
+
# Make sure we're working with an extension / copy
|
15
|
+
# of the given model class
|
16
|
+
# ---------------------------------------------------------------
|
17
|
+
unless always_extend_model_class?
|
18
|
+
fail 'This operation class must be configured to always extend the model class as `nest_model_op` modifies it.'
|
19
|
+
end
|
20
|
+
|
21
|
+
# ---------------------------------------------------------------
|
22
|
+
# Validate association (currently, we only support belongs_to)
|
23
|
+
# ---------------------------------------------------------------
|
24
|
+
reflection = model.reflect_on_association(attribute)
|
25
|
+
|
26
|
+
if reflection.nil?
|
27
|
+
fail "Association #{attribute} could not be found for #{model.model_name}."
|
28
|
+
elsif !reflection.belongs_to?
|
29
|
+
fail 'Method nest_model_op only supports :belongs_to associations, '\
|
30
|
+
"but association #{attribute} of model #{model.model_name} is a "\
|
31
|
+
"#{reflection.macro} association."
|
32
|
+
elsif reflection.options[:autosave] != false
|
33
|
+
fail "Association #{attribute} of #{model.model_name} has :autosave turned on. "\
|
34
|
+
'This is not supported by nest_model_op.'
|
35
|
+
elsif !reflection.options[:validate]
|
36
|
+
fail "Association #{attribute} of #{model.model_name} has :validate turned off. "\
|
37
|
+
'This is not supported by nest_model_op.'
|
38
|
+
end
|
39
|
+
|
40
|
+
# ---------------------------------------------------------------
|
41
|
+
# Define attributes setter on model.
|
42
|
+
#
|
43
|
+
# Model nesting is not compatible with accepts_nested_attributes_for
|
44
|
+
# as it automatically enables :autosave which is not supported.
|
45
|
+
# As we can't use accepts_nested_attributes_for, no nested attributes
|
46
|
+
# setter is generated. This also means that `fields_for` in form
|
47
|
+
# generation don't detect the attribute as nested and mistakenly omit the
|
48
|
+
# _attributes suffix. Therefore, we define this method but fail if it
|
49
|
+
# ever gets called.
|
50
|
+
# ---------------------------------------------------------------
|
51
|
+
model.send(:define_method, "#{attribute}_attributes=") do |_value|
|
52
|
+
fail 'This operation model does not allow receiving nested attributes' \
|
53
|
+
"for #{attribute}, as this is saved using a nested model operation."
|
54
|
+
end
|
55
|
+
|
56
|
+
# ---------------------------------------------------------------
|
57
|
+
# Validate inverse association reflection if given
|
58
|
+
# ---------------------------------------------------------------
|
59
|
+
if (inverse_reflection = reflection.inverse_of)
|
60
|
+
if inverse_reflection.options[:autosave] != false
|
61
|
+
fail "Association #{inverse_reflection.name} of #{inverse_reflection.active_record} has :autosave turned on. "\
|
62
|
+
'This is not supported by nest_model_op.'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# ---------------------------------------------------------------
|
67
|
+
# Store configuration
|
68
|
+
# ---------------------------------------------------------------
|
69
|
+
self._nested_model_ops = _nested_model_ops.merge(
|
70
|
+
attribute => {
|
71
|
+
klass: klass,
|
72
|
+
params_proc: params_block,
|
73
|
+
lookup_via_id_on_update: lookup_via_id_on_update
|
74
|
+
}
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def nested_model_param_keys
|
79
|
+
_nested_model_ops.keys.collect do |attribute|
|
80
|
+
"#{attribute}_attributes"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def nested_model_ops_performed?
|
86
|
+
@nested_model_ops_performed || false
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def nested_model_op(attribute)
|
92
|
+
fail 'Nested model operations have not been built yet.' unless @nested_model_ops
|
93
|
+
return @nested_model_ops[attribute]
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_nested_model_ops(action)
|
97
|
+
# Validate action
|
98
|
+
fail 'Unsupported action.' unless [:create, :update].include?(action)
|
99
|
+
|
100
|
+
# Make sure that this method can only be run once per operation
|
101
|
+
fail 'Nested model operations can only be built once.' if @nested_model_ops
|
102
|
+
@nested_model_ops = {}
|
103
|
+
|
104
|
+
self.class._nested_model_ops.each do |attribute, config|
|
105
|
+
op_params = extract_attributes_from_params["#{attribute}_attributes"] || {}
|
106
|
+
|
107
|
+
# Remove id field as this is commonly supplied by Rails' `fields_for` if
|
108
|
+
# the nested model is persisted. We don't usually need this.
|
109
|
+
op_params = op_params.except(:id)
|
110
|
+
|
111
|
+
# Apply custom params processing callback if given
|
112
|
+
if config[:params_proc]
|
113
|
+
op_params = instance_exec(op_params, &config[:params_proc])
|
114
|
+
end
|
115
|
+
|
116
|
+
# Wrap parameters for nested model operation
|
117
|
+
if action == :create
|
118
|
+
wrapped_params = {
|
119
|
+
attribute => op_params
|
120
|
+
}
|
121
|
+
elsif action == :update
|
122
|
+
if config[:lookup_via_id_on_update]
|
123
|
+
foreign_key = model.class.reflect_on_association(attribute).foreign_key
|
124
|
+
id = model.send(foreign_key)
|
125
|
+
else
|
126
|
+
id = model.send(attribute).id
|
127
|
+
end
|
128
|
+
|
129
|
+
wrapped_params = {
|
130
|
+
:id => id,
|
131
|
+
attribute => op_params
|
132
|
+
}
|
133
|
+
else
|
134
|
+
fail "Unsupported action #{action}."
|
135
|
+
end
|
136
|
+
|
137
|
+
# Instantiate nested operation
|
138
|
+
@nested_model_ops[attribute] = sub_op(config[:klass], wrapped_params)
|
139
|
+
|
140
|
+
# Inject model of nested operation to our own model
|
141
|
+
nested_model = @nested_model_ops[attribute].model
|
142
|
+
model.send("#{attribute}=", nested_model)
|
143
|
+
|
144
|
+
# Inject our own model to model of nested operation (if the inverse
|
145
|
+
# reflection can be resolved)
|
146
|
+
if (inverse_reflection = model.class.reflect_on_association(attribute).inverse_of)
|
147
|
+
nested_model.send("#{inverse_reflection.name}=", model)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Tries to save nested models using their respective operations.
|
153
|
+
def perform_nested_model_ops!
|
154
|
+
fail 'Nested model operations can only be performed once.' if nested_model_ops_performed?
|
155
|
+
|
156
|
+
# Validate the whole model hierarchy. Since we're calling 'model' here, this
|
157
|
+
# line also makes sure a model is built.
|
158
|
+
model.validate!
|
159
|
+
|
160
|
+
# Make sure nested model operations are build
|
161
|
+
fail 'Nested model operations are not built yet. Make sure the model is built.' unless @nested_model_ops
|
162
|
+
|
163
|
+
@nested_model_ops.each do |attribute, op|
|
164
|
+
# Run the nested model operation and fail hard if a validation error
|
165
|
+
# arises. This should generally not happen as the whole hierarchy has been
|
166
|
+
# validated before. It is vital that the transaction gets rolled back if
|
167
|
+
# an exception happens here.
|
168
|
+
begin
|
169
|
+
op.run!
|
170
|
+
rescue op.validation_errors => e
|
171
|
+
fail RailsOps::Exceptions::SubOpValidationFailed, e
|
172
|
+
end
|
173
|
+
|
174
|
+
# Assign model again so that the ID gets updated
|
175
|
+
model.send("#{attribute}=", op.model)
|
176
|
+
end
|
177
|
+
|
178
|
+
@nested_model_ops_performed = true
|
179
|
+
end
|
180
|
+
end
|