This question comes up a lot, people want to have an object, lets call it a Product that they want to create in several different steps. Let’s say our product has a few fields name, price, and category and to have a valid product all these fields must be present.

This is a re-post of a wiki I wrote for Wicked. While it was written to be used with a wizard, the pattern can be used without it. Enjoy!

The Problem

We want to build an object in several different steps but we can’t because that object needs validations. Lets take a look at our Product model.

class Product < ActiveRecord::Base
  validates :name, :price, :category, :presence => true

end

So we have a product that relies on name, price, and category to all be there. Lets take a look at a simple Wizard controller for ProductController.

class ProductController < ApplicationController
  include Wicked::Wizard

  steps :add_name, :add_price, :add_category

  def show
    @product = Product.find(params[:product_id])
    render_wizard
  end


  def update
    @product = Product.find(params[:product_id])
    @product.update_attributes(params[:product])
    render_wizard @product
  end


  def create
    @product = Product.create
    redirect_to wizard_path(steps.first, :product_id => @product.id)
  end
end

Here the create action won’t work because our product didn’t save. OhNo!

The Solution

The best way to build an object incrementally with validations is to save the state of our product in the database and use conditional validation. To do this we’re going to add a status field to our Product class.

class ProductStatus < ActiveRecord::Migration
  def up
    add_column :products, :status, :string
  end

  def down
    remove_column :product, :status
  end
end

Now we want to add an active state to our Product model.

def active?
  status == 'active'
end

And we can add a conditional validation to our model.

class Product < ActiveRecord::Base
  validates :name, :price, :category, :presence => true, :if => :active?

  def active?
    status == 'active'
  end
end

Now we can create our Product and we won’t have any validation errors, when the time comes that we want to release the product into the wild you’ll want to remember to change the status of our Product on the last step.

class ProductController < ApplicationController
  include Wicked::Wizard

  steps :add_name, :add_price, :add_category

  def update
    @product = Product.find(params[:product_id])
    params[:product][:status] = 'active' if step == steps.last
    @product.update_attributes(params[:product])
    render_wizard @product
  end

Great, but…

So that works well, but what if we want to disallow a user to go to the next step unless they’ve properly set the value before it. We’ll need to split up or validations to support multiple conditional validations.

class Product < ActiveRecord::Base
  validates :name,      :presence => true, :if => :active_or_name?
  validates :price,     :presence => true, :if => :active_or_price?
  validates :category,  :presence => true, :if => :active_or_category?

  def active?
    status == 'active'
  end

  def active_or_name?
    status.include?('name') || active?
  end

  def active_or_price?
    status.include?('price') || active?
  end

  def active_or_category?
    status.include?('category') || active?
  end

end

Then in our ProductController Wizard we can set the status to the current step name in in our update.

  def update
    @product = Product.find(params[:product_id])
    params[:product][:status] = step
    params[:product][:status] = 'active' if step == steps.last
    @product.update_attributes(params[:product])
    render_wizard @product
  end

So on the :add_name step status.include?('name') will be true and our product will not save if it isn’t present. So in the update action of our controller if @product.save returns false then the render_wizard @product will direct the user back to the same step :add_name. We still set our status to active on the last step since we want all of our validations to run.

Wow that’s cool, but seems like a bunch of work

What you’re trying to do is fairly complicated, we’re essentially turning our Product model into a state machine, and we’re building it inside of our wizard which is a state machine. Yo dawg, i heard you like state machines… This is a very manual process which gives you, the programmer, as much control as you like.

Cleaning up

If you have conditional validation it can be easy to have incomplete Product’s laying around in your database, you should set up a sweeper task using something like Cron, or Heroku’s scheduler to clean up Product’s that are not complete.

lib/tasks/cleanup.rake

namespace :cleanup do
  desc "removes stale and inactive products from the database"
  task :products => :environment do
    # Find all the products older than yesterday, that are not active yet
    stale_products = Product.where("DATE(created_at) < DATE(?)", Date.yesterday).where("status is not 'active'")

    # delete them
    stale_products.map(&:destroy)
  end
end

When cleaning up stale data, be very very sure that your query is correct before running the code. You should also be backing up your whole database periodically using a tool such as Heroku’s PGBackups incase you accidentally delete incorrect data.

Wrap it up

Hope this helps, I’ll try to do a screencast on this pattern. It will really help if you’ve had problems implementing this, to let me know what they were. Also if you have another method of doing partial model validation with a wizard, I’m interested in that too. As always you can find me on the internet @schneems. Thanks for using Wicked!