I approve... approvable things ... and you can too!

I recently built a website for a client that needed every type of content “approved” before it should appear on its public pages. I started out thinking that there may be many different states of approval and that perhaps I should use actsasstatemachine to manage these state transitions. After a bit of thinking and frustration with how AASM saves and reloads each model after a transition, I decided I could create something much simpler for my own needs.

What I ended up with was an Approvable Module that I could add into any of my models that had this Approvable behavior. For tracking approvals, I simply use a datetime field called approved_at in each of my Approvable models, whos value would either be Nil or the datetime it was approved at.

In my initial use case, I had 3 models to consider … Users, Affiliations and their hasmany :through join … Affiliations. A User hasmany Affiliates :through their Affiliations. All content is created by a User … in the case that content is created but an Admin User, I want that content approved by default. Both Affiliates and their Affiliations are approvable, so when an Admin creats an Affiliate, it was important to also approve his affiliation. I make use of ActiveRecord Callbacks for auto approving.

And this is how I did it. (this code is mostly written for rails 2.1)

First, a peak at my controller for Affilates. You can see in the index action that I’ve predefined a named_scope called ‘approved’ that filters a find to retrieve only approved Affiliates. I might have also used another called ‘unapproved’ in its place. My create action creates an Affiliate and assigns some of its attributes that are protected from mass-assignment in the normal Rails way. All the magic of approval is in the model’s themselves. Not much else to see here.

Worth mentioning I suppose, I could also use any of the approving methods manually from my controller, but I prefer to keep my business logic in my models. Most of the methods are injected into the model objects themselves, so really they can be used anywhere.

class AffiliatesController < ApplicationController

  def index
    @affiliates = Affiliate.approved.find.all

    respond_to do |format|
      format.html # index.html.erb
    end
  end

  def create
    @affiliate = Affiliate.new(params[:affiliate])
    raise NoPermission unless @affiliate.is_createable_by?(current_user)
    @affiliate.contact = current_user
    @affiliate.creator = current_user
    @affiliate.users << current_user
    respond_to do |format|
      if @affiliate.save
        flash[:notice] = 'Affiliate was successfully created.'
        format.html { redirect_to(account_affiliations_path) }
      else
        format.html { render :action => "new" }
      end
    end
  end

end

This is my example migration for Affiliates. Note the t.datetime :approved_at field.

class CreateAffiliates < ActiveRecord::Migration
  def self.up
    create_table :affiliates do |t|
      t.references :creator, :null => false
      t.string :name
      t.text :description
      t.datetime :approved_at, :default => nil
      t.timestamps
    end
    add_index :affiliates, :creator_id
  end

  def self.down
    drop_table :affiliates
  end
end

This is my example migration for Affiliations. Note the t.datetime :approved_at field.

class CreateAffiliations < ActiveRecord::Migration
  def self.up
    create_table :affiliations do |t|
      t.references :affiliate, :null => false
      t.references :user, :null => false
      t.datetime :approved_at, :default => nil
      t.timestamps
    end
    add_index :affiliations, [:affiliate_id, :user_id]
  end

  def self.down
    drop_table :affiliations
  end
end

Here is my Approvable module which I’ve stuck in my RAILS_ROOT/lib folder. It’s responsible for the magic of providing methods to my Approvable models to approve/unapprove them. You’ll notice that there are all the usual accessors and a few methods to change the approval state of my models.

module Approvable

  def self.included(base)
    base.send :include, InstanceMethods
    base.named_scope :approved, lambda { |*args| {:conditions => ["#{base.to_s.tableize}.approved_at IS NOT NULL AND #{base.to_s.tableize}.approved_at < ?", (args.first || Time.now)]} }
    base.named_scope :unapproved, lambda { |*args| {:conditions => ["#{base.to_s.tableize}.approved_at IS NULL OR #{base.to_s.tableize}.approved_at > ?", (args.first || Time.now)]} }
  end

  module InstanceMethods

    def approved?
      !self.approved_at.nil?
    end

    def approve
      self.approved_at = Time.now
    end

    def approve!
      self.approved_at = Time.now
      save!
      reload
    end

    def approval_status
      approved? ? "Approved" : "Unapproved"
    end

    def unapproved?
      self.approved_at.nil?
    end

    def unapprove
      self.approved_at = nil
    end

    def unapprove!
      self.approved_at = nil
      save!
      reload
    end

    def toggle_approval!
      approved? ? unapprove! : approve!
    end

  end

end

Here is an example of how to use the Approvable module in your models. Simply include the module and you’re good to go. In my example, a retrieved Affiliate object can be approved and saved by saying @affiliate.approve! … or you can approve it without saving by means of @affiliate.approve. The creator.has_role?(‘admin’) stuff is something I coded separately into my User model for permissioning.

class Affiliate < ActiveRecord::Base

  include Approvable

  belongs_to :creator, :class_name => 'User'
  has_many :affiliations
  has_many :users, :through => :affiliations

  before_create :auto_approve_affiliate_if_creator_is_an_admin

  private

    def auto_approve_affiliate_if_creator_is_an_admin
      approve if creator.has_role?('admin')
    end

end

Here is the same example, but on the Affiliations join model.

class Affiliation < ActiveRecord::Base

  include Approvable

  belongs_to :affiliate
  belongs_to :user

  before_create :auto_approve_affiliation_if_user_is_an_admin

  private

    def auto_approve_affiliation_if_user_is_an_admin
      approve if user.has_role?('admin')
    end

end

And that’s basically it, if you have any questions please feel free to ask. I’ve found this module pretty reusable accross multiple applications. I hope you do as well. I’ve posted the official code over on github as usual: useful-modules-for-rails

comments powered by Disqus