Wizarding multiple models

There are cases in which you need to create multiple models at once. For example, a guest can subscribe for an account, but this should also entail that the guest will be registered as a user. As a bonus, imagine that an account also needs to have ‘billing details’, so we know where to send the subscription fee to.

James Golick has created a gem that behaves as an ActiveRecord and can ‘present’ multiple models at once. It’s called ActivePresenter. It’s fairly new (version 0.0.3), but it does the job nicely. Unfortunately, it only supports creating the presented objects at once, resulting in one huge web form. What I needed was a wizard.

There aren’t many options yet for creating a wizard in RoR. I found a plugin called acts_as_wizard and a tutorial on Ruby-coloured glasses. I liked that acts_as_wizard moves logic to the models, but saw that a) the example saves the object between pages (which could be done different though), b) it uses AASM, which I was already using for that model, c) doesn’t tell you when the wizard has been finished and d) needs migrations, while I think that shouldn’t be necessary for page navigation.
The tutorial does things by just using Rails and doesn’t have logic in models. Also in this case, models were saved between pages of the wizard.

You could say that I have combined both approaches, since my approach borrows features from both implementations. The logic has been partly moved to the model (in this case an ActivePresenter object, which is good, because wizard logic isn’t really (ActiveRecord) model logic), uses a constant WIZARD_STEPS to define which actions are part of the wizard and uses a session variable before actually saving the objects. However, I still think this implementation is far from perfect and could be optimized in several ways. It does however what I need for the time being.

First, I’ll show you the ActivePresenter object called SignupPresenter (located in /app/presenters):

class SignupPresenter < ActivePresenter::Base
  presents :account, :user, :billing_details

  # The steps in the wizard
  WIZARD_STEPS = &#91;:new_account, :new_user, :new_billing_details, :review_and_submit&#93;

  # Dirty attributes can be defined to only show validation error
  # after a presentable has been submitted
  def dirty_presentables
    @dirty_presentables ||= &#91;&#93;
    @dirty_presentables = @dirty_presentables.uniq

  # ActivePresenters behave like they exist, therefore force
  # this presenter to be a new record
  def id

  def new_record?

  # Override valid? to only add errors for dirty presentables
  # :validate_all can be specified as an option to force all
  # representables to be validated
  def valid?(options = {})

    if options&#91;:validate_all&#93;
      presented.keys.each do |type|
        if dirty_presentables.include?(type)
          presented_inst = send(type)
          merge_errors(presented_inst, type) unless presented_inst.valid?

  # Sets new attributes without saving. The found attributes are added
  # to the dirty presentables and validated
  def set_attributes(atts)
    if !atts.empty?
      self.attributes = atts
      atts.each { |k,v| (dirty_presentables << presentable_for(k)) if !presentable_for(k).nil? }

  # Define a to_xml for this Presenter to return the XML structure of a new presenter
  def to_xml
    &#91;account, user, billing_details&#93;.to_xml(:root => 'signup_presenter')

  # Associate the presentables with each other as defined by the RailsCluster design
  def associate_present_models
    self.billing_details.user = self.user if billing_details && user
    self.account.billing_details = self.billing_details if account && billing_details

  # Wizard method to get the next or a user requested step
  # Forms should contain the :current_representable and :next_action field
  def get_next_or_chosen_wizard_step(current_presentable = nil, chosen_step = nil)
    chosen_step_index = WIZARD_STEPS.index(chosen_step.to_sym) if chosen_step
    current_presentable = send(current_presentable.to_sym) if current_presentable   

    # When validation for the current step failed, rerender that step, else render the requested step
    if current_presentable && chosen_step_index
      (!current_presentable.valid?) ? WIZARD_STEPS[chosen_step_index - 1] : chosen_step
    # Render the requested step (if any), otherwise
    elsif chosen_step_index
    # Choose for the user otherwise
      next_step = 3
        when !account.valid?         then next_step = 0
        when !user.valid?            then next_step = 1
        when !billing_details.valid? then next_step = 2


get_next_or_chosen_wizard_step is the most important method in this case. When you go to the next page in the wizard, it validates the current object and shows validation errors of some fields were incorrect. However, it will also let a user go to any other step in the wizard if he wants, even though some pages are not filled in correctly. This enables a user to fill in user details before filling in the account. By doing this, you do need to make sure to validate all objects again before the wizard is finalized. The controller does this by calling get_next_or_chosen_wizard_step without parameters, which causes the wizard to go to the the first page that is invalid.

The other thing you may notice is the dirty_representables variable. I use this to make sure that validation errors are only shown for a model after some data for it has been submitted, otherwise you’d see the errors when the user first visits the page.

Over to the controller, the SignupsController. I have created a route for this to work properly: map.resource :signup, :collection => { :create_step => :post }:

class SignupsController < ApplicationController  

  before_filter :find_presenter, :except => :new
  before_filter :prepare_for_step, :except => [ :new, :create_step, :create ]

  def new
    respond_to do |format|
      format.xml  { render :xml => SignupPresenter.new }

  def create
    respond_to do |format|
      format.html do
        if @signup_presenter.valid?(:validate_all => true) && @signup_presenter.save
          session[:signup_presenter] = nil
          flash[:notice] = 'Signup successful! Please activate your account to finish your registration.'
          redirect_to root_path
          flash[:notice] = 'Some required fields are not filled in correctly. Please correct these below and try again.'
          redirect_to :action => @signup_presenter.get_next_or_chosen_wizard_step
      format.xml do
        @signup_presenter.set_attributes(params[:signup_presenter]) if params[:signup_presenter]
        if @signup_presenter.valid?(:validate_all => true) && @signup_presenter.save
          render :xml => @signup_presenter, :status => :created
          render :xml => @signup_presenter.errors, :status => :unprocessable_entity

  def create_step
    @signup_presenter.set_attributes(params[:signup_presenter]) if params[:signup_presenter]

    respond_to do |format|
        format.html  { redirect_to :action => @signup_presenter.get_next_or_chosen_wizard_step(params[:current_presentable], params[:next_action]) }


  def find_presenter
    session[:signup_presenter] ||= SignupPresenter.new
    @signup_presenter = session[:signup_presenter]

  def prepare_for_step
    respond_to do |format|
      format.html { render :action => @signup_presenter.get_next_or_chosen_wizard_step(params[:current_presentable], params[:action])}
      format.xml  { render :xml => @signup_presenter }


When first accessing the controller by browsing to /signup, the prepare_for_step-filter automatically redirects the user to the first page in the wizard (as defined by WIZARD_STEPS in the presenter). For every step in the wizard there is an action, but since they don’t do anything (except for triggering the before filters), they can be left out.
If you don’t want to be able to use XML format for signing up, the new action (and format.xml calls) can be omitted, too. Each form belonging to an action in the wizard will post to create_step, which updates (but doesn’t save) the presenter and then redirects to the next step.
The last step in the wizard post to create, so that the controller knows the presenter should be completely valid now and the models are ready to be saved. To be sure, the whole presenter is validated and when validations fail, the user will be redirected to the page that contains the error.

The presenter used is saved as a session variable and destroyed after finishing the wizard. Using a session variable could pose a security risk when using cookies, so you should use an ActiveRecord session store if the data contains sensitive data. You should always use an AR session store for this, I found some weird bugs when I didn’t use it.

Finally, let’s take a look at some of the views. A typical form for a step (signups/new_account.html.erb) in the wizard looks like this:

<h1>Signup (Step 1)</h1>
<% form_for @signup_presenter, :url => create_step_signup_path do |f| -%>
<h2>Account Details</h2>
Enter the name of the account
<div class="formField">
    <%= f.label :account_name, "Account Name" %>
<div class="inputField">
        <%= hidden_field_tag :current_presentable, 'account' %>
        <%= hidden_field_tag :next_action, 'new_user' %>
        <%= f.text_field :account_name %>
        <%= f.error_message_on :account_name, "Account name " %></div>
<%= f.submit "Next" %>

<% end %>

Notice the hidden fields that contain which presentable belongs to the form and what the next action should be. These fields are really only there (and only needed) when you’d like your user to fill in the wizard in any arbitrary order. It makes sure that when going to the next action, the page is redisplayed when the validation for the model fails. However, it should not do that when the user manually requests to go to another page.

Now, let’s show the last page (signups/review_and_submit.html.erb) of the wizard:

Please review your details
<h3>Account Details</h3>
<td>Account Name</td>
// Omitted details for user and billing details

<%= button_to "Complete signup", :action => "create" %>
<%= button_to "Back", :action => "new_billing_details" %>

The page just shows what the user has filled in. You could probably also show validation errors here, but I left them out. By completing the signup, create is called, which does a final validation check and then saves the new models. This page also shows a back button, which takes the user to the new_billing_details page. Of course, you can add such buttons anywhere to any page of the wizard using this implementation. You could even browse away from the wizard; by calling /signup again, it would just load the session variable again and redirect you to the last invalid page! Please tell me what you think of this approach. Comments are always welcome!

~ by moiristo on August 9, 2008.

2 Responses to “Wizarding multiple models”

  1. Hi,
    i am a newbie to Rails. I am trying to use your approach to develop some complex application where i must get a lot of data. i have some questions:
    1) how can i use the Rails cluster design to model a n,n relation in the activepresenter object ? i’ve tried some test by an id is required in the relation table before i can save.
    2) how can i save multiple items, for example accounts, for one sign up?
    Thank you for this great tutorial !

  2. I think you’ve really pinned down the limitations of a presenter🙂 I’m not sure what I myself would do to solve your problems. I think that it should be possible to solve 1) by using a transaction. And for 2), I’d probably modify the activepresenter plugin to support aliases for models, so you could do something like:

    class SignupPresenter < ActivePresenter::Base
    presents :account1, :as => :account
    presents :account2, :as => :account

    Maybe you could contact the author of the plugin for this…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: