about me

I'm a 35'ish year old web application developer from South Portland, Maine. I love meeting fellow techies, drop me a line if you want to talk shop.

Categories

I approve… approvable things … and you can too!

Posted on 09/22/2008 at 07:50 AM

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 acts_as_statemachine 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 has_many :through join ... Affiliations. A User has_many 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 :approvedlambda { |*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 :unapprovedlambda { |*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

1 Comments

Comment #1 by Jason Leveille  on  09/22  at  08:19 PM

This looks really elegant.  Nice work.

Leave a comment?

Please use Pastie or Gist if you need to write code in your comments.

Name:

Email:

Location:

URL:

Remember my personal information

Notify me of follow-up comments?

Please enter the word you see in the image below:


RailsConf 2008 Often times I will release code for free or go that extra distance to help others online. If my skills were useful to you, please consider a small donation. Thank you very much.

recommend me!

Search

You Can Find Me

@ github.com
@ twitter.com
@ calendaraboutnothing

My Wishlists

@ Amazon.com

My Other Sites

Foundation's Edge, RJones Family, We're Not.com (only for staging), Ailee Jones (same as rjones for now)

Friends of Mine

Aaron, Barnaby, Brian, Chris, Dirk, Frank, Four, Justin, Matt, Mike, Monty, Paul, Sean, Travis

IRC Hangouts

I can usually be found lounging on irc.freenode.net while I work, on the following channels: #fauna, #github, #hello-heroku, #jquery, #passenger, #ruby, #rubyonrails, #slicehost, #sproutcore, #textmate, #werenot.