spree-state_machine 2.0.0.beta1
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 +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +12 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +502 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +1246 -0
- data/Rakefile +20 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +14 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +13 -0
- data/examples/car.rb +21 -0
- data/examples/doc/AutoShop.html +2856 -0
- data/examples/doc/AutoShop_state.png +0 -0
- data/examples/doc/Car.html +919 -0
- data/examples/doc/Car_state.png +0 -0
- data/examples/doc/TrafficLight.html +2230 -0
- data/examples/doc/TrafficLight_state.png +0 -0
- data/examples/doc/Vehicle.html +7921 -0
- data/examples/doc/Vehicle_state.png +0 -0
- data/examples/doc/_index.html +136 -0
- data/examples/doc/class_list.html +47 -0
- data/examples/doc/css/common.css +1 -0
- data/examples/doc/css/full_list.css +55 -0
- data/examples/doc/css/style.css +322 -0
- data/examples/doc/file_list.html +46 -0
- data/examples/doc/frames.html +13 -0
- data/examples/doc/index.html +136 -0
- data/examples/doc/js/app.js +205 -0
- data/examples/doc/js/full_list.js +173 -0
- data/examples/doc/js/jquery.js +16 -0
- data/examples/doc/method_list.html +734 -0
- data/examples/doc/top-level-namespace.html +105 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +7 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view__form.html.erb +34 -0
- data/examples/rails-rest/view_edit.html.erb +6 -0
- data/examples/rails-rest/view_index.html.erb +25 -0
- data/examples/rails-rest/view_new.html.erb +5 -0
- data/examples/rails-rest/view_show.html.erb +19 -0
- data/examples/traffic_light.rb +9 -0
- data/examples/vehicle.rb +33 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/branch.rb +225 -0
- data/lib/state_machine/callback.rb +236 -0
- data/lib/state_machine/core.rb +7 -0
- data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
- data/lib/state_machine/core_ext.rb +2 -0
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +87 -0
- data/lib/state_machine/event.rb +257 -0
- data/lib/state_machine/event_collection.rb +141 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/graph.rb +92 -0
- data/lib/state_machine/helper_module.rb +17 -0
- data/lib/state_machine/initializers/rails.rb +25 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +33 -0
- data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
- data/lib/state_machine/integrations/active_model/versions.rb +31 -0
- data/lib/state_machine/integrations/active_model.rb +585 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record/versions.rb +123 -0
- data/lib/state_machine/integrations/active_record.rb +525 -0
- data/lib/state_machine/integrations/base.rb +100 -0
- data/lib/state_machine/integrations.rb +121 -0
- data/lib/state_machine/machine.rb +2287 -0
- data/lib/state_machine/machine_collection.rb +74 -0
- data/lib/state_machine/macro_methods.rb +522 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +222 -0
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +297 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/state_context.rb +138 -0
- data/lib/state_machine/transition.rb +470 -0
- data/lib/state_machine/transition_collection.rb +245 -0
- data/lib/state_machine/version.rb +3 -0
- data/lib/state_machine/yard/handlers/base.rb +32 -0
- data/lib/state_machine/yard/handlers/event.rb +25 -0
- data/lib/state_machine/yard/handlers/machine.rb +344 -0
- data/lib/state_machine/yard/handlers/state.rb +25 -0
- data/lib/state_machine/yard/handlers/transition.rb +47 -0
- data/lib/state_machine/yard/handlers.rb +12 -0
- data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
- data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
- data/lib/state_machine/yard/templates.rb +3 -0
- data/lib/state_machine/yard.rb +8 -0
- data/lib/state_machine.rb +8 -0
- data/lib/yard-state_machine.rb +2 -0
- data/state_machine.gemspec +22 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +15 -0
- data/test/functional/state_machine_test.rb +1066 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/branch_test.rb +969 -0
- data/test/unit/callback_test.rb +704 -0
- data/test/unit/error_test.rb +43 -0
- data/test/unit/eval_helpers_test.rb +270 -0
- data/test/unit/event_collection_test.rb +398 -0
- data/test/unit/event_test.rb +1196 -0
- data/test/unit/graph_test.rb +98 -0
- data/test/unit/helper_module_test.rb +17 -0
- data/test/unit/integrations/active_model_test.rb +1245 -0
- data/test/unit/integrations/active_record_test.rb +2551 -0
- data/test/unit/integrations/base_test.rb +104 -0
- data/test/unit/integrations_test.rb +71 -0
- data/test/unit/invalid_event_test.rb +20 -0
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +115 -0
- data/test/unit/machine_collection_test.rb +603 -0
- data/test/unit/machine_test.rb +3395 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +362 -0
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +352 -0
- data/test/unit/state_context_test.rb +441 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +1101 -0
- data/test/unit/transition_collection_test.rb +2168 -0
- data/test/unit/transition_test.rb +1558 -0
- metadata +264 -0
@@ -0,0 +1,585 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
# Adds support for integrating state machines with ActiveModel classes.
|
4
|
+
#
|
5
|
+
# == Examples
|
6
|
+
#
|
7
|
+
# If using ActiveModel directly within your class, then any one of the
|
8
|
+
# following features need to be included in order for the integration to be
|
9
|
+
# detected:
|
10
|
+
# * ActiveModel::Observing
|
11
|
+
# * ActiveModel::Validations
|
12
|
+
#
|
13
|
+
# Below is an example of a simple state machine defined within an
|
14
|
+
# ActiveModel class:
|
15
|
+
#
|
16
|
+
# class Vehicle
|
17
|
+
# include ActiveModel::Observing
|
18
|
+
# include ActiveModel::Validations
|
19
|
+
#
|
20
|
+
# attr_accessor :state
|
21
|
+
# define_attribute_methods [:state]
|
22
|
+
#
|
23
|
+
# state_machine :initial => :parked do
|
24
|
+
# event :ignite do
|
25
|
+
# transition :parked => :idling
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The examples in the sections below will use the above class as a
|
31
|
+
# reference.
|
32
|
+
#
|
33
|
+
# == Actions
|
34
|
+
#
|
35
|
+
# By default, no action will be invoked when a state is transitioned. This
|
36
|
+
# means that if you want to save changes when transitioning, you must
|
37
|
+
# define the action yourself like so:
|
38
|
+
#
|
39
|
+
# class Vehicle
|
40
|
+
# include ActiveModel::Validations
|
41
|
+
# attr_accessor :state
|
42
|
+
#
|
43
|
+
# state_machine :action => :save do
|
44
|
+
# ...
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# def save
|
48
|
+
# # Save changes
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# == Validations
|
53
|
+
#
|
54
|
+
# As mentioned in StateMachine::Machine#state, you can define behaviors,
|
55
|
+
# like validations, that only execute for certain states. One *important*
|
56
|
+
# caveat here is that, due to a constraint in ActiveModel's validation
|
57
|
+
# framework, custom validators will not work as expected when defined to run
|
58
|
+
# in multiple states. For example:
|
59
|
+
#
|
60
|
+
# class Vehicle
|
61
|
+
# include ActiveModel::Validations
|
62
|
+
#
|
63
|
+
# state_machine do
|
64
|
+
# ...
|
65
|
+
# state :first_gear, :second_gear do
|
66
|
+
# validate :speed_is_legal
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# In this case, the <tt>:speed_is_legal</tt> validation will only get run
|
72
|
+
# for the <tt>:second_gear</tt> state. To avoid this, you can define your
|
73
|
+
# custom validation like so:
|
74
|
+
#
|
75
|
+
# class Vehicle
|
76
|
+
# include ActiveModel::Validations
|
77
|
+
#
|
78
|
+
# state_machine do
|
79
|
+
# ...
|
80
|
+
# state :first_gear, :second_gear do
|
81
|
+
# validate {|vehicle| vehicle.speed_is_legal}
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# == Validation errors
|
87
|
+
#
|
88
|
+
# In order to hook in validation support for your model, the
|
89
|
+
# ActiveModel::Validations feature must be included. If this is included
|
90
|
+
# and an event fails to successfully fire because there are no matching
|
91
|
+
# transitions for the object, a validation error is added to the object's
|
92
|
+
# state attribute to help in determining why it failed.
|
93
|
+
#
|
94
|
+
# For example,
|
95
|
+
#
|
96
|
+
# vehicle = Vehicle.new
|
97
|
+
# vehicle.ignite # => false
|
98
|
+
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
|
99
|
+
#
|
100
|
+
# In addition, if you're using the <tt>ignite!</tt> version of the event,
|
101
|
+
# then the failure reason (such as the current validation errors) will be
|
102
|
+
# included in the exception that gets raised when the event fails. For
|
103
|
+
# example, assuming there's a validation on a field called +name+ on the class:
|
104
|
+
#
|
105
|
+
# vehicle = Vehicle.new
|
106
|
+
# vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
|
107
|
+
#
|
108
|
+
# === Security implications
|
109
|
+
#
|
110
|
+
# Beware that public event attributes mean that events can be fired
|
111
|
+
# whenever mass-assignment is being used. If you want to prevent malicious
|
112
|
+
# users from tampering with events through URLs / forms, the attribute
|
113
|
+
# should be protected like so:
|
114
|
+
#
|
115
|
+
# class Vehicle
|
116
|
+
# include ActiveModel::MassAssignmentSecurity
|
117
|
+
# attr_accessor :state
|
118
|
+
#
|
119
|
+
# attr_protected :state_event
|
120
|
+
# # attr_accessible ... # Alternative technique
|
121
|
+
#
|
122
|
+
# state_machine do
|
123
|
+
# ...
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# If you want to only have *some* events be able to fire via mass-assignment,
|
128
|
+
# you can build two state machines (one public and one protected) like so:
|
129
|
+
#
|
130
|
+
# class Vehicle
|
131
|
+
# include ActiveModel::MassAssignmentSecurity
|
132
|
+
# attr_accessor :state
|
133
|
+
#
|
134
|
+
# attr_protected :state_event # Prevent access to events in the first machine
|
135
|
+
#
|
136
|
+
# state_machine do
|
137
|
+
# # Define private events here
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# # Public machine targets the same state as the private machine
|
141
|
+
# state_machine :public_state, :attribute => :state do
|
142
|
+
# # Define public events here
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# == Callbacks
|
147
|
+
#
|
148
|
+
# All before/after transition callbacks defined for ActiveModel models
|
149
|
+
# behave in the same way that other ActiveSupport callbacks behave. The
|
150
|
+
# object involved in the transition is passed in as an argument.
|
151
|
+
#
|
152
|
+
# For example,
|
153
|
+
#
|
154
|
+
# class Vehicle
|
155
|
+
# include ActiveModel::Validations
|
156
|
+
# attr_accessor :state
|
157
|
+
#
|
158
|
+
# state_machine :initial => :parked do
|
159
|
+
# before_transition any => :idling do |vehicle|
|
160
|
+
# vehicle.put_on_seatbelt
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# before_transition do |vehicle, transition|
|
164
|
+
# # log message
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# event :ignite do
|
168
|
+
# transition :parked => :idling
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# def put_on_seatbelt
|
173
|
+
# ...
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# Note, also, that the transition can be accessed by simply defining
|
178
|
+
# additional arguments in the callback block.
|
179
|
+
#
|
180
|
+
# == Observers
|
181
|
+
#
|
182
|
+
# In order to hook in observer support for your application, the
|
183
|
+
# ActiveModel::Observing feature must be included. Because of the way
|
184
|
+
# ActiveModel observers are designed, there is less flexibility around the
|
185
|
+
# specific transitions that can be hooked in. However, a large number of
|
186
|
+
# hooks *are* supported. For example, if a transition for a object's
|
187
|
+
# +state+ attribute changes the state from +parked+ to +idling+ via the
|
188
|
+
# +ignite+ event, the following observer methods are supported:
|
189
|
+
# * before/after/after_failure_to-_ignite_from_parked_to_idling
|
190
|
+
# * before/after/after_failure_to-_ignite_from_parked
|
191
|
+
# * before/after/after_failure_to-_ignite_to_idling
|
192
|
+
# * before/after/after_failure_to-_ignite
|
193
|
+
# * before/after/after_failure_to-_transition_state_from_parked_to_idling
|
194
|
+
# * before/after/after_failure_to-_transition_state_from_parked
|
195
|
+
# * before/after/after_failure_to-_transition_state_to_idling
|
196
|
+
# * before/after/after_failure_to-_transition_state
|
197
|
+
# * before/after/after_failure_to-_transition
|
198
|
+
#
|
199
|
+
# The following class shows an example of some of these hooks:
|
200
|
+
#
|
201
|
+
# class VehicleObserver < ActiveModel::Observer
|
202
|
+
# # Callback for :ignite event *before* the transition is performed
|
203
|
+
# def before_ignite(vehicle, transition)
|
204
|
+
# # log message
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# # Callback for :ignite event *after* the transition has been performed
|
208
|
+
# def after_ignite(vehicle, transition)
|
209
|
+
# # put on seatbelt
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# # Generic transition callback *before* the transition is performed
|
213
|
+
# def after_transition(vehicle, transition)
|
214
|
+
# Audit.log(vehicle, transition)
|
215
|
+
# end
|
216
|
+
#
|
217
|
+
# def after_failure_to_transition(vehicle, transition)
|
218
|
+
# Audit.error(vehicle, transition)
|
219
|
+
# end
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
# More flexible transition callbacks can be defined directly within the
|
223
|
+
# model as described in StateMachine::Machine#before_transition
|
224
|
+
# and StateMachine::Machine#after_transition.
|
225
|
+
#
|
226
|
+
# To define a single observer for multiple state machines:
|
227
|
+
#
|
228
|
+
# class StateMachineObserver < ActiveModel::Observer
|
229
|
+
# observe Vehicle, Switch, Project
|
230
|
+
#
|
231
|
+
# def after_transition(object, transition)
|
232
|
+
# Audit.log(object, transition)
|
233
|
+
# end
|
234
|
+
# end
|
235
|
+
#
|
236
|
+
# == Internationalization
|
237
|
+
#
|
238
|
+
# Any error message that is generated from performing invalid transitions
|
239
|
+
# can be localized. The following default translations are used:
|
240
|
+
#
|
241
|
+
# en:
|
242
|
+
# activemodel:
|
243
|
+
# errors:
|
244
|
+
# messages:
|
245
|
+
# invalid: "is invalid"
|
246
|
+
# # %{value} = attribute value, %{state} = Human state name
|
247
|
+
# invalid_event: "cannot transition when %{state}"
|
248
|
+
# # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
|
249
|
+
# invalid_transition: "cannot transition via %{event}"
|
250
|
+
#
|
251
|
+
# You can override these for a specific model like so:
|
252
|
+
#
|
253
|
+
# en:
|
254
|
+
# activemodel:
|
255
|
+
# errors:
|
256
|
+
# models:
|
257
|
+
# user:
|
258
|
+
# invalid: "is not valid"
|
259
|
+
#
|
260
|
+
# In addition to the above, you can also provide translations for the
|
261
|
+
# various states / events in each state machine. Using the Vehicle example,
|
262
|
+
# state translations will be looked for using the following keys, where
|
263
|
+
# +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
|
264
|
+
# * <tt>activemodel.state_machines.#{model_name}.#{machine_name}.states.#{state_name}</tt>
|
265
|
+
# * <tt>activemodel.state_machines.#{model_name}.states.#{state_name}</tt>
|
266
|
+
# * <tt>activemodel.state_machines.#{machine_name}.states.#{state_name}</tt>
|
267
|
+
# * <tt>activemodel.state_machines.states.#{state_name}</tt>
|
268
|
+
#
|
269
|
+
# Event translations will be looked for using the following keys, where
|
270
|
+
# +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
|
271
|
+
# * <tt>activemodel.state_machines.#{model_name}.#{machine_name}.events.#{event_name}</tt>
|
272
|
+
# * <tt>activemodel.state_machines.#{model_name}.events.#{event_name}</tt>
|
273
|
+
# * <tt>activemodel.state_machines.#{machine_name}.events.#{event_name}</tt>
|
274
|
+
# * <tt>activemodel.state_machines.events.#{event_name}</tt>
|
275
|
+
#
|
276
|
+
# An example translation configuration might look like so:
|
277
|
+
#
|
278
|
+
# es:
|
279
|
+
# activemodel:
|
280
|
+
# state_machines:
|
281
|
+
# states:
|
282
|
+
# parked: 'estacionado'
|
283
|
+
# events:
|
284
|
+
# park: 'estacionarse'
|
285
|
+
#
|
286
|
+
# == Dirty Attribute Tracking
|
287
|
+
#
|
288
|
+
# When using the ActiveModel::Dirty extension, your model will keep track of
|
289
|
+
# any changes that are made to attributes. Depending on your ORM, an object
|
290
|
+
# will only be saved when there are attributes that have changed on the
|
291
|
+
# object. When integrating with state_machine, typically the +state+ field
|
292
|
+
# will be marked as dirty after a transition occurs. In some situations,
|
293
|
+
# however, this isn't the case.
|
294
|
+
#
|
295
|
+
# If you define loopback transitions in your state machine, the value for
|
296
|
+
# the machine's attribute (e.g. state) will not change. Unless you explicitly
|
297
|
+
# indicate so, this means that your object won't persist anything on a
|
298
|
+
# loopback. For example:
|
299
|
+
#
|
300
|
+
# class Vehicle
|
301
|
+
# include ActiveModel::Validations
|
302
|
+
# include ActiveModel::Dirty
|
303
|
+
# attr_accessor :state
|
304
|
+
#
|
305
|
+
# state_machine :initial => :parked do
|
306
|
+
# event :park do
|
307
|
+
# transition :parked => :parked, ...
|
308
|
+
# end
|
309
|
+
# end
|
310
|
+
# end
|
311
|
+
#
|
312
|
+
# If, instead, you'd like your object to always persist regardless of
|
313
|
+
# whether the value actually changed, you can do so by using the
|
314
|
+
# <tt>#{attribute}_will_change!</tt> helpers or defining a +before_transition+
|
315
|
+
# callback that actually changes an attribute on the model. For example:
|
316
|
+
#
|
317
|
+
# class Vehicle
|
318
|
+
# ...
|
319
|
+
# state_machine :initial => :parked do
|
320
|
+
# before_transition all => same do |vehicle|
|
321
|
+
# vehicle.state_will_change!
|
322
|
+
#
|
323
|
+
# # Alternative solution, updating timestamp
|
324
|
+
# # vehicle.updated_at = Time.curent
|
325
|
+
# end
|
326
|
+
# end
|
327
|
+
# end
|
328
|
+
#
|
329
|
+
# == Creating new integrations
|
330
|
+
#
|
331
|
+
# If you want to integrate state_machine with an ORM that implements parts
|
332
|
+
# or all of the ActiveModel API, only the machine defaults need to be
|
333
|
+
# specified. Otherwise, the implementation is similar to any other
|
334
|
+
# integration.
|
335
|
+
#
|
336
|
+
# For example,
|
337
|
+
#
|
338
|
+
# module StateMachine::Integrations::MyORM
|
339
|
+
# include StateMachine::Integrations::ActiveModel
|
340
|
+
#
|
341
|
+
# @defaults = {:action = > :persist}
|
342
|
+
#
|
343
|
+
# def self.matches?(klass)
|
344
|
+
# defined?(::MyORM::Base) && klass <= ::MyORM::Base
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
# protected
|
348
|
+
# def runs_validations_on_action?
|
349
|
+
# action == :persist
|
350
|
+
# end
|
351
|
+
# end
|
352
|
+
#
|
353
|
+
# If you wish to implement other features, such as attribute initialization
|
354
|
+
# with protected attributes, named scopes, or database transactions, you
|
355
|
+
# must add these independent of the ActiveModel integration. See the
|
356
|
+
# ActiveRecord implementation for examples of these customizations.
|
357
|
+
module ActiveModel
|
358
|
+
def self.included(base) #:nodoc:
|
359
|
+
base.versions.unshift(*versions)
|
360
|
+
end
|
361
|
+
|
362
|
+
include Base
|
363
|
+
extend ClassMethods
|
364
|
+
|
365
|
+
require 'state_machine/integrations/active_model/versions'
|
366
|
+
|
367
|
+
@defaults = {}
|
368
|
+
|
369
|
+
# Classes that include ActiveModel::Observing or ActiveModel::Validations
|
370
|
+
# will automatically use the ActiveModel integration.
|
371
|
+
def self.matching_ancestors
|
372
|
+
%w(ActiveModel ActiveModel::Observing ActiveModel::Validations)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Adds a validation error to the given object
|
376
|
+
def invalidate(object, attribute, message, values = [])
|
377
|
+
if supports_validations?
|
378
|
+
attribute = self.attribute(attribute)
|
379
|
+
options = values.inject({}) do |h, (key, value)|
|
380
|
+
h[key] = value
|
381
|
+
h
|
382
|
+
end
|
383
|
+
|
384
|
+
default_options = default_error_message_options(object, attribute, message)
|
385
|
+
object.errors.add(attribute, message, options.merge(default_options))
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Describes the current validation errors on the given object. If none
|
390
|
+
# are specific, then the default error is interpeted as a "halt".
|
391
|
+
def errors_for(object)
|
392
|
+
object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
|
393
|
+
end
|
394
|
+
|
395
|
+
# Resets any errors previously added when invalidating the given object
|
396
|
+
def reset(object)
|
397
|
+
object.errors.clear if supports_validations?
|
398
|
+
end
|
399
|
+
|
400
|
+
protected
|
401
|
+
# Whether observers are supported in the integration. Only true if
|
402
|
+
# ActiveModel::Observer is available.
|
403
|
+
def supports_observers?
|
404
|
+
defined?(::ActiveModel::Observing) && owner_class <= ::ActiveModel::Observing
|
405
|
+
end
|
406
|
+
|
407
|
+
# Whether validations are supported in the integration. Only true if
|
408
|
+
# the ActiveModel feature is enabled on the owner class.
|
409
|
+
def supports_validations?
|
410
|
+
defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
|
411
|
+
end
|
412
|
+
|
413
|
+
# Do validations run when the action configured this machine is
|
414
|
+
# invoked? This is used to determine whether to fire off attribute-based
|
415
|
+
# event transitions when the action is run.
|
416
|
+
def runs_validations_on_action?
|
417
|
+
false
|
418
|
+
end
|
419
|
+
|
420
|
+
# Gets the terminator to use for callbacks
|
421
|
+
def callback_terminator
|
422
|
+
@terminator ||= lambda {|result| result == false}
|
423
|
+
end
|
424
|
+
|
425
|
+
# Determines the base scope to use when looking up translations
|
426
|
+
def i18n_scope(klass)
|
427
|
+
klass.i18n_scope
|
428
|
+
end
|
429
|
+
|
430
|
+
# The default options to use when generating messages for validation
|
431
|
+
# errors
|
432
|
+
def default_error_message_options(object, attribute, message)
|
433
|
+
{:message => @messages[message]}
|
434
|
+
end
|
435
|
+
|
436
|
+
# Translates the given key / value combo. Translation keys are looked
|
437
|
+
# up in the following order:
|
438
|
+
# * <tt>#{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}</tt>
|
439
|
+
# * <tt>#{i18n_scope}.state_machines.#{model_name}.#{plural_key}.#{value}</tt>
|
440
|
+
# * <tt>#{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}</tt>
|
441
|
+
# * <tt>#{i18n_scope}.state_machines.#{plural_key}.#{value}</tt>
|
442
|
+
#
|
443
|
+
# If no keys are found, then the humanized value will be the fallback.
|
444
|
+
def translate(klass, key, value)
|
445
|
+
ancestors = ancestors_for(klass)
|
446
|
+
group = key.to_s.pluralize
|
447
|
+
value = value ? value.to_s : 'nil'
|
448
|
+
|
449
|
+
# Generate all possible translation keys
|
450
|
+
translations = ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}"}
|
451
|
+
translations.concat(ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}"})
|
452
|
+
translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
|
453
|
+
I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope(klass), :state_machines])
|
454
|
+
end
|
455
|
+
|
456
|
+
# Build a list of ancestors for the given class to use when
|
457
|
+
# determining which localization key to use for a particular string.
|
458
|
+
def ancestors_for(klass)
|
459
|
+
klass.lookup_ancestors
|
460
|
+
end
|
461
|
+
|
462
|
+
# Initializes class-level extensions and defaults for this machine
|
463
|
+
def after_initialize
|
464
|
+
super
|
465
|
+
load_locale
|
466
|
+
load_observer_extensions
|
467
|
+
add_default_callbacks
|
468
|
+
end
|
469
|
+
|
470
|
+
# Loads any locale files needed for translating validation errors
|
471
|
+
def load_locale
|
472
|
+
I18n.load_path.unshift(@integration.locale_path) unless I18n.load_path.include?(@integration.locale_path)
|
473
|
+
end
|
474
|
+
|
475
|
+
# Loads extensions to ActiveModel's Observers
|
476
|
+
def load_observer_extensions
|
477
|
+
require 'state_machine/integrations/active_model/observer'
|
478
|
+
require 'state_machine/integrations/active_model/observer_update'
|
479
|
+
end
|
480
|
+
|
481
|
+
# Adds a set of default callbacks that utilize the Observer extensions
|
482
|
+
def add_default_callbacks
|
483
|
+
if supports_observers?
|
484
|
+
callbacks[:before] << Callback.new(:before) {|object, transition| notify(:before, object, transition)}
|
485
|
+
callbacks[:after] << Callback.new(:after) {|object, transition| notify(:after, object, transition)}
|
486
|
+
callbacks[:failure] << Callback.new(:failure) {|object, transition| notify(:after_failure_to, object, transition)}
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# Skips defining reader/writer methods since this is done automatically
|
491
|
+
def define_state_accessor
|
492
|
+
name = self.name
|
493
|
+
|
494
|
+
owner_class.validates_each(attribute) do |object, attr, value|
|
495
|
+
machine = object.class.state_machine(name)
|
496
|
+
machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
|
497
|
+
end if supports_validations?
|
498
|
+
end
|
499
|
+
|
500
|
+
# Adds hooks into validation for automatically firing events
|
501
|
+
def define_action_helpers
|
502
|
+
super
|
503
|
+
define_validation_hook if runs_validations_on_action?
|
504
|
+
end
|
505
|
+
|
506
|
+
# Hooks into validations by defining around callbacks for the
|
507
|
+
# :validation event
|
508
|
+
def define_validation_hook
|
509
|
+
owner_class.set_callback(:validation, :around, self, :prepend => true)
|
510
|
+
end
|
511
|
+
|
512
|
+
# Runs state events around the object's validation process
|
513
|
+
def around_validation(object)
|
514
|
+
object.class.state_machines.transitions(object, action, :after => false).perform { yield }
|
515
|
+
end
|
516
|
+
|
517
|
+
# Creates a new callback in the callback chain, always inserting it
|
518
|
+
# before the default Observer callbacks that were created after
|
519
|
+
# initialization.
|
520
|
+
def add_callback(type, options, &block)
|
521
|
+
options[:terminator] = callback_terminator
|
522
|
+
|
523
|
+
if supports_observers?
|
524
|
+
@callbacks[type == :around ? :before : type].insert(-2, callback = Callback.new(type, options, &block))
|
525
|
+
add_states(callback.known_states)
|
526
|
+
callback
|
527
|
+
else
|
528
|
+
super
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Configures new states with the built-in humanize scheme
|
533
|
+
def add_states(new_states)
|
534
|
+
super.each do |new_state|
|
535
|
+
new_state.human_name = lambda {|state, klass| translate(klass, :state, state.name)}
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Configures new event with the built-in humanize scheme
|
540
|
+
def add_events(new_events)
|
541
|
+
super.each do |new_event|
|
542
|
+
new_event.human_name = lambda {|event, klass| translate(klass, :event, event.name)}
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
# Notifies observers on the given object that a callback occurred
|
547
|
+
# involving the given transition. This will attempt to call the
|
548
|
+
# following methods on observers:
|
549
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}_to_#{to}</tt>
|
550
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}</tt>
|
551
|
+
# * <tt>#{type}_#{qualified_event}_to_#{to}</tt>
|
552
|
+
# * <tt>#{type}_#{qualified_event}</tt>
|
553
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}_to_#{to}</tt>
|
554
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}</tt>
|
555
|
+
# * <tt>#{type}_transition_#{machine_name}_to_#{to}</tt>
|
556
|
+
# * <tt>#{type}_transition_#{machine_name}</tt>
|
557
|
+
# * <tt>#{type}_transition</tt>
|
558
|
+
#
|
559
|
+
# This will always return true regardless of the results of the
|
560
|
+
# callbacks.
|
561
|
+
def notify(type, object, transition)
|
562
|
+
name = self.name
|
563
|
+
event = transition.qualified_event
|
564
|
+
from = transition.from_name || 'nil'
|
565
|
+
to = transition.to_name || 'nil'
|
566
|
+
|
567
|
+
# Machine-specific updates
|
568
|
+
["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
|
569
|
+
["_from_#{from}", nil].each do |from_segment|
|
570
|
+
["_to_#{to}", nil].each do |to_segment|
|
571
|
+
object.class.changed if object.class.respond_to?(:changed)
|
572
|
+
object.class.notify_observers('update_with_transition', ObserverUpdate.new([event_segment, from_segment, to_segment].join, object, transition))
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
# Generic updates
|
578
|
+
object.class.changed if object.class.respond_to?(:changed)
|
579
|
+
object.class.notify_observers('update_with_transition', ObserverUpdate.new("#{type}_transition", object, transition))
|
580
|
+
|
581
|
+
true
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
|
2
|
+
translations = eval(IO.read(File.expand_path(filename)), binding, filename)
|
3
|
+
translations[:en][:activerecord] = translations[:en].delete(:activemodel)
|
4
|
+
|
5
|
+
# Only ActiveRecord 2.3.5+ can pull i18n >= 0.1.3 from system-wide gems (and
|
6
|
+
# therefore possibly have I18n::VERSION available)
|
7
|
+
begin
|
8
|
+
require 'i18n/version'
|
9
|
+
rescue Exception => ex
|
10
|
+
end unless ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
|
11
|
+
|
12
|
+
# Only i18n 0.4.0+ has the new %{key} syntax
|
13
|
+
if !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0'
|
14
|
+
translations[:en][:activerecord][:errors][:messages].each do |key, message|
|
15
|
+
message.gsub!('%{', '{{')
|
16
|
+
message.gsub!('}', '}}')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
translations
|