state_machines-activemodel 0.8.0 → 0.101.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 +8 -27
- data/lib/state_machines/integrations/active_model/locale.rb +7 -3
- data/lib/state_machines/integrations/active_model/version.rb +3 -1
- data/lib/state_machines/integrations/active_model.rb +74 -99
- data/lib/state_machines-activemodel.rb +2 -0
- data/test/event_human_name_test.rb +176 -0
- data/test/human_name_preservation_test.rb +75 -0
- data/test/integration_test.rb +4 -2
- data/test/machine_by_default_test.rb +3 -1
- data/test/machine_errors_test.rb +3 -1
- data/test/machine_initialization_compatibility_test.rb +57 -0
- data/test/machine_multiple_test.rb +4 -2
- data/test/machine_with_callbacks_test.rb +3 -1
- data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +8 -5
- data/test/machine_with_dirty_attribute_and_state_events_test.rb +7 -5
- data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +11 -8
- data/test/machine_with_dirty_attributes_during_loopback_test.rb +7 -5
- data/test/machine_with_dirty_attributes_test.rb +10 -8
- data/test/machine_with_dynamic_initial_state_test.rb +3 -1
- data/test/machine_with_events_test.rb +3 -1
- data/test/machine_with_failed_after_callbacks_test.rb +3 -1
- data/test/machine_with_failed_before_callbacks_test.rb +3 -1
- data/test/machine_with_initialized_aliased_attribute_test.rb +44 -0
- data/test/machine_with_initialized_state_test.rb +3 -1
- data/test/machine_with_internationalization_test.rb +12 -10
- data/test/machine_with_model_state_attribute_test.rb +3 -1
- data/test/machine_with_non_model_state_attribute_undefined_test.rb +5 -4
- data/test/machine_with_state_driven_validations_test.rb +4 -2
- data/test/machine_with_states_test.rb +3 -1
- data/test/machine_with_static_initial_state_test.rb +3 -1
- data/test/machine_with_validations_and_custom_attribute_test.rb +3 -1
- data/test/machine_with_validations_test.rb +3 -1
- data/test/state_human_name_test.rb +152 -0
- data/test/test_helper.rb +26 -32
- metadata +18 -24
- data/.gitignore +0 -22
- data/.travis.yml +0 -21
- data/Appraisals +0 -20
- data/Gemfile +0 -8
- data/Rakefile +0 -9
- data/gemfiles/active_model_5.1.gemfile +0 -11
- data/gemfiles/active_model_5.2.gemfile +0 -11
- data/gemfiles/active_model_6.0.gemfile +0 -11
- data/gemfiles/active_model_6.1.gemfile +0 -11
- data/gemfiles/active_model_edge.gemfile +0 -11
- data/state_machines-activemodel.gemspec +0 -28
- data/test/files/en.yml +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca8150306fc5ce879e2c381e4689b049b5208189f4e15efb4f6a0d378647cbb5
|
|
4
|
+
data.tar.gz: 1bad79de7e3337208c134dcea4fbaa90eee5677182dca38f6bfd4b9b943546b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d8cca8f00006c4a43b9c82fade3babc662027b7d5971c5d009c9b6da087cc3d93a4ed60a2f8656cb59346c1dfab115405f23db2237b25e185ac718ca09f8e1f
|
|
7
|
+
data.tar.gz: 8a39da61112b9c6e7869e1d4779381062857ea36ffd1e026031c04852f30561d3cacc60e8a0a98daa112576924009d109f02fb1f738672996a7ec5a89b2ef6bc
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
[](https://codeclimate.com/github/state-machines/state_machines-activemodel)
|
|
1
|
+

|
|
3
2
|
|
|
4
3
|
# StateMachines ActiveModel Integration
|
|
5
4
|
|
|
@@ -23,7 +22,7 @@ Or install it yourself as:
|
|
|
23
22
|
|
|
24
23
|
## Dependencies
|
|
25
24
|
|
|
26
|
-
Active Model
|
|
25
|
+
Active Model 7.1+
|
|
27
26
|
|
|
28
27
|
## Usage
|
|
29
28
|
|
|
@@ -36,19 +35,19 @@ class Vehicle
|
|
|
36
35
|
attr_accessor :state
|
|
37
36
|
define_attribute_methods [:state]
|
|
38
37
|
|
|
39
|
-
state_machine :
|
|
40
|
-
before_transition :
|
|
41
|
-
after_transition any
|
|
38
|
+
state_machine initial: :parked do
|
|
39
|
+
before_transition parked: any - :parked, do: :put_on_seatbelt
|
|
40
|
+
after_transition any: :parked do |vehicle, transition|
|
|
42
41
|
vehicle.seatbelt = 'off'
|
|
43
42
|
end
|
|
44
43
|
around_transition :benchmark
|
|
45
44
|
|
|
46
|
-
event :
|
|
47
|
-
transition :
|
|
45
|
+
event ignite: do
|
|
46
|
+
transition parked: :idling
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
state :first_gear, :second_gear do
|
|
51
|
-
|
|
50
|
+
validates :seatbelt_on, presence: true
|
|
52
51
|
end
|
|
53
52
|
end
|
|
54
53
|
|
|
@@ -62,24 +61,6 @@ class Vehicle
|
|
|
62
61
|
...
|
|
63
62
|
end
|
|
64
63
|
end
|
|
65
|
-
|
|
66
|
-
class VehicleObserver < ActiveModel::Observer
|
|
67
|
-
# Callback for :ignite event *before* the transition is performed
|
|
68
|
-
def before_ignite(vehicle, transition)
|
|
69
|
-
# log message
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Generic transition callback *after* the transition is performed
|
|
73
|
-
def after_transition(vehicle, transition)
|
|
74
|
-
Audit.log(vehicle, transition)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Generic callback after the transition fails to perform
|
|
78
|
-
def after_failure_to_transition(vehicle, transition)
|
|
79
|
-
Audit.error(vehicle, transition)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
64
|
```
|
|
84
65
|
|
|
85
66
|
## Contributing
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Use lazy evaluation to avoid circular dependencies with frozen default_messages
|
|
4
|
+
# This ensures messages can be updated after gem loading while maintaining thread safety
|
|
1
5
|
{ en: {
|
|
2
6
|
activemodel: {
|
|
3
7
|
errors: {
|
|
4
8
|
messages: {
|
|
5
|
-
invalid: StateMachines::Machine.default_messages[:invalid],
|
|
6
|
-
invalid_event: StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'],
|
|
7
|
-
invalid_transition: StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}']
|
|
9
|
+
invalid: lambda { |*| StateMachines::Machine.default_messages[:invalid] },
|
|
10
|
+
invalid_event: lambda { |*| StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'] },
|
|
11
|
+
invalid_transition: lambda { |*| StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}'] }
|
|
8
12
|
}
|
|
9
13
|
}
|
|
10
14
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_model'
|
|
2
4
|
require 'active_support/core_ext/hash/keys'
|
|
3
|
-
require 'active_support/core_ext/module/attribute_accessors
|
|
5
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
|
4
6
|
require 'state_machines'
|
|
5
7
|
require 'state_machines/integrations/base'
|
|
6
8
|
require 'state_machines/integrations/active_model/version'
|
|
7
9
|
|
|
8
10
|
module StateMachines
|
|
9
|
-
module Integrations
|
|
11
|
+
module Integrations # :nodoc:
|
|
10
12
|
# Adds support for integrating state machines with ActiveModel classes.
|
|
11
13
|
#
|
|
12
14
|
# == Examples
|
|
@@ -25,9 +27,9 @@ module StateMachines
|
|
|
25
27
|
# attr_accessor :state
|
|
26
28
|
# define_attribute_methods [:state]
|
|
27
29
|
#
|
|
28
|
-
# state_machine :
|
|
30
|
+
# state_machine initial: :parked do
|
|
29
31
|
# event :ignite do
|
|
30
|
-
# transition :
|
|
32
|
+
# transition parked: :idling
|
|
31
33
|
# end
|
|
32
34
|
# end
|
|
33
35
|
# end
|
|
@@ -45,7 +47,7 @@ module StateMachines
|
|
|
45
47
|
# include ActiveModel::Validations
|
|
46
48
|
# attr_accessor :state
|
|
47
49
|
#
|
|
48
|
-
# state_machine :
|
|
50
|
+
# state_machine action: :save do
|
|
49
51
|
# ...
|
|
50
52
|
# end
|
|
51
53
|
#
|
|
@@ -83,7 +85,7 @@ module StateMachines
|
|
|
83
85
|
# state_machine do
|
|
84
86
|
# ...
|
|
85
87
|
# state :first_gear, :second_gear do
|
|
86
|
-
# validate {|vehicle| vehicle.speed_is_legal}
|
|
88
|
+
# validate { |vehicle| vehicle.speed_is_legal }
|
|
87
89
|
# end
|
|
88
90
|
# end
|
|
89
91
|
# end
|
|
@@ -115,38 +117,44 @@ module StateMachines
|
|
|
115
117
|
# Beware that public event attributes mean that events can be fired
|
|
116
118
|
# whenever mass-assignment is being used. If you want to prevent malicious
|
|
117
119
|
# users from tampering with events through URLs / forms, the attribute
|
|
118
|
-
# should be protected
|
|
120
|
+
# should be protected using Strong Parameters in your controllers:
|
|
119
121
|
#
|
|
120
122
|
# class Vehicle
|
|
121
|
-
# include ActiveModel::MassAssignmentSecurity
|
|
122
123
|
# attr_accessor :state
|
|
123
124
|
#
|
|
124
|
-
# attr_protected :state_event
|
|
125
|
-
# # attr_accessible ... # Alternative technique
|
|
126
|
-
#
|
|
127
125
|
# state_machine do
|
|
128
126
|
# ...
|
|
129
127
|
# end
|
|
130
128
|
# end
|
|
131
129
|
#
|
|
130
|
+
# # In your controller
|
|
131
|
+
# def vehicle_params
|
|
132
|
+
# params.require(:vehicle).permit(:attribute1, :attribute2) # Exclude :state_event
|
|
133
|
+
# end
|
|
134
|
+
#
|
|
132
135
|
# If you want to only have *some* events be able to fire via mass-assignment,
|
|
133
|
-
# you can build two state machines (one
|
|
136
|
+
# you can build two state machines (one private and one public) like so:
|
|
134
137
|
#
|
|
135
138
|
# class Vehicle
|
|
136
139
|
# attr_accessor :state
|
|
137
140
|
#
|
|
138
|
-
# attr_protected :state_event # Prevent access to events in the first machine
|
|
139
|
-
#
|
|
140
141
|
# state_machine do
|
|
141
142
|
# # Define private events here
|
|
142
143
|
# end
|
|
143
144
|
#
|
|
144
145
|
# # Public machine targets the same state as the private machine
|
|
145
|
-
# state_machine :public_state, :
|
|
146
|
+
# state_machine :public_state, attribute: :state do
|
|
146
147
|
# # Define public events here
|
|
147
148
|
# end
|
|
148
149
|
# end
|
|
149
150
|
#
|
|
151
|
+
# # In your controller
|
|
152
|
+
# def vehicle_params
|
|
153
|
+
# # Only permit events from the public state machine
|
|
154
|
+
# params.require(:vehicle).permit(:attribute1, :attribute2, :public_state_event)
|
|
155
|
+
# # The private state_event is not permitted
|
|
156
|
+
# end
|
|
157
|
+
#
|
|
150
158
|
# == Callbacks
|
|
151
159
|
#
|
|
152
160
|
# All before/after transition callbacks defined for ActiveModel models
|
|
@@ -159,7 +167,7 @@ module StateMachines
|
|
|
159
167
|
# include ActiveModel::Validations
|
|
160
168
|
# attr_accessor :state
|
|
161
169
|
#
|
|
162
|
-
# state_machine :
|
|
170
|
+
# state_machine initial: :parked do
|
|
163
171
|
# before_transition any => :idling do |vehicle|
|
|
164
172
|
# vehicle.put_on_seatbelt
|
|
165
173
|
# end
|
|
@@ -169,7 +177,7 @@ module StateMachines
|
|
|
169
177
|
# end
|
|
170
178
|
#
|
|
171
179
|
# event :ignite do
|
|
172
|
-
# transition :
|
|
180
|
+
# transition parked: :idling
|
|
173
181
|
# end
|
|
174
182
|
# end
|
|
175
183
|
#
|
|
@@ -181,62 +189,6 @@ module StateMachines
|
|
|
181
189
|
# Note, also, that the transition can be accessed by simply defining
|
|
182
190
|
# additional arguments in the callback block.
|
|
183
191
|
#
|
|
184
|
-
# == Observers
|
|
185
|
-
#
|
|
186
|
-
# In order to hook in observer support for your application, the
|
|
187
|
-
# ActiveModel::Observing feature must be included. Because of the way
|
|
188
|
-
# ActiveModel observers are designed, there is less flexibility around the
|
|
189
|
-
# specific transitions that can be hooked in. However, a large number of
|
|
190
|
-
# hooks *are* supported. For example, if a transition for a object's
|
|
191
|
-
# +state+ attribute changes the state from +parked+ to +idling+ via the
|
|
192
|
-
# +ignite+ event, the following observer methods are supported:
|
|
193
|
-
# * before/after/after_failure_to-_ignite_from_parked_to_idling
|
|
194
|
-
# * before/after/after_failure_to-_ignite_from_parked
|
|
195
|
-
# * before/after/after_failure_to-_ignite_to_idling
|
|
196
|
-
# * before/after/after_failure_to-_ignite
|
|
197
|
-
# * before/after/after_failure_to-_transition_state_from_parked_to_idling
|
|
198
|
-
# * before/after/after_failure_to-_transition_state_from_parked
|
|
199
|
-
# * before/after/after_failure_to-_transition_state_to_idling
|
|
200
|
-
# * before/after/after_failure_to-_transition_state
|
|
201
|
-
# * before/after/after_failure_to-_transition
|
|
202
|
-
#
|
|
203
|
-
# The following class shows an example of some of these hooks:
|
|
204
|
-
#
|
|
205
|
-
# class VehicleObserver < ActiveModel::Observer
|
|
206
|
-
# # Callback for :ignite event *before* the transition is performed
|
|
207
|
-
# def before_ignite(vehicle, transition)
|
|
208
|
-
# # log message
|
|
209
|
-
# end
|
|
210
|
-
#
|
|
211
|
-
# # Callback for :ignite event *after* the transition has been performed
|
|
212
|
-
# def after_ignite(vehicle, transition)
|
|
213
|
-
# # put on seatbelt
|
|
214
|
-
# end
|
|
215
|
-
#
|
|
216
|
-
# # Generic transition callback *before* the transition is performed
|
|
217
|
-
# def after_transition(vehicle, transition)
|
|
218
|
-
# Audit.log(vehicle, transition)
|
|
219
|
-
# end
|
|
220
|
-
#
|
|
221
|
-
# def after_failure_to_transition(vehicle, transition)
|
|
222
|
-
# Audit.error(vehicle, transition)
|
|
223
|
-
# end
|
|
224
|
-
# end
|
|
225
|
-
#
|
|
226
|
-
# More flexible transition callbacks can be defined directly within the
|
|
227
|
-
# model as described in StateMachine::Machine#before_transition
|
|
228
|
-
# and StateMachine::Machine#after_transition.
|
|
229
|
-
#
|
|
230
|
-
# To define a single observer for multiple state machines:
|
|
231
|
-
#
|
|
232
|
-
# class StateMachineObserver < ActiveModel::Observer
|
|
233
|
-
# observe Vehicle, Switch, Project
|
|
234
|
-
#
|
|
235
|
-
# def after_transition(object, transition)
|
|
236
|
-
# Audit.log(object, transition)
|
|
237
|
-
# end
|
|
238
|
-
# end
|
|
239
|
-
#
|
|
240
192
|
# == Internationalization
|
|
241
193
|
#
|
|
242
194
|
# Any error message that is generated from performing invalid transitions
|
|
@@ -306,9 +258,9 @@ module StateMachines
|
|
|
306
258
|
# include ActiveModel::Dirty
|
|
307
259
|
# attr_accessor :state
|
|
308
260
|
#
|
|
309
|
-
# state_machine :
|
|
261
|
+
# state_machine initial: :parked do
|
|
310
262
|
# event :park do
|
|
311
|
-
# transition :
|
|
263
|
+
# transition parked: :parked, ...
|
|
312
264
|
# end
|
|
313
265
|
# end
|
|
314
266
|
# end
|
|
@@ -320,7 +272,7 @@ module StateMachines
|
|
|
320
272
|
#
|
|
321
273
|
# class Vehicle
|
|
322
274
|
# ...
|
|
323
|
-
# state_machine :
|
|
275
|
+
# state_machine initial: :parked do
|
|
324
276
|
# before_transition all => same do |vehicle|
|
|
325
277
|
# vehicle.state_will_change!
|
|
326
278
|
#
|
|
@@ -342,7 +294,7 @@ module StateMachines
|
|
|
342
294
|
# module StateMachine::Integrations::MyORM
|
|
343
295
|
# include ActiveModel
|
|
344
296
|
#
|
|
345
|
-
# mattr_accessor(:defaults) { :
|
|
297
|
+
# mattr_accessor(:defaults) { { action: :persist } }
|
|
346
298
|
#
|
|
347
299
|
# def self.matches?(klass)
|
|
348
300
|
# defined?(::MyORM::Base) && klass <= ::MyORM::Base
|
|
@@ -372,22 +324,19 @@ module StateMachines
|
|
|
372
324
|
|
|
373
325
|
# Adds a validation error to the given object
|
|
374
326
|
def invalidate(object, attribute, message, values = [])
|
|
375
|
-
|
|
376
|
-
attribute = self.attribute(attribute)
|
|
377
|
-
options = values.reduce({}) do |h, (key, value)|
|
|
378
|
-
h[key] = value
|
|
379
|
-
h
|
|
380
|
-
end
|
|
327
|
+
return unless supports_validations?
|
|
381
328
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
329
|
+
attribute = self.attribute(attribute)
|
|
330
|
+
options = values.to_h
|
|
331
|
+
|
|
332
|
+
default_options = default_error_message_options(object, attribute, message)
|
|
333
|
+
object.errors.add(attribute, message, **options, **default_options)
|
|
385
334
|
end
|
|
386
335
|
|
|
387
336
|
# Describes the current validation errors on the given object. If none
|
|
388
337
|
# are specific, then the default error is interpeted as a "halt".
|
|
389
338
|
def errors_for(object)
|
|
390
|
-
object.errors.empty? ? 'Transition halted' : object.errors.full_messages
|
|
339
|
+
object.errors.empty? ? 'Transition halted' : object.errors.full_messages.join(', ')
|
|
391
340
|
end
|
|
392
341
|
|
|
393
342
|
# Resets any errors previously added when invalidating the given object
|
|
@@ -396,18 +345,32 @@ module StateMachines
|
|
|
396
345
|
end
|
|
397
346
|
|
|
398
347
|
# Runs state events around the object's validation process
|
|
399
|
-
def around_validation(object)
|
|
400
|
-
object.class.state_machines.transitions(object, action, after: false).perform
|
|
348
|
+
def around_validation(object, &)
|
|
349
|
+
object.class.state_machines.transitions(object, action, after: false).perform(&)
|
|
401
350
|
end
|
|
402
351
|
|
|
403
352
|
protected
|
|
404
353
|
|
|
405
354
|
def define_state_initializer
|
|
406
|
-
define_helper :instance, <<-
|
|
407
|
-
def initialize(params =
|
|
408
|
-
|
|
355
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
356
|
+
def initialize(params = nil, **kwargs)
|
|
357
|
+
# Support both positional hash and keyword arguments
|
|
358
|
+
attrs = params.nil? ? kwargs : params
|
|
359
|
+
#{' '}
|
|
360
|
+
attrs.transform_keys! do |key|
|
|
361
|
+
self.class.attribute_aliases[key.to_s] || key.to_s
|
|
362
|
+
end if self.class.respond_to?(:attribute_aliases)
|
|
363
|
+
|
|
364
|
+
# Call super with the appropriate arguments based on what we received
|
|
365
|
+
self.class.state_machines.initialize_states(self, {}, attrs) do
|
|
366
|
+
if params
|
|
367
|
+
super(params)
|
|
368
|
+
else
|
|
369
|
+
super(**kwargs)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
409
372
|
end
|
|
410
|
-
|
|
373
|
+
END_EVAL
|
|
411
374
|
end
|
|
412
375
|
|
|
413
376
|
# Whether validations are supported in the integration. Only true if
|
|
@@ -425,7 +388,7 @@ module StateMachines
|
|
|
425
388
|
|
|
426
389
|
# Gets the terminator to use for callbacks
|
|
427
390
|
def callback_terminator
|
|
428
|
-
@
|
|
391
|
+
@callback_terminator ||= ->(result) { result == false }
|
|
429
392
|
end
|
|
430
393
|
|
|
431
394
|
# Determines the base scope to use when looking up translations
|
|
@@ -455,7 +418,7 @@ module StateMachines
|
|
|
455
418
|
# Generate all possible translation keys
|
|
456
419
|
translations = ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}" }
|
|
457
420
|
translations.concat(ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}" })
|
|
458
|
-
translations.
|
|
421
|
+
translations.push(:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase)
|
|
459
422
|
I18n.translate(translations.shift, default: translations, scope: [i18n_scope(klass), :state_machines])
|
|
460
423
|
end
|
|
461
424
|
|
|
@@ -469,10 +432,12 @@ module StateMachines
|
|
|
469
432
|
def define_state_accessor
|
|
470
433
|
name = self.name
|
|
471
434
|
|
|
435
|
+
return unless supports_validations?
|
|
436
|
+
|
|
472
437
|
owner_class.validates_each(attribute) do |object|
|
|
473
438
|
machine = object.class.state_machine(name)
|
|
474
439
|
machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
|
|
475
|
-
end
|
|
440
|
+
end
|
|
476
441
|
end
|
|
477
442
|
|
|
478
443
|
# Adds hooks into validation for automatically firing events
|
|
@@ -490,7 +455,7 @@ module StateMachines
|
|
|
490
455
|
# Creates a new callback in the callback chain, always inserting it
|
|
491
456
|
# before the default Observer callbacks that were created after
|
|
492
457
|
# initialization.
|
|
493
|
-
def add_callback(type, options, &
|
|
458
|
+
def add_callback(type, options, &)
|
|
494
459
|
options[:terminator] = callback_terminator
|
|
495
460
|
super
|
|
496
461
|
end
|
|
@@ -498,14 +463,24 @@ module StateMachines
|
|
|
498
463
|
# Configures new states with the built-in humanize scheme
|
|
499
464
|
def add_states(*)
|
|
500
465
|
super.each do |new_state|
|
|
501
|
-
|
|
466
|
+
# Only set the translation lambda if human_name is the default auto-generated value
|
|
467
|
+
# This preserves user-specified human names while still applying translations for defaults
|
|
468
|
+
default_human_name = new_state.name ? new_state.name.to_s.tr('_', ' ') : 'nil'
|
|
469
|
+
if new_state.human_name == default_human_name
|
|
470
|
+
new_state.human_name = ->(state, klass) { translate(klass, :state, state.name) }
|
|
471
|
+
end
|
|
502
472
|
end
|
|
503
473
|
end
|
|
504
474
|
|
|
505
475
|
# Configures new event with the built-in humanize scheme
|
|
506
476
|
def add_events(*)
|
|
507
477
|
super.each do |new_event|
|
|
508
|
-
|
|
478
|
+
# Only set the translation lambda if human_name is the default auto-generated value
|
|
479
|
+
# This preserves user-specified human names while still applying translations for defaults
|
|
480
|
+
default_human_name = new_event.name ? new_event.name.to_s.tr('_', ' ') : 'nil'
|
|
481
|
+
if new_event.human_name == default_human_name
|
|
482
|
+
new_event.human_name = ->(event, klass) { translate(klass, :event, event.name) }
|
|
483
|
+
end
|
|
509
484
|
end
|
|
510
485
|
end
|
|
511
486
|
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
class EventHumanNameTest < BaseTestCase
|
|
6
|
+
def setup
|
|
7
|
+
@model = new_model do
|
|
8
|
+
include ActiveModel::Validations
|
|
9
|
+
attr_accessor :status
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_should_allow_custom_human_name_on_event
|
|
14
|
+
machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
|
|
15
|
+
event :start, human_name: 'Start Engine' do
|
|
16
|
+
transition parked: :running
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
event :stop do
|
|
20
|
+
transition running: :parked
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
event :pause, human_name: 'Temporarily Pause' do
|
|
24
|
+
transition running: :paused
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
assert_equal 'Start Engine', machine.events[:start].human_name(@model)
|
|
29
|
+
assert_equal 'Temporarily Pause', machine.events[:pause].human_name(@model)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_should_not_override_custom_event_human_name_with_translation
|
|
33
|
+
# Set up I18n translations
|
|
34
|
+
I18n.backend.store_translations(:en, {
|
|
35
|
+
activemodel: {
|
|
36
|
+
state_machines: {
|
|
37
|
+
events: {
|
|
38
|
+
ignite: 'Translation for Ignite',
|
|
39
|
+
park: 'Translation for Park',
|
|
40
|
+
repair: 'Translation for Repair'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
|
|
47
|
+
event :ignite, human_name: 'Custom Ignition' do
|
|
48
|
+
transition parked: :idling
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
event :park do
|
|
52
|
+
transition idling: :parked
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
event :repair, human_name: 'Custom Repair Process' do
|
|
56
|
+
transition any => :parked
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Custom human names should be preserved
|
|
61
|
+
assert_equal 'Custom Ignition', machine.events[:ignite].human_name(@model)
|
|
62
|
+
assert_equal 'Custom Repair Process', machine.events[:repair].human_name(@model)
|
|
63
|
+
|
|
64
|
+
# Event without custom human_name should use translation
|
|
65
|
+
assert_equal 'Translation for Park', machine.events[:park].human_name(@model)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_should_allow_custom_event_human_name_as_string
|
|
69
|
+
machine = StateMachines::Machine.new(@model, :status) do
|
|
70
|
+
event :activate, human_name: 'Turn On'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
assert_equal 'Turn On', machine.events[:activate].human_name(@model)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_should_allow_custom_event_human_name_as_lambda
|
|
77
|
+
machine = StateMachines::Machine.new(@model, :status) do
|
|
78
|
+
event :process, human_name: ->(event, klass) { "#{klass.name}: #{event.name.to_s.capitalize} Action" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
assert_equal 'Foo: Process Action', machine.events[:process].human_name(@model)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_should_use_default_translation_when_no_custom_event_human_name
|
|
85
|
+
machine = StateMachines::Machine.new(@model, :status) do
|
|
86
|
+
event :idle
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Should fall back to humanized version when no translation exists
|
|
90
|
+
assert_equal 'idle', machine.events[:idle].human_name(@model)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_should_handle_nil_event_human_name
|
|
94
|
+
machine = StateMachines::Machine.new(@model, :status) do
|
|
95
|
+
event :wait
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Explicitly set to nil
|
|
99
|
+
machine.events[:wait].human_name = nil
|
|
100
|
+
|
|
101
|
+
# When human_name is nil, Event#human_name returns nil
|
|
102
|
+
assert_nil machine.events[:wait].human_name(@model)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_should_preserve_event_human_name_through_multiple_definitions
|
|
106
|
+
machine = StateMachines::Machine.new(@model, :status, initial: :draft)
|
|
107
|
+
|
|
108
|
+
# First define event with custom human name
|
|
109
|
+
machine.event :publish, human_name: 'Make Public' do
|
|
110
|
+
transition draft: :published
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Redefine the same event (this should not override the human_name)
|
|
114
|
+
machine.event :publish do
|
|
115
|
+
transition pending: :published
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
assert_equal 'Make Public', machine.events[:publish].human_name(@model)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_should_work_with_state_machine_helper_method
|
|
122
|
+
@model.class_eval do
|
|
123
|
+
state_machine :status, initial: :pending do
|
|
124
|
+
event :approve, human_name: 'Grant Approval' do
|
|
125
|
+
transition pending: :approved
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
event :reject do
|
|
129
|
+
transition pending: :rejected
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
machine = @model.state_machine(:status)
|
|
135
|
+
assert_equal 'Grant Approval', machine.events[:approve].human_name(@model)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_should_handle_complex_i18n_lookup_with_custom_event_human_name
|
|
139
|
+
# Set up complex I18n structure
|
|
140
|
+
I18n.backend.store_translations(:en, {
|
|
141
|
+
activemodel: {
|
|
142
|
+
state_machines: {
|
|
143
|
+
foo: {
|
|
144
|
+
status: {
|
|
145
|
+
events: {
|
|
146
|
+
submit: 'Model Specific Submit'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
status: {
|
|
151
|
+
events: {
|
|
152
|
+
submit: 'Machine Specific Submit'
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
events: {
|
|
156
|
+
submit: 'Generic Submit'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
machine = StateMachines::Machine.new(@model, :status) do
|
|
163
|
+
event :submit, human_name: 'Send for Review' do
|
|
164
|
+
transition draft: :pending
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Should use the custom human_name, not any of the I18n translations
|
|
169
|
+
assert_equal 'Send for Review', machine.events[:submit].human_name(@model)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def teardown
|
|
173
|
+
# Clear I18n translations after each test
|
|
174
|
+
I18n.backend.reload!
|
|
175
|
+
end
|
|
176
|
+
end
|