I'm trying to implement a "suspend" event that transitions the object to the :suspended state. But I need to be able to "unsuspend", and return to the previous state. I added a previous_state field to the model, but I can't see how to access it inside an event block.
This is the basic logic I'm trying to implement:
event :suspend do
owner.previous_state = self.state
transition [:new, :old] => :suspended
end
event :unsuspend do
transition :suspended => owner.previous_state.to_sym
owner.previous_state = nil
end
The state_machine docs haven't been very helpful, and I can't find examples online. Sometimes it's tough to know how to describe something to google :)
The author of state_machine has also provided an alternate solution here: https://groups.google.com/d/msg/pluginaweek-talk/LL9VJdL_x9c/vP1qv6br734J
To wit:
Another possible solution is to be a little creative with how the state machine works. There are plenty of hooks in ORMs like ActiveRecord that give us the ability to set the state at any stage in the process. Consider the following:
class Vehicle < ActiveRecord::Base
before_validation do |vehicle|
# Set the actual value based on the previous state if we've just restored
vehicle.state = vehicle.previous_state if vehicle.restored?
end
state_machine :initial => :parked do
event :ignite do
transition :parked => :idling
end
event :restore do
transition any => :restored
end
state :parked do
validates_presence_of :name
end
end
# Look up previous state here...
def previous_state
'parked'
end
end
In this example, a new state, restored, is introduced even though it never actually gets persisted in the database. We instead provide a before_validation hook that rewrites the state based on the previous state. You can see the results below:
v = Vehicle.new(:name => 'test') # => #<Vehicle id: nil, name: "test", state: "parked">
v.save # => true
v.name = nil # => nil
v.ignite # => true
v # => #<Vehicle id: 1, name: nil, state: "idling">
v.restore # => false
v.errors # => #<OrderedHash {:name=>["can't be blank"]}>
v.state # => "idling"
v.name = 'test' # => "test"
v.restore # => true
v # => #<Vehicle id: 1, name: "test", state: "parked">
v.parked? # => true
This should require one less database hit as it occurs before validation. In my case the complete picture looks like this:
module Interpreting::Status
extend ActiveSupport::Concern
included do
before_validation :restore_previous_state, if: :interpreter_cancelled?
state_machine :state, :initial => :ordered do
before_transition :to => :interpreter_booked, :do => :set_previous_state
state :ordered
state :confirmed
state :interpreter_booked
state :interpreter_cancelled # Transient status
end
end
protected
def set_previous_state
self.previous_state = self.state
end
def restore_previous_state
self.state = self.previous_state
end
end
This isn't a perfect solution in my opinion, but I did figure out how to accomplish my task:
state_machine :initial => :new do
state :new
state :old
state :suspended
before_transition :to => :suspended, :do => :set_previous_state
state :unsuspended
after_transition :to => :unsuspended, :do => :restore_previous_state
event :suspend do
transition any - :suspended => :suspended
end
event :unsuspend do
transition :suspended => :unsuspended, :if => :previous_state_present?
end
end
private
def previous_state_present?
previous_state.present?
end
def set_previous_state
self.previous_state = state
end
def restore_previous_state
if previous_state
self.state = previous_state
self.previous_state = nil
end
end
I started by adding an "unsuspended" state to my machine. While I never want something to stay in this state, I can't dynamically tell state_machine what state I want to unsuspend to.
I added a before_transition callback to the suspend event, to save the state before it is suspended.
I added an after_transition callback to the unsuspend event, so the state is immediately updated to the previous state, and that previous state is then wiped out to prevent issues later in the object's life.
This is not perfect. It works, but it's a lot more complicated than just creating the suspend and unsuspend events as standalone methods. I didn't go that route because I want state_machine controlling all state changes, and breaking out of that removes the protections against moving to/from invalid states, callbacks, etc.
Simple solution
I am providing an alternative version to the solution that uses owner
block argument. Might be useful is some cases.
state_machine :initial => :new do
state :new
state :old
before_transition :on => :suspend do |owner|
owner.previous_state = owner.state
end
before_transition :on => :unsuspend do |owner|
owner.previous_state.present?
end
after_transition :on => :unsuspend do |owner|
owner.state = owner.previous_state
end
event :suspend do
transition any - :suspended => :suspended
end
event :unsuspend do
transition :suspended => :unsuspended
end
end
Use of around_transition
Also note that you can replace the two unsuspend
blocks with around_transition
:
around_transition :on => :unsuspend do |owner, transition_block|
if owner.previous_state.present?
transition_block.call
owner.state = owner.previous_state
end
end