Return to previous saved state with state_machine & state_machine_audit_trail

227 views
Skip to first unread message

Simon Rascovsky

unread,
Sep 19, 2011, 11:04:42 PM9/19/11
to pluginaw...@googlegroups.com

Hi all,

I have a Rails 3.1 application that uses the state_machine gem (https://github.com/pluginaweek/state_machine) to track a model's states. and the state_machine-audit_trail gem (https://github.com/wvanbergen/state_machine-audit_trail) to save the state changes.

#State Machine states for Work Orders
state_machine
:initial => :created do
after_transition
:on=>:validate, :do=>:create_report

store_audit_trail
  event
:validate do
    transition
:created => :validated
 
end
  event
:reject do
    transition
:created => :rejected
 
end
end

There is one particular state 'rejected' that is temporary, and I'm looking for a way to trigger an event to return the model to the previous state as saved in the WorkOrderStateTransition model (provided by the audit gem).

I created a method 'previous_state' that finds the last state transition, and when the method is called via console it doesreturn the previous state name.

The problem is I'm unable to call that method within the state_machine transition to use it as the destination state on a restore. To illustrate, something like this does not work:

  event :restore do
    transition
:rejected => lambda {|wo| wo.previous_state}
 
end

What I get stored as a state is a Proc.

I would have thought this would be a pretty common use case (returning to a previously saved state) but I have found very little info while searching. The only post that is relevant is Using pluginaweek's state_machine, can I reference the activerecord object during an event? But this does not use the workflow history provided by the audit gem , and also returns the state to a 'restored' state, which is not what I need.

Has anybody come across this same use case? I'm somewhat new to Ruby/Rails and I realize this may be entirely due to my lack of understanding of lambdas and variable scopes, but I've been struggling with this problem for days, and could sure use some help!

Thanks!

Aaron Pfeifer

unread,
Nov 3, 2011, 8:56:19 AM11/3/11
to pluginaw...@googlegroups.com
Hey Simon -

This is a really great question that I've been thinking about for a while now.  I apologize that no one else here, including myself, was able to offer feedback earlier.  I completely understand your use case and it makes total sense.  I don't have a *great* solution that you might be able to use, but I can offer a few workarounds.  My first suggestion might be the obvious one: be explicit about what transitions can be restored.  This allows you to have complete control over what can be restored and what can't be.  For example:

class Vehicle
  state_machine :initial => :parked do
    event :ignite do
      transition :parked => :idling
    end
    
    event :shift_up do
      transition :idling => :first_gear, :first_gear => :second_gear
    end
    
    event :restore do
      transition :second_gear => :first_gear, :first_gear => :idling, :idling => :parked
    end
  end
end

In this example, the restore event is explicit about what transitions are allowed; the event is not allowed to be called when in the parked state.  This can potentially get a bit cumbersome if you have a large state machine.  There are perhaps cleaner ways of inspecting the state machine and building reverse transitions, but that's an exercise I'll leave to others.

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

Hopefully this gives you some ideas if you haven't already come up with a workaround.  There are certainly other ways of doing it, like building transitions manually, but these are the cleanest and most supported solutions I can think of.  I haven't convinced myself that dynamic from / to states is something I want in the core.

Best of luck!

-Aaron
Reply all
Reply to author
Forward
0 new messages