Summer Rails Plugin Series #2: needs_approval
(by Erik Peterson on July 9th 2008)

Ok, so I know I promised a Rails plugin every week. About a week after I made that promise I learned something: making a Rails plugin every week is fucking impossible. Even if you've already written the code and all you need to do is extract it. It is impossible. I'd be lucky to fit in one plugin a month. Still, one plugin a month for 6 months is still a pretty decent clip, so I'd be satisfied if I can hold myself to that (relaxed) schedule.

Anyways, next up is a plugin called needs_appoval. This is a pretty simple plugin designed to help you manage approval flows. If you have records that need to be approved by users, this plugin might help you out. It also has the following features:

  • Ladder-based approvals (where one user can only approve after the users listed previous to him have approved as well)
  • Approval requirement conditions (where a user only needs to approve on certain conditions)
  • Optional, built-in authentication (where a user must enter in their password whenever they approve something- often required by 21 CFR Part 11 and ISO 9001)

Instructions

Install using the command

script/plugin install git://github.com/subwindow/needs_approval.git

Generate a scaffolded approval model/controller using the command

script/generate approval APPROVALMODEL USERMODEL [--include-authentication] [--skip-migration]
Example
script/generate approval approval user

Put the approval structure definition in your desired models

class Order < ActiveRecord::Base
    needs_approval do
      of User.find_by_login("boss")
      of User.find_by_login("bigboss") if total_price > 1000
    end
  end

For ladder-based approvals, use the following. The function next_approvers goes in order of approval definition.

needs_approval(:ladder => true)

Include approval information in the show page of an approved object

<%= approvals_for(@order) %>

Other useful functions:

# Boolean, returns true if all approvals have been satisfied
@order.has_all_approvals?

# Returns an array of all users that are required to approve this object
@order.approvals_needed

# Returns an array of users that have approved this object
@order.approvals

# Returns an array of users that need to approve this object, but have not yet
@order.failed_approvals

# Boolean, returns true if this object requires and does not yet have the approval of this user
@order.needs_approval_of(user)

# Boolean, returns true if this user has approved this object
@order.passes_approval(user)

# Returns an array of users that are set to approve this next (for ladder-based approvals)
@order.next_approver

Note

This plugin kind of depends on restful_authentication or something similar. A "current_user" method must be accessible which returns the currently logged in user.

If the current_user function is named differently, either:

  • define an alias, or
  • change the references in the approvals controller and pass in the current user as a second argument to the approvals_for helper [eg: "approvals_for(@order, active_user)"]

An example app is located in the directory example_app. You can delete this if you don't want it (especially if you use TextMate-style "Go To File"). To run, execute the following commands:

cd vendor/plugins/needs_approval/example_app
script/plugin install git://github.com/subwindow/needs_approval.git
rake db:create
rake db:migrate
rake db:fixtures:load
script/server

Conclusion

I hope somebody can find this helpful. This was originally a much larger plugin (>100 lines; currently ~50), but in the process of extracting it I refactored the code and made it much smaller. This means it doesn't seem as useful, but at the same time it was very useful to myself because I end up with a much nicer codebase. As usual, If you have any questions, concerns or have found any bugs, please email me at erik [at] subwindow dotcom. Or, reply here. Or, go to the github page and fork the code.

Next up: acts_as_filtered. A plugin to assist in dynamically filtered models. The code is very mature and in production use, but it might need to be refactored and generalized a bit. Hopefully I'll get it out before the end of the month.