state_machines-activerecord 0.8.0 → 0.200.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 +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +178 -8
- data/lib/state_machines/integrations/active_record/locale.rb +12 -9
- data/lib/state_machines/integrations/active_record/type/integer.rb +122 -0
- data/lib/state_machines/integrations/active_record/version.rb +3 -1
- data/lib/state_machines/integrations/active_record.rb +548 -181
- data/lib/state_machines-activerecord.rb +2 -0
- metadata +37 -154
- data/.gitignore +0 -22
- data/.travis.yml +0 -19
- data/Appraisals +0 -34
- data/Gemfile +0 -6
- data/Rakefile +0 -9
- data/gemfiles/active_record_5.1.gemfile +0 -14
- data/gemfiles/active_record_5.2.gemfile +0 -14
- data/gemfiles/active_record_6.0.gemfile +0 -14
- data/gemfiles/active_record_6.1.gemfile +0 -14
- data/gemfiles/active_record_edge.gemfile +0 -14
- data/log/.gitkeep +0 -0
- data/state_machines-activerecord.gemspec +0 -28
- data/test/files/en.yml +0 -5
- data/test/files/models/post.rb +0 -11
- data/test/integration_test.rb +0 -25
- data/test/machine_by_default_test.rb +0 -16
- data/test/machine_errors_test.rb +0 -19
- data/test/machine_multiple_test.rb +0 -17
- data/test/machine_nested_action_test.rb +0 -38
- data/test/machine_unmigrated_test.rb +0 -14
- data/test/machine_with_aliased_attribute_test.rb +0 -23
- data/test/machine_with_callbacks_test.rb +0 -172
- data/test/machine_with_column_state_attribute_test.rb +0 -44
- data/test/machine_with_complex_pluralization_scopes_test.rb +0 -16
- data/test/machine_with_conflicting_predicate_test.rb +0 -18
- data/test/machine_with_conflicting_state_name_test.rb +0 -29
- data/test/machine_with_custom_attribute_test.rb +0 -21
- data/test/machine_with_default_scope_test.rb +0 -18
- data/test/machine_with_different_column_default_test.rb +0 -27
- data/test/machine_with_different_integer_column_default_test.rb +0 -29
- data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +0 -24
- data/test/machine_with_dirty_attribute_and_state_events_test.rb +0 -20
- data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +0 -32
- data/test/machine_with_dirty_attributes_during_loopback_test.rb +0 -22
- data/test/machine_with_dirty_attributes_test.rb +0 -35
- data/test/machine_with_dynamic_initial_state_test.rb +0 -99
- data/test/machine_with_event_attributes_on_autosave_test.rb +0 -48
- data/test/machine_with_event_attributes_on_custom_action_test.rb +0 -41
- data/test/machine_with_event_attributes_on_save_bang_test.rb +0 -82
- data/test/machine_with_event_attributes_on_save_test.rb +0 -244
- data/test/machine_with_event_attributes_on_validation_test.rb +0 -143
- data/test/machine_with_events_test.rb +0 -13
- data/test/machine_with_failed_action_test.rb +0 -40
- data/test/machine_with_failed_after_callbacks_test.rb +0 -35
- data/test/machine_with_failed_before_callbacks_test.rb +0 -36
- data/test/machine_with_initialized_state_test.rb +0 -41
- data/test/machine_with_internationalization_test.rb +0 -180
- data/test/machine_with_loopback_test.rb +0 -22
- data/test/machine_with_non_column_state_attribute_defined_test.rb +0 -29
- data/test/machine_with_same_column_default_test.rb +0 -26
- data/test/machine_with_same_integer_column_default_test.rb +0 -30
- data/test/machine_with_scopes_and_joins_test.rb +0 -37
- data/test/machine_with_scopes_and_owner_subclass_test.rb +0 -27
- data/test/machine_with_scopes_test.rb +0 -70
- data/test/machine_with_state_driven_validations_test.rb +0 -30
- data/test/machine_with_states_test.rb +0 -13
- data/test/machine_with_static_initial_state_test.rb +0 -166
- data/test/machine_with_transactions_test.rb +0 -26
- data/test/machine_with_validations_and_custom_attribute_test.rb +0 -21
- data/test/machine_with_validations_test.rb +0 -47
- data/test/machine_without_database_test.rb +0 -20
- data/test/machine_without_transactions_test.rb +0 -26
- data/test/model_test.rb +0 -12
- data/test/test_helper.rb +0 -52
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'state_machines-activemodel'
|
|
2
4
|
require 'active_record'
|
|
3
5
|
require 'state_machines/integrations/active_record/version'
|
|
6
|
+
require 'state_machines/integrations/active_record/type/integer'
|
|
4
7
|
|
|
5
8
|
module StateMachines
|
|
6
|
-
module Integrations
|
|
9
|
+
module Integrations # :nodoc:
|
|
7
10
|
# Adds support for integrating state machines with ActiveRecord models.
|
|
8
11
|
#
|
|
9
12
|
# == Examples
|
|
@@ -11,7 +14,7 @@ module StateMachines
|
|
|
11
14
|
# Below is an example of a simple state machine defined within an
|
|
12
15
|
# ActiveRecord model:
|
|
13
16
|
#
|
|
14
|
-
# class Vehicle <
|
|
17
|
+
# class Vehicle < ApplicationRecord
|
|
15
18
|
# state_machine :initial => :parked do
|
|
16
19
|
# event :ignite do
|
|
17
20
|
# transition :parked => :idling
|
|
@@ -78,25 +81,22 @@ module StateMachines
|
|
|
78
81
|
# === Security implications
|
|
79
82
|
#
|
|
80
83
|
# Beware that public event attributes mean that events can be fired
|
|
81
|
-
# whenever mass-assignment is being used.
|
|
82
|
-
# users from tampering with events through URLs / forms,
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# class
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
# state_machine do
|
|
90
|
-
# ...
|
|
84
|
+
# whenever mass-assignment is being used. If you want to prevent malicious
|
|
85
|
+
# users from tampering with events through URLs / forms, you should use
|
|
86
|
+
# Rails' strong parameters to control which attributes are permitted:
|
|
87
|
+
#
|
|
88
|
+
# class VehiclesController < ApplicationController
|
|
89
|
+
# def vehicle_params
|
|
90
|
+
# params.require(:vehicle).permit(:color, :make, :model)
|
|
91
|
+
# # Exclude state_event to prevent tampering
|
|
91
92
|
# end
|
|
92
93
|
# end
|
|
93
94
|
#
|
|
94
95
|
# If you want to only have *some* events be able to fire via mass-assignment,
|
|
95
96
|
# you can build two state machines (one public and one protected) like so:
|
|
96
97
|
#
|
|
97
|
-
# class Vehicle <
|
|
98
|
-
#
|
|
99
|
-
#
|
|
98
|
+
# class Vehicle < ApplicationRecord
|
|
99
|
+
# # Define private machine
|
|
100
100
|
# state_machine do
|
|
101
101
|
# # Define private events here
|
|
102
102
|
# end
|
|
@@ -105,6 +105,8 @@ module StateMachines
|
|
|
105
105
|
# state_machine :public_state, :attribute => :state do
|
|
106
106
|
# # Define public events here
|
|
107
107
|
# end
|
|
108
|
+
#
|
|
109
|
+
# # Control access via strong parameters in your controller
|
|
108
110
|
# end
|
|
109
111
|
#
|
|
110
112
|
# == Transactions
|
|
@@ -115,7 +117,7 @@ module StateMachines
|
|
|
115
117
|
#
|
|
116
118
|
# For example,
|
|
117
119
|
#
|
|
118
|
-
# class Message <
|
|
120
|
+
# class Message < ApplicationRecord
|
|
119
121
|
# end
|
|
120
122
|
#
|
|
121
123
|
# Vehicle.state_machine do
|
|
@@ -136,7 +138,7 @@ module StateMachines
|
|
|
136
138
|
#
|
|
137
139
|
# To turn off transactions:
|
|
138
140
|
#
|
|
139
|
-
# class Vehicle <
|
|
141
|
+
# class Vehicle < ApplicationRecord
|
|
140
142
|
# state_machine :initial => :parked, :use_transactions => false do
|
|
141
143
|
# ...
|
|
142
144
|
# end
|
|
@@ -150,7 +152,7 @@ module StateMachines
|
|
|
150
152
|
# framework, custom validators will not work as expected when defined to run
|
|
151
153
|
# in multiple states. For example:
|
|
152
154
|
#
|
|
153
|
-
# class Vehicle <
|
|
155
|
+
# class Vehicle < ApplicationRecord
|
|
154
156
|
# state_machine do
|
|
155
157
|
# ...
|
|
156
158
|
# state :first_gear, :second_gear do
|
|
@@ -163,7 +165,7 @@ module StateMachines
|
|
|
163
165
|
# for the <tt>:second_gear</tt> state. To avoid this, you can define your
|
|
164
166
|
# custom validation like so:
|
|
165
167
|
#
|
|
166
|
-
# class Vehicle <
|
|
168
|
+
# class Vehicle < ApplicationRecord
|
|
167
169
|
# state_machine do
|
|
168
170
|
# ...
|
|
169
171
|
# state :first_gear, :second_gear do
|
|
@@ -195,38 +197,48 @@ module StateMachines
|
|
|
195
197
|
# example, assuming there's a validation on a field called +name+ on the class:
|
|
196
198
|
#
|
|
197
199
|
# vehicle = Vehicle.new
|
|
198
|
-
# vehicle.ignite! # => StateMachines::InvalidTransition: Cannot transition state via :ignite from :parked
|
|
200
|
+
# vehicle.ignite! # => StateMachines::InvalidTransition: Cannot transition state via :ignite from :parked
|
|
201
|
+
# # (Reason(s): Name cannot be blank)
|
|
199
202
|
#
|
|
200
203
|
# == Scopes
|
|
201
204
|
#
|
|
202
|
-
# To assist in filtering models with specific states, a series of
|
|
203
|
-
#
|
|
205
|
+
# To assist in filtering models with specific states, a series of scopes
|
|
206
|
+
# are defined on the model for finding records with or without a
|
|
204
207
|
# particular set of states.
|
|
205
208
|
#
|
|
206
|
-
# These
|
|
209
|
+
# These scopes are essentially the functional equivalent of the
|
|
207
210
|
# following definitions:
|
|
208
211
|
#
|
|
209
|
-
# class Vehicle <
|
|
210
|
-
# named_scope :with_states, lambda {|*states| {:conditions => {:state => states}}}
|
|
212
|
+
# class Vehicle < ApplicationRecord
|
|
211
213
|
# # with_states also aliased to with_state
|
|
214
|
+
# scope :with_states, ->(states) { states.present? ? where(state: states) : all }
|
|
212
215
|
#
|
|
213
|
-
# named_scope :without_states, lambda {|*states| {:conditions => ['state NOT IN (?)', states]}}
|
|
214
216
|
# # without_states also aliased to without_state
|
|
217
|
+
# scope :without_states, ->(states) { states.present? ? where.not(state: states) : all }
|
|
215
218
|
# end
|
|
216
219
|
#
|
|
217
220
|
# *Note*, however, that the states are converted to their stored values
|
|
218
221
|
# before being passed into the query.
|
|
219
222
|
#
|
|
220
|
-
# Because of the way
|
|
223
|
+
# Because of the way scopes work in ActiveRecord, they can be
|
|
221
224
|
# chained like so:
|
|
222
225
|
#
|
|
223
|
-
# Vehicle.with_state(:parked).
|
|
226
|
+
# Vehicle.with_state(:parked).order(id: :desc)
|
|
224
227
|
#
|
|
225
228
|
# Note that states can also be referenced by the string version of their
|
|
226
229
|
# name:
|
|
227
230
|
#
|
|
228
231
|
# Vehicle.with_state('parked')
|
|
229
232
|
#
|
|
233
|
+
# === Transparent Scopes
|
|
234
|
+
#
|
|
235
|
+
# When `nil` is passed to any of the state scopes, they return `all` records
|
|
236
|
+
# without applying any filters. This allows for more flexible scope chaining
|
|
237
|
+
# in search interfaces:
|
|
238
|
+
#
|
|
239
|
+
# Vehicle.with_state(params[:state]) # Returns all vehicles if params[:state] is nil
|
|
240
|
+
# Vehicle.where(color: 'red').with_state(nil) # Returns all red vehicles
|
|
241
|
+
#
|
|
230
242
|
# == Callbacks
|
|
231
243
|
#
|
|
232
244
|
# All before/after transition callbacks defined for ActiveRecord models
|
|
@@ -235,7 +247,7 @@ module StateMachines
|
|
|
235
247
|
#
|
|
236
248
|
# For example,
|
|
237
249
|
#
|
|
238
|
-
# class Vehicle <
|
|
250
|
+
# class Vehicle < ApplicationRecord
|
|
239
251
|
# state_machine :initial => :parked do
|
|
240
252
|
# before_transition any => :idling do |vehicle|
|
|
241
253
|
# vehicle.put_on_seatbelt
|
|
@@ -266,11 +278,11 @@ module StateMachines
|
|
|
266
278
|
# ActiveRecord, a save failure will cause any records that get created in
|
|
267
279
|
# your callback to roll back. You can work around this issue like so:
|
|
268
280
|
#
|
|
269
|
-
# class TransitionLog <
|
|
270
|
-
#
|
|
281
|
+
# class TransitionLog < ApplicationRecord
|
|
282
|
+
# connects_to database: { writing: :primary, reading: :primary }
|
|
271
283
|
# end
|
|
272
284
|
#
|
|
273
|
-
# class Vehicle <
|
|
285
|
+
# class Vehicle < ApplicationRecord
|
|
274
286
|
# state_machine do
|
|
275
287
|
# after_failure do |vehicle, transition|
|
|
276
288
|
# TransitionLog.create(:vehicle => vehicle, :transition => transition)
|
|
@@ -280,7 +292,7 @@ module StateMachines
|
|
|
280
292
|
# end
|
|
281
293
|
# end
|
|
282
294
|
#
|
|
283
|
-
# The +TransitionLog+ model establishes a
|
|
295
|
+
# The +TransitionLog+ model establishes a separate connection to the database
|
|
284
296
|
# that allows new records to be saved without being affected by rollbacks
|
|
285
297
|
# in the +Vehicle+ model's transaction.
|
|
286
298
|
#
|
|
@@ -304,66 +316,16 @@ module StateMachines
|
|
|
304
316
|
# * (8) *after_transition*
|
|
305
317
|
# * (-) end transaction (if enabled)
|
|
306
318
|
# * (9) after_commit
|
|
319
|
+
# * (10) *after_transition* callbacks defined with <tt>:after_commit => true</tt>
|
|
307
320
|
#
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
# observers are designed, there is less flexibility around the specific
|
|
313
|
-
# transitions that can be hooked in. However, a large number of hooks
|
|
314
|
-
# *are* supported. For example, if a transition for a record's +state+
|
|
315
|
-
# attribute changes the state from +parked+ to +idling+ via the +ignite+
|
|
316
|
-
# event, the following observer methods are supported:
|
|
317
|
-
# * before/after/after_failure_to-_ignite_from_parked_to_idling
|
|
318
|
-
# * before/after/after_failure_to-_ignite_from_parked
|
|
319
|
-
# * before/after/after_failure_to-_ignite_to_idling
|
|
320
|
-
# * before/after/after_failure_to-_ignite
|
|
321
|
-
# * before/after/after_failure_to-_transition_state_from_parked_to_idling
|
|
322
|
-
# * before/after/after_failure_to-_transition_state_from_parked
|
|
323
|
-
# * before/after/after_failure_to-_transition_state_to_idling
|
|
324
|
-
# * before/after/after_failure_to-_transition_state
|
|
325
|
-
# * before/after/after_failure_to-_transition
|
|
326
|
-
#
|
|
327
|
-
# The following class shows an example of some of these hooks:
|
|
328
|
-
#
|
|
329
|
-
# class VehicleObserver < ActiveRecord::Observer
|
|
330
|
-
# def before_save(vehicle)
|
|
331
|
-
# # log message
|
|
332
|
-
# end
|
|
333
|
-
#
|
|
334
|
-
# # Callback for :ignite event *before* the transition is performed
|
|
335
|
-
# def before_ignite(vehicle, transition)
|
|
336
|
-
# # log message
|
|
337
|
-
# end
|
|
338
|
-
#
|
|
339
|
-
# # Callback for :ignite event *after* the transition has been performed
|
|
340
|
-
# def after_ignite(vehicle, transition)
|
|
341
|
-
# # put on seatbelt
|
|
342
|
-
# end
|
|
343
|
-
#
|
|
344
|
-
# # Generic transition callback *before* the transition is performed
|
|
345
|
-
# def after_transition(vehicle, transition)
|
|
346
|
-
# Audit.log(vehicle, transition)
|
|
347
|
-
# end
|
|
348
|
-
# end
|
|
349
|
-
#
|
|
350
|
-
# More flexible transition callbacks can be defined directly within the
|
|
351
|
-
# model as described in StateMachines::Machine#before_transition
|
|
352
|
-
# and StateMachines::Machine#after_transition.
|
|
353
|
-
#
|
|
354
|
-
# To define a single observer for multiple state machines:
|
|
355
|
-
#
|
|
356
|
-
# class StateMachineObserver < ActiveRecord::Observer
|
|
357
|
-
# observe Vehicle, Switch, Project
|
|
358
|
-
#
|
|
359
|
-
# def after_transition(record, transition)
|
|
360
|
-
# Audit.log(record, transition)
|
|
361
|
-
# end
|
|
362
|
-
# end
|
|
321
|
+
# An +after_transition+ callback defined with the <tt>:after_commit</tt>
|
|
322
|
+
# option is deferred until the surrounding database transaction has been
|
|
323
|
+
# committed (and discarded if it rolls back). See the documentation for
|
|
324
|
+
# +after_transition+ in this integration for more details.
|
|
363
325
|
#
|
|
364
326
|
# == Internationalization
|
|
365
327
|
#
|
|
366
|
-
#
|
|
328
|
+
# Any error message that is generated from performing invalid
|
|
367
329
|
# transitions can be localized. The following default translations are used:
|
|
368
330
|
#
|
|
369
331
|
# en:
|
|
@@ -376,9 +338,6 @@ module StateMachines
|
|
|
376
338
|
# # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
|
|
377
339
|
# invalid_transition: "cannot transition via %{event}"
|
|
378
340
|
#
|
|
379
|
-
# Notice that the interpolation syntax is %{key} in Rails 3+. In Rails 2.x,
|
|
380
|
-
# the appropriate syntax is {{key}}.
|
|
381
|
-
#
|
|
382
341
|
# You can override these for a specific model like so:
|
|
383
342
|
#
|
|
384
343
|
# en:
|
|
@@ -418,8 +377,441 @@ module StateMachines
|
|
|
418
377
|
include ActiveModel
|
|
419
378
|
|
|
420
379
|
# The default options to use for state machines using this integration
|
|
421
|
-
@defaults = {:
|
|
380
|
+
@defaults = { action: :save, use_transactions: true }
|
|
381
|
+
@auto_convert_integer_state_attributes = true
|
|
382
|
+
|
|
383
|
+
# Machine-specific methods for enum integration
|
|
384
|
+
module MachineMethods
|
|
385
|
+
# Enum integration metadata storage
|
|
386
|
+
attr_accessor :enum_integration
|
|
387
|
+
|
|
388
|
+
# Hook called after machine initialization
|
|
389
|
+
def after_initialize
|
|
390
|
+
super
|
|
391
|
+
initialize_enum_integration
|
|
392
|
+
|
|
393
|
+
register_integer_type if register_integer_type?
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Hook called when a machine is assigned to a class, including when an
|
|
397
|
+
# inherited machine is cloned for an STI subclass. The cloned machine
|
|
398
|
+
# carries the parent's @integer_type_registered flag while the subclass
|
|
399
|
+
# still inherits the parent's attribute type, which references the
|
|
400
|
+
# parent machine's state collection. Re-register so the subclass type
|
|
401
|
+
# sees this machine's (cloned) states, picking up subclass-added states
|
|
402
|
+
# as they are defined.
|
|
403
|
+
#
|
|
404
|
+
# @param klass [Class] the new owner class
|
|
405
|
+
# @return [Class] the assigned owner class
|
|
406
|
+
def owner_class=(klass)
|
|
407
|
+
super.tap do
|
|
408
|
+
register_integer_type if integer_type_registered?
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Check if enum integration should be enabled for this machine
|
|
413
|
+
def detect_enum_integration
|
|
414
|
+
return nil unless owner_class.defined_enums.key?(attribute.to_s)
|
|
415
|
+
|
|
416
|
+
# For now, auto-detect enum and enable basic integration
|
|
417
|
+
# Later we can add explicit configuration options
|
|
418
|
+
{
|
|
419
|
+
enabled: true,
|
|
420
|
+
prefix: true,
|
|
421
|
+
suffix: false,
|
|
422
|
+
scopes: true,
|
|
423
|
+
enum_values: owner_class.defined_enums[attribute.to_s] || {},
|
|
424
|
+
original_enum_methods: detect_existing_enum_methods,
|
|
425
|
+
state_machine_methods: []
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Initialize enum integration if enum is detected
|
|
430
|
+
def initialize_enum_integration
|
|
431
|
+
detected_config = detect_enum_integration
|
|
432
|
+
return unless detected_config
|
|
433
|
+
|
|
434
|
+
# Store enum integration metadata
|
|
435
|
+
self.enum_integration = detected_config
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Override state method to trigger method generation after states are defined
|
|
439
|
+
def state(*, &)
|
|
440
|
+
result = super
|
|
441
|
+
|
|
442
|
+
# Generate methods after each state addition if enum integration is enabled
|
|
443
|
+
generate_state_machine_methods if enum_integrated?
|
|
444
|
+
|
|
445
|
+
# States defined after initialization (e.g. adding an explicit value
|
|
446
|
+
# to the initial state) can change how the column default maps to
|
|
447
|
+
# states, so re-evaluate the conflicting-default warning
|
|
448
|
+
recheck_conflicting_attribute_default if integer_type_registered?
|
|
449
|
+
|
|
450
|
+
result
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Warns at most once when the column default conflicts with the
|
|
454
|
+
# machine's initial state. The conflict is re-evaluated as states are
|
|
455
|
+
# defined, so remember when the warning has been issued.
|
|
456
|
+
def check_conflicting_attribute_default
|
|
457
|
+
return if @attribute_default_conflict_warned
|
|
458
|
+
|
|
459
|
+
initial_state = states.detect(&:initial)
|
|
460
|
+
conflict = !owner_class_attribute_default.nil? && (
|
|
461
|
+
dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
|
|
462
|
+
)
|
|
463
|
+
return unless conflict
|
|
464
|
+
|
|
465
|
+
@attribute_default_conflict_warned = true
|
|
466
|
+
super
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Returns true when this machine should use the custom integer attribute type
|
|
470
|
+
# to convert between Ruby state names and integer database values. This only
|
|
471
|
+
# applies to non-enum integer columns when automatic conversion is enabled.
|
|
472
|
+
def register_integer_type?
|
|
473
|
+
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes &&
|
|
474
|
+
integer_column? &&
|
|
475
|
+
!enum_integrated?
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Check if this machine has enum integration enabled
|
|
479
|
+
def enum_integrated?
|
|
480
|
+
enum_integration && enum_integration[:enabled]
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Get the enum mapping for this attribute
|
|
484
|
+
def enum_mapping
|
|
485
|
+
return {} unless enum_integrated?
|
|
486
|
+
|
|
487
|
+
enum_integration[:enum_values] || {}
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Get list of original enum methods that were preserved
|
|
491
|
+
def original_enum_methods
|
|
492
|
+
return [] unless enum_integrated?
|
|
493
|
+
|
|
494
|
+
enum_integration[:original_enum_methods] || []
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Get list of state machine methods that were generated
|
|
498
|
+
def state_machine_methods
|
|
499
|
+
return [] unless enum_integrated?
|
|
500
|
+
|
|
501
|
+
enum_integration[:state_machine_methods] || []
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def integer_type_registered?
|
|
505
|
+
!!@integer_type_registered
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Creates a callback that will be invoked *after* a transition is
|
|
509
|
+
# performed, so long as the given requirements match the transition.
|
|
510
|
+
#
|
|
511
|
+
# In addition to the configuration options supported by the core
|
|
512
|
+
# +after_transition+ (see StateMachines::Machine#after_transition), the
|
|
513
|
+
# ActiveRecord integration supports:
|
|
514
|
+
# * <tt>:after_commit</tt> - Defer execution of the callback until the
|
|
515
|
+
# database transaction wrapping the transition has been committed.
|
|
516
|
+
# When no transaction is open at that point, the callback runs
|
|
517
|
+
# immediately. When the transaction (or an outer transaction wrapping
|
|
518
|
+
# it) is rolled back, the callback is discarded.
|
|
519
|
+
#
|
|
520
|
+
# This is the safe place to enqueue background jobs that reference the
|
|
521
|
+
# record (e.g. via GlobalID), since a regular +after_transition+ runs
|
|
522
|
+
# inside the transaction, before the record's changes are visible to
|
|
523
|
+
# other connections:
|
|
524
|
+
#
|
|
525
|
+
# class Vehicle < ApplicationRecord
|
|
526
|
+
# state_machine do
|
|
527
|
+
# after_transition on: :ignite, after_commit: true do |vehicle|
|
|
528
|
+
# EngineWarmupJob.perform_later(vehicle)
|
|
529
|
+
# end
|
|
530
|
+
#
|
|
531
|
+
# ...
|
|
532
|
+
# end
|
|
533
|
+
# end
|
|
534
|
+
#
|
|
535
|
+
# Note that a deferred callback cannot halt the callback chain or
|
|
536
|
+
# affect the result of the transition: by the time it runs, the
|
|
537
|
+
# transition has already been committed. For the same reason, an
|
|
538
|
+
# exception raised by a deferred callback is not propagated (doing so
|
|
539
|
+
# would revert the record's in-memory state even though the database
|
|
540
|
+
# was already updated); it is reported to +ActiveSupport.error_reporter+
|
|
541
|
+
# (+Rails.error+) instead. Conditions (<tt>:if</tt>/<tt>:unless</tt>)
|
|
542
|
+
# and state requirements are evaluated when the transition is
|
|
543
|
+
# performed, not at commit time.
|
|
544
|
+
#
|
|
545
|
+
# Like ActiveRecord's own +after_commit+, a surrounding
|
|
546
|
+
# <tt>transaction(joinable: false)</tt> wrapper is transparent: the
|
|
547
|
+
# callback fires at the inner commit. This is what makes it fire
|
|
548
|
+
# under transactional test fixtures.
|
|
549
|
+
def after_transition(*args, **options, &block)
|
|
550
|
+
# The flag may hide in a legacy trailing positional options hash
|
|
551
|
+
positional_options = args.last.is_a?(Hash) ? args.pop.dup : {}
|
|
552
|
+
options = positional_options.merge(options)
|
|
553
|
+
|
|
554
|
+
# Only a boolean is the flag — a non-boolean value is the implicit
|
|
555
|
+
# state-requirement form for a state named :after_commit
|
|
556
|
+
flag = options[:after_commit]
|
|
557
|
+
return super unless flag == true || flag == false
|
|
558
|
+
|
|
559
|
+
options.delete(:after_commit)
|
|
560
|
+
return super unless flag
|
|
561
|
+
|
|
562
|
+
# Method handling goes to a real Callback (reusing core's binding,
|
|
563
|
+
# arity and :do semantics); branch matching stays on the wrapper so
|
|
564
|
+
# conditions are evaluated at transition time
|
|
565
|
+
parsed = parse_callback_arguments(args, options)
|
|
566
|
+
method_options = parsed.slice(:do, :bind_to_object)
|
|
567
|
+
method_options[:terminator] = callback_terminator
|
|
568
|
+
branch_options = parsed.except(:do, :bind_to_object, :terminator)
|
|
569
|
+
|
|
570
|
+
deferred = Callback.new(:after, method_options, &block)
|
|
571
|
+
|
|
572
|
+
super(**branch_options, bind_to_object: false) do |object, transition|
|
|
573
|
+
object.class.current_transaction.after_commit do
|
|
574
|
+
# The transition's catch(:halt) is gone at commit time
|
|
575
|
+
catch(:halt) { deferred.call(object, {}, transition) }
|
|
576
|
+
rescue StandardError => e
|
|
577
|
+
# Raising would roll back in-memory state already committed; report instead
|
|
578
|
+
ActiveSupport.error_reporter.report(e, handled: false, source: 'state_machines-activerecord')
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Machine internals (state matching, validations) call read() to get the
|
|
584
|
+
# current state value and compare it against state.value. The custom
|
|
585
|
+
# integer type already returns the canonical value when state values are
|
|
586
|
+
# uniform: raw integers when every named state has an explicit integer
|
|
587
|
+
# value (passthrough), name strings when none do (state.value is the
|
|
588
|
+
# name). Only machines mixing explicit and auto-indexed values need an
|
|
589
|
+
# override, because the type returns name strings while the explicit
|
|
590
|
+
# states match on integers; map the stored value back to the matched
|
|
591
|
+
# state's canonical state.value.
|
|
592
|
+
#
|
|
593
|
+
# @param object [ActiveRecord::Base] record being read
|
|
594
|
+
# @param attr_sym [Symbol] attribute kind (:state, :event, ...)
|
|
595
|
+
# @param ivar [Boolean] whether to read from an instance variable
|
|
596
|
+
# @return [Object] a value machine internals can match on state.value
|
|
597
|
+
def read(object, attr_sym, ivar = false)
|
|
598
|
+
return super unless integer_type_registered? && attr_sym == :state
|
|
599
|
+
return super unless mixed_integer_state_values?
|
|
600
|
+
|
|
601
|
+
raw = object.read_attribute_before_type_cast(attribute.to_s)
|
|
602
|
+
if raw.is_a?(::String) || raw.is_a?(::Symbol)
|
|
603
|
+
matched = states.detect { |s| s.name && s.name.to_s == raw.to_s }
|
|
604
|
+
return matched.value if matched
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
name = owner_class.type_for_attribute(attribute.to_s).deserialize(raw)
|
|
608
|
+
matched = states.detect { |s| s.name && s.name.to_s == name.to_s }
|
|
609
|
+
matched ? matched.value : raw
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
private
|
|
613
|
+
|
|
614
|
+
# Re-runs the conflicting-default check for states defined after
|
|
615
|
+
# initialization. Skipped until an initial state exists (the DSL block
|
|
616
|
+
# evaluates before initial_state= during Machine.new).
|
|
617
|
+
#
|
|
618
|
+
# @return [void]
|
|
619
|
+
def recheck_conflicting_attribute_default
|
|
620
|
+
return unless states.detect(&:initial)
|
|
621
|
+
|
|
622
|
+
check_conflicting_attribute_default
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Returns true when named states mix explicit integer values with
|
|
626
|
+
# auto-indexed (name-valued) states. Uses value(false) so dynamic
|
|
627
|
+
# (Proc) state values are never evaluated for this metadata decision.
|
|
628
|
+
#
|
|
629
|
+
# @return [Boolean]
|
|
630
|
+
def mixed_integer_state_values?
|
|
631
|
+
named = states.select(&:name)
|
|
632
|
+
explicit = named.count { |s| s.value(false).is_a?(::Integer) }
|
|
633
|
+
explicit.positive? && explicit < named.size
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Returns true when the state machine attribute is backed by an integer column
|
|
637
|
+
def integer_column?
|
|
638
|
+
return false unless owner_class.respond_to?(:type_for_attribute)
|
|
639
|
+
return false unless owner_class.connected? && owner_class.table_exists?
|
|
640
|
+
|
|
641
|
+
owner_class.type_for_attribute(attribute.to_s).type == :integer
|
|
642
|
+
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionNotEstablished
|
|
643
|
+
false
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Registers a custom AR attribute type so that integer columns transparently
|
|
647
|
+
# convert between state name strings and stored integers.
|
|
648
|
+
# Saves the raw column default first so the conflicting-default check
|
|
649
|
+
# (which fires later, during initial_state=) still compares raw integers.
|
|
650
|
+
def register_integer_type
|
|
651
|
+
@raw_integer_column_default = owner_class.column_defaults[attribute.to_s]
|
|
652
|
+
@integer_type_registered = true
|
|
653
|
+
|
|
654
|
+
# When re-registering (e.g. for an STI subclass that inherited the
|
|
655
|
+
# parent's custom type), unwrap it to keep the column's original type
|
|
656
|
+
# as the passthrough delegate.
|
|
657
|
+
current_type = owner_class.type_for_attribute(attribute.to_s)
|
|
658
|
+
raw_type = current_type.is_a?(StateMachines::Type::Integer) ? current_type.raw_type : current_type
|
|
659
|
+
|
|
660
|
+
owner_class.attribute(attribute.to_s, StateMachines::Type::Integer.new(states, raw_type: raw_type))
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Detect existing enum methods for this attribute
|
|
664
|
+
def detect_existing_enum_methods
|
|
665
|
+
return [] unless owner_class.defined_enums.key?(attribute.to_s)
|
|
666
|
+
|
|
667
|
+
enum_values = owner_class.defined_enums[attribute.to_s]
|
|
668
|
+
methods = []
|
|
669
|
+
|
|
670
|
+
enum_values.each_key do |value|
|
|
671
|
+
# Predicate methods like 'active?'
|
|
672
|
+
predicate = "#{value}?"
|
|
673
|
+
methods << predicate if owner_class.method_defined?(predicate)
|
|
674
|
+
|
|
675
|
+
# Bang methods like 'active!'
|
|
676
|
+
bang_method = "#{value}!"
|
|
677
|
+
methods << bang_method if owner_class.method_defined?(bang_method)
|
|
678
|
+
|
|
679
|
+
# Scope methods (class-level)
|
|
680
|
+
methods << value.to_s if owner_class.respond_to?(value)
|
|
681
|
+
methods << "not_#{value}" if owner_class.respond_to?("not_#{value}")
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
methods
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Generate method name with prefix/suffix based on configuration
|
|
688
|
+
def generate_state_method_name(state_name, method_type)
|
|
689
|
+
return state_name unless enum_integrated?
|
|
690
|
+
|
|
691
|
+
config = enum_integration
|
|
692
|
+
base_name = case method_type
|
|
693
|
+
when :predicate
|
|
694
|
+
"#{state_name}?"
|
|
695
|
+
when :bang
|
|
696
|
+
"#{state_name}!"
|
|
697
|
+
else
|
|
698
|
+
state_name.to_s
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# Apply prefix
|
|
702
|
+
if config[:prefix]
|
|
703
|
+
prefix = config[:prefix] == true ? "#{attribute}_" : "#{config[:prefix]}_"
|
|
704
|
+
base_name = "#{prefix}#{base_name}"
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Apply suffix
|
|
708
|
+
if config[:suffix]
|
|
709
|
+
suffix = config[:suffix] == true ? "_#{attribute}" : "_#{config[:suffix]}"
|
|
710
|
+
base_name = base_name.gsub(/(\?|!)$/, "#{suffix}\\1")
|
|
711
|
+
base_name = "#{base_name}#{suffix}" unless base_name.end_with?('?', '!')
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
base_name
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Generate state machine methods with conflict resolution
|
|
718
|
+
def generate_state_machine_methods
|
|
719
|
+
return unless enum_integrated?
|
|
720
|
+
|
|
721
|
+
# Initialize tracking if not already done
|
|
722
|
+
@processed_states ||= Set.new
|
|
723
|
+
enum_integration[:state_machine_methods] ||= []
|
|
724
|
+
|
|
725
|
+
# Get all states for this machine
|
|
726
|
+
states.each do |state|
|
|
727
|
+
state_name = state.name.to_s
|
|
728
|
+
next if state.nil? # Skip nil state
|
|
729
|
+
next if @processed_states.include?(state_name) # Skip already processed states
|
|
730
|
+
|
|
731
|
+
# Generate predicate method (e.g., status_pending?)
|
|
732
|
+
predicate_method = generate_state_method_name(state_name, :predicate)
|
|
733
|
+
if predicate_method != "#{state_name}?"
|
|
734
|
+
define_state_predicate_method(state_name, predicate_method)
|
|
735
|
+
track_generated_method(predicate_method)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Generate bang method (e.g., status_pending!)
|
|
739
|
+
bang_method = generate_state_method_name(state_name, :bang)
|
|
740
|
+
if bang_method != "#{state_name}!"
|
|
741
|
+
define_state_bang_method(state_name, bang_method)
|
|
742
|
+
track_generated_method(bang_method)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Generate scope methods (e.g., status_pending) if scopes are enabled
|
|
746
|
+
if enum_integration[:scopes]
|
|
747
|
+
scope_method = generate_state_method_name(state_name, :scope)
|
|
748
|
+
if scope_method != state_name
|
|
749
|
+
define_state_scope_method(state_name, scope_method)
|
|
750
|
+
track_generated_method(scope_method)
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Mark this state as processed
|
|
755
|
+
@processed_states.add(state_name)
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Define a prefixed predicate method for a state
|
|
760
|
+
def define_state_predicate_method(state_name, method_name)
|
|
761
|
+
machine_attribute = attribute
|
|
762
|
+
target_state_name = state_name.to_sym
|
|
763
|
+
owner_class.define_method(method_name) do
|
|
764
|
+
machine = self.class.state_machine(machine_attribute)
|
|
765
|
+
machine.states.matches?(self, target_state_name)
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# Define a prefixed bang method for a state
|
|
770
|
+
def define_state_bang_method(state_name, method_name)
|
|
771
|
+
owner_class.define_method(method_name) do
|
|
772
|
+
# Raise an error with actionable guidance
|
|
773
|
+
raise "#{method_name} is a conflict-resolution placeholder. " \
|
|
774
|
+
"Use the original enum method '#{state_name}!' or state machine events instead."
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# Define a prefixed scope method for a state
|
|
779
|
+
def define_state_scope_method(state_name, method_name)
|
|
780
|
+
machine_attribute = attribute
|
|
781
|
+
scope_lambda = lambda do |value = true|
|
|
782
|
+
machine = state_machine(machine_attribute)
|
|
783
|
+
state_value = machine.states[state_name.to_sym].value
|
|
784
|
+
if value
|
|
785
|
+
where(machine_attribute => state_value)
|
|
786
|
+
else
|
|
787
|
+
where.not(machine_attribute => state_value)
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
owner_class.define_singleton_method(method_name, &scope_lambda)
|
|
792
|
+
owner_class.define_singleton_method("not_#{method_name}") do
|
|
793
|
+
public_send(method_name, false)
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# Track generated state machine methods for introspection
|
|
798
|
+
def track_generated_method(method_name)
|
|
799
|
+
return unless enum_integrated?
|
|
800
|
+
|
|
801
|
+
# Use a Set to ensure no duplicates
|
|
802
|
+
enum_integration[:state_machine_methods] ||= []
|
|
803
|
+
return if enum_integration[:state_machine_methods].include?(method_name)
|
|
804
|
+
|
|
805
|
+
enum_integration[:state_machine_methods] << method_name
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Include MachineMethods to make enum integration methods available on machine instances
|
|
810
|
+
include MachineMethods
|
|
811
|
+
|
|
422
812
|
class << self
|
|
813
|
+
attr_accessor :auto_convert_integer_state_attributes
|
|
814
|
+
|
|
423
815
|
# Classes that inherit from ActiveRecord::Base will automatically use
|
|
424
816
|
# the ActiveRecord integration.
|
|
425
817
|
def matching_ancestors
|
|
@@ -434,79 +826,50 @@ module StateMachines
|
|
|
434
826
|
action == :save
|
|
435
827
|
end
|
|
436
828
|
|
|
437
|
-
# Gets the db default for the machine's attribute
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
def owner_class_attribute_default
|
|
446
|
-
if owner_class.connected? && owner_class.table_exists?
|
|
447
|
-
if column = owner_class.columns_hash[attribute.to_s]
|
|
448
|
-
column.default
|
|
449
|
-
end
|
|
450
|
-
end
|
|
451
|
-
end
|
|
829
|
+
# Gets the db default for the machine's attribute.
|
|
830
|
+
# For integer columns the raw pre-type-registration default is returned so
|
|
831
|
+
# that check_conflicting_attribute_default can compare integers to integers.
|
|
832
|
+
def owner_class_attribute_default
|
|
833
|
+
return @raw_integer_column_default if defined?(@raw_integer_column_default)
|
|
834
|
+
return unless owner_class.connected? && owner_class.table_exists?
|
|
835
|
+
|
|
836
|
+
owner_class.column_defaults[attribute.to_s]
|
|
452
837
|
end
|
|
453
838
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
end_eval
|
|
466
|
-
elsif ::ActiveRecord.gem_version >= Gem::Version.new('4.2')
|
|
467
|
-
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
|
468
|
-
def initialize(attributes = nil, options = {})
|
|
469
|
-
scoped_attributes = (attributes || {}).merge(self.class.scope_attributes)
|
|
470
|
-
|
|
471
|
-
super(attributes, options) do |*args|
|
|
472
|
-
self.class.state_machines.initialize_states(self, {}, scoped_attributes)
|
|
473
|
-
yield(*args) if block_given?
|
|
474
|
-
end
|
|
475
|
-
end
|
|
476
|
-
end_eval
|
|
477
|
-
else
|
|
478
|
-
# Initializes static states
|
|
479
|
-
#
|
|
480
|
-
# This is the only available hook where the default set of attributes
|
|
481
|
-
# can be overridden for a new object *prior* to the processing of the
|
|
482
|
-
# attributes passed into #initialize
|
|
483
|
-
define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
|
|
484
|
-
def column_defaults(*) #:nodoc:
|
|
485
|
-
result = super
|
|
486
|
-
# No need to pass in an object, since the overrides will be forced
|
|
487
|
-
self.state_machines.initialize_states(nil, :static => :force, :dynamic => false, :to => result)
|
|
488
|
-
result
|
|
489
|
-
end
|
|
490
|
-
end_eval
|
|
839
|
+
# Checks whether the given state matches the column default. When the
|
|
840
|
+
# custom integer type is registered, auto-indexed states match on their
|
|
841
|
+
# name string while the column default is a raw integer, so the default
|
|
842
|
+
# is also compared through the type's mapping (e.g. 0 matches the first
|
|
843
|
+
# auto-indexed state).
|
|
844
|
+
#
|
|
845
|
+
# @param state [StateMachines::State] the state to compare (the initial state)
|
|
846
|
+
# @return [Boolean] whether the column default represents this state
|
|
847
|
+
def owner_class_attribute_default_matches?(state)
|
|
848
|
+
matches = super
|
|
849
|
+
return matches if matches || !integer_type_registered?
|
|
491
850
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
scoped_attributes = (attributes || {}).merge(self.class.scope_attributes)
|
|
851
|
+
default = owner_class_attribute_default
|
|
852
|
+
state.matches?(owner_class.type_for_attribute(attribute.to_s).deserialize(default))
|
|
853
|
+
end
|
|
496
854
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
855
|
+
def define_state_initializer
|
|
856
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
857
|
+
def initialize(attributes = nil, *)
|
|
858
|
+
super(attributes) do |*args|
|
|
859
|
+
attributes = (attributes || {}).transform_keys { |key| self.class.attribute_aliases[key.to_s] || key }
|
|
860
|
+
scoped_attributes = attributes.merge(self.class.scope_attributes)
|
|
861
|
+
|
|
862
|
+
self.class.state_machines.initialize_states(self, {}, scoped_attributes)
|
|
863
|
+
yield(*args) if block_given?
|
|
501
864
|
end
|
|
502
|
-
|
|
503
|
-
|
|
865
|
+
end
|
|
866
|
+
END_EVAL
|
|
504
867
|
end
|
|
505
868
|
|
|
506
869
|
# Uses around callbacks to run state events if using the :save hook
|
|
507
870
|
def define_action_hook
|
|
508
871
|
if action_hook == :save
|
|
509
|
-
define_helper :instance, <<-
|
|
872
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
510
873
|
def save(*, **)
|
|
511
874
|
self.class.state_machine(#{name.inspect}).send(:around_save, self) { super }
|
|
512
875
|
end
|
|
@@ -519,33 +882,42 @@ module StateMachines
|
|
|
519
882
|
def changed_for_autosave?
|
|
520
883
|
super || self.class.state_machines.any? {|name, machine| machine.action == :save && machine.read(self, :event)}
|
|
521
884
|
end
|
|
522
|
-
|
|
885
|
+
END_EVAL
|
|
523
886
|
else
|
|
524
887
|
super
|
|
525
888
|
end
|
|
526
889
|
end
|
|
527
890
|
|
|
528
891
|
# Runs state events around the machine's :save action
|
|
529
|
-
def around_save(object)
|
|
530
|
-
|
|
892
|
+
def around_save(object, &)
|
|
893
|
+
# Pass fiber: false to avoid deadlocks with ActiveRecord's LoadInterlockAwareMonitor
|
|
894
|
+
object.class.state_machines.transitions(object, action, fiber: false).perform(&)
|
|
531
895
|
end
|
|
532
896
|
|
|
533
897
|
# Creates a scope for finding records *with* a particular state or
|
|
534
898
|
# states for the attribute
|
|
535
|
-
def create_with_scope(
|
|
536
|
-
|
|
899
|
+
def create_with_scope(_name)
|
|
900
|
+
attr_name = attribute
|
|
901
|
+
lambda do |klass, values|
|
|
902
|
+
if values.present?
|
|
903
|
+
klass.where(attr_name => values)
|
|
904
|
+
else
|
|
905
|
+
klass.all
|
|
906
|
+
end
|
|
907
|
+
end
|
|
537
908
|
end
|
|
538
909
|
|
|
539
910
|
# Creates a scope for finding records *without* a particular state or
|
|
540
911
|
# states for the attribute
|
|
541
|
-
def create_without_scope(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
912
|
+
def create_without_scope(_name)
|
|
913
|
+
attr_name = attribute
|
|
914
|
+
lambda do |klass, values|
|
|
915
|
+
if values.present?
|
|
916
|
+
klass.where.not(attr_name => values)
|
|
917
|
+
else
|
|
918
|
+
klass.all
|
|
919
|
+
end
|
|
920
|
+
end
|
|
549
921
|
end
|
|
550
922
|
|
|
551
923
|
# Runs a new database transaction, rolling back any changes by raising
|
|
@@ -554,7 +926,7 @@ module StateMachines
|
|
|
554
926
|
def transaction(object)
|
|
555
927
|
result = nil
|
|
556
928
|
object.class.transaction do
|
|
557
|
-
raise ::ActiveRecord::Rollback unless result = yield
|
|
929
|
+
raise ::ActiveRecord::Rollback unless (result = yield)
|
|
558
930
|
end
|
|
559
931
|
result
|
|
560
932
|
end
|
|
@@ -565,17 +937,12 @@ module StateMachines
|
|
|
565
937
|
|
|
566
938
|
private
|
|
567
939
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
# breaks both ancestor lookups and defined?(super). Need to special-case
|
|
575
|
-
# the existence of query attribute methods.
|
|
576
|
-
def owner_class_ancestor_has_method?(scope, method)
|
|
577
|
-
scope == :instance && method == "#{attribute}?" ? owner_class : super
|
|
578
|
-
end
|
|
940
|
+
# ActiveModel's use of method_missing / respond_to for attribute methods
|
|
941
|
+
# breaks both ancestor lookups and defined?(super). Need to special-case
|
|
942
|
+
# the existence of query attribute methods.
|
|
943
|
+
def owner_class_ancestor_has_method?(scope, method)
|
|
944
|
+
scope == :instance && method == "#{attribute}?" ? owner_class : super
|
|
945
|
+
end
|
|
579
946
|
end
|
|
580
947
|
register(ActiveRecord)
|
|
581
948
|
end
|