Multistep / Multimodel Workflow in Rails

27 views
Skip to first unread message

Tony Byrne

unread,
Apr 11, 2008, 10:59:19 AM4/11/08
to Ruby Ireland
Hi Folks,

Anyone have any experience implementing a sequential multistep
workflow in Rails that touches a number of models?

I'm building a multi-step purchase process for an insurance product.
The nature of the process means that I have number of models, roughly
one per form. At the moment I've implemented each step as an action
and am using redirect_to to navigate between steps. It works well,
but I can't help thinking that the redirect is wasteful, what with it
requiring an additional round trip to the server before the user sees
the next step.

I've toyed around with using 'render :action => "my_next_step"'
instead but it confuses my views which expect instance variables to be
set up for their respective steps.

Anyone got a better idea?

Thanks,

Regards,

Tony.

Jonathan Clarke

unread,
Apr 11, 2008, 11:03:48 AM4/11/08
to ruby_i...@googlegroups.com
You could set up a model which details out the next step in the DB, i.e. the specific controller, action to present next. On each step being complete it would simply refer to the proceeding step in the model.  That way you could present the full steps to the user, not just the next one but also the proceeding one as well.

Jonathan

Dave Rice

unread,
Apr 13, 2008, 9:41:16 AM4/13/08
to ruby_i...@googlegroups.com

Tony, redirecting between nicely restful controllers is the easiest way, and works very well. The best paradigm for this imho is a series of AJAX calls which should be applied unobtrusively once you've got everything working and tested without adding the js complexity.

Good luck!


On Fri, Apr 11, 2008 at 3:59 PM, Tony Byrne <tony...@gmail.com> wrote:


Hi Folks,

Anyone have any experience implementing a sequential multistep
workflow in Rails that touches a number of models?

I'm building a multi-step purchase process for an insurance product.
The nature of the process means that I have number of models, roughly
one per form.  At the moment I've implemented each step as an action
and am using redirect_to to navigate between steps.  It works well,
but I can't help thinking that the redirect is wasteful, what with it
requiring an additional round trip to the server before the user sees
the next step.

I've toyed around with using 'render :action => "my_next_step"'
instead but it confuses my views which expect instance variables to be
set up for their respective steps.

Anyone got a better idea?

Thanks,

Regards,

Tony.







---
David Rice
+44 (0)78 708 12996




Ana

unread,
Apr 14, 2008, 9:51:39 AM4/14/08
to Ruby Ireland
I have a similar problem to solve in one of my applications. I have a
model which represents the purchase process, it stores the IDs of the
objects chosen in the various steps. This model has a state machine, I
used acts_as_state_machine, and it knows the next step it needs to
take based on the state machine specification.

The state of the state machine corresponds to the name of the action
within the purchase controller.

The advantage of doing it this way is that if a customer gets
interrupted, or if an invalid choice is somehow made in one of the
steps, the state machine can always be trusted to present the next
question required. It's also flexible enough to, say, skip a step
which might be irrelevant based on a previous choice.



On Apr 13, 2:41 pm, Dave Rice <davidjr...@gmail.com> wrote:
> Tony, redirecting between nicely restful controllers is the easiest  
> way, and works very well. The best paradigm for this imho is a series  
> of AJAX calls which should be applied unobtrusively once you've got  
> everything working and tested without adding the js complexity.
>
> Good luck!
>
>
>
>
>
> > On Fri, Apr 11, 2008 at 3:59 PM, Tony Byrne <tonyby...@gmail.com>  

macarthy

unread,
Apr 14, 2008, 10:32:20 AM4/14/08
to Ruby Ireland
I'd agree with Ana on this one. In fact there is an example of exactly
this in the advanced Rails book from prag, Programmers.

Justin

Tony Byrne

unread,
Apr 14, 2008, 10:49:14 AM4/14/08
to ruby_i...@googlegroups.com
Hi Ana,

>
> I have a similar problem to solve in one of my applications. I have a
> model which represents the purchase process, it stores the IDs of the
> objects chosen in the various steps. This model has a state machine, I
> used acts_as_state_machine, and it knows the next step it needs to
> take based on the state machine specification.
>
> The state of the state machine corresponds to the name of the action
> within the purchase controller.
>
> The advantage of doing it this way is that if a customer gets
> interrupted, or if an invalid choice is somehow made in one of the
> steps, the state machine can always be trusted to present the next
> question required. It's also flexible enough to, say, skip a step
> which might be irrelevant based on a previous choice.

That sounds like it might be what I need. Any pointers to sample
code? My Googling only reveals code snippets for the model, but
I don't understand how to make use of a FSM equipped model
in the controller.

Thanks!

Regards,

Tony.


Ana

unread,
Apr 15, 2008, 5:58:09 AM4/15/08
to Ruby Ireland
Hi, Tony,

I'll try to post some code later today.

-Ana

Ana

unread,
Apr 15, 2008, 9:19:01 AM4/15/08
to Ruby Ireland
My formula for this...

In the model, in this case "flight", we have a state machine. Each
event in the state machine corresponds to a stage in the dispatching
process.

event :pilot do
transitions :to => :pilot_assigned, :from
=> :aircraft_assigned, :guard => Proc.new { |o| !o.pilot_id.nil? }
end

There should be a single sequence of states which represent your
process from start to finish...

:new -> :aircraft_assigned -> :pilot_assigned -> :dispatch_flight

:guard should make sure you have a valid object. Note that if :guard
returns false (which should not happen if your users behave
themselves), the state will not change and hence the same question
will be asked again.

:guard can also be used to skip a step, to define more complex
validations, whatever you want. If you want to write complex :guard
actions then put them in a function and call this function from
Proc.new rather than cluttering up your state machine specification.

In the controller, define a method called "choose". This is the main
workhorse method. Basically, what you are doing is finding a
collection of objects which the user has to choose from. This might be
a real collection of objects from another active record model you have
defined, or if you need to ask questions which don't relate to models
then do a polymorphic belongs_to on a class (mine is called Constant)
and you can put whatever you want in there. This means you can store
your questions in a database table and add more at any time, translate
etc.

Controller choose method:
http://pastie.caboo.se/181013

The view can be as simple as:
http://pastie.caboo.se/181014

Routes make this pretty:
map.connect 'xxx/choose/:resource_type/:id', :controller =>
'xxx', :action => 'choose'
map.connect 'xxx/choose/:resource_type', :controller =>
'xxx', :action => 'choose'


A few orienting pointers:
@coll is the collection of objects from which the user has to choose.
If there are no objects in @coll, we have a problem and we raise an
error. If there is just 1, we choose it automatically. After the user
has chosen we make sure that the selected object is in this approved
list.

You will need to define collection_for_action methods which return
collections, they are passed a symbol identifying the circumstances so
you can have different collections in different places, see line 10 of
the controller.

This could be as simple as

def self.collection_for_action(action_name, obj = nil)
find(:all)
end

or more complex

def self.collection_for_action(action_name, obj = nil)
case action_name
when :...
find(:all, :conditions => ...)
when ...
find(:all, :conditions => ...)
else
raise "unhandled action in #{class.name}.collection_for_action:
#{action_name.to_s}"
end
end

in this way you can vary the collection based on parameters already
chosen, i.e. limit to car insurance if the type of object earlier was
a car.

So, this is based on a very old pattern I used way back in my Visual
Basic days. It was REALLY painful back then without state machines or
metaprogramming, but it saved me from having about 300 forms in my DB.
I've given you some very complex, undocumented code and I doubt it
will make sense straight away. It make sense to me only because I know
this particular app inside out and backwards having coded it about 5
times in VB and Ruby.

I suggest you just try doing the state machine bit first, get
comfortable with that, work out and test your business logic and get
to appreciate how much you can do in the :guard statements. Then you
might incrementally build up that controller action and hopefully it
will make more sense once you know how the state machine works.

I am going to try to write this up using a very simple example, but I
am just ridiculously busy these days so I don't know when that will
happen. If you have questions, please post them here and that will
help me to know which bits of this are difficult to understand.

I really like acts_as_state_machine but it does have its limitations,
the most serious being that you can only have 1 state machine per
model. So, I will probably be replacing this with something like ragel
eventually. But AASM is a nice place to start, it's native ruby, well
documented and pretty easy to use.

OH! Almost forgot, you will need some mods to the default AASM:
http://dev.agent.ie/svn/rails/patches/aasm.diff

I just typed this up hastily on my lunch break so please post with any
obvious errors/corrections/questions.

You (and anyone else reading this) are welcome to include that code in
what you write but I REALLY do not recommend you copy and paste the
"choose" method. I suggest you build your own from scratch and add
complexity gradually, it might end up looking like mine eventually but
I doubt my business logic will be identical to yours and it will take
a lot longer and be very buggy if you try to copy and paste. This is
not a particularly quick method to get started with but it is very
robust and flexible once it has been set up properly and understood
thoroughly.

Tony Byrne

unread,
Apr 15, 2008, 9:28:42 AM4/15/08
to ruby_i...@googlegroups.com
Ana,

I owe you a debt of gratitude for taking the time to detail your
solution (especially since you did it on your lunch break!).

I'll have a look at the code and see whether inspiration strikes.

Thanks again!

Regards,

Tony.

Reply all
Reply to author
Forward
0 new messages