acts_as_commentable is a nice little ruby on rails plugin. It extends your ActiveRecord classes giving them comments. We are going to use comments on all kinds of things, starting with recipes, of course. However, AAC lacks a critical feature: the ability for users to approve comments before they are displayed. In this post I am going to run through extending AAC using acts_as_state_machine.
The first thing I did (and do to all the plugins we use) was pistonize the plugin so I could hack on it without fear of getting my changes destroyed.
I start off simply here by adding two states to the Comment model: :pending and :approved.
class Comment < ActiveRecord::Base
# The first element of this array is the initial state
VALID_STATES = [ :pending, :approved ]
acts_as_state_machine :initial => VALID_STATES[0]
event :approve do
transitions :from => :pending, :to => :approved
end
VALID_STATES.each do |_state|
# Define _state as a state
state _state
end
# More code snipped
end |
class Comment < ActiveRecord::Base
# The first element of this array is the initial state
VALID_STATES = [ :pending, :approved ]
acts_as_state_machine :initial => VALID_STATES[0]
event :approve do
transitions :from => :pending, :to => :approved
end
VALID_STATES.each do |_state|
# Define _state as a state
state _state
end
# More code snipped
end
Now we are going to write some real code, so here comes a little RSpec. aac provides three class methods:
class Comment < ActiveRecord::Base
class << self
# Helper class method to lookup all comments assigned
# to all commentable types for a given user.
def find_comments_by_user(user)
# Helper class method to look up all comments for
# commentable class name and commentable id.
def find_comments_for_commentable(commentable_str, commentable_id)
# Helper class method to look up a commentable object
# given the commentable class name and id
def find_commentable(commentable_str, commentable_id)
end |
class Comment < ActiveRecord::Base
class << self
# Helper class method to lookup all comments assigned
# to all commentable types for a given user.
def find_comments_by_user(user)
# Helper class method to look up all comments for
# commentable class name and commentable id.
def find_comments_for_commentable(commentable_str, commentable_id)
# Helper class method to look up a commentable object
# given the commentable class name and id
def find_commentable(commentable_str, commentable_id)
end
Since it didn’t come with Test::Unit or RSpec tests I wrote up some test for these methods.
describe Comment, "class methods" do
fixtures :comments, :recipes, :users
it "should find comments by user" do
Comment.find_comments_by_user( comments(:comment_one).user ).should all_belong_to( comments(:comment_one).user )
end
# This could be more specific
it "should find comments for a particular class" do
Comment.find_comments_for_commentable( Comments(:comment_one).commentable_type, comments(:comment_one).commentable_id ).should be_an_instance_of(Array)
end
it "should find all comments for a particular class" do
# I happen to know that comment_one is a recipe comment
Comment.find_commentable( "Recipe", comments(:comment_one).commentable_id ).should be_an_instance_of(Recipe)
end
end |
describe Comment, "class methods" do
fixtures :comments, :recipes, :users
it "should find comments by user" do
Comment.find_comments_by_user( comments(:comment_one).user ).should all_belong_to( comments(:comment_one).user )
end
# This could be more specific
it "should find comments for a particular class" do
Comment.find_comments_for_commentable( Comments(:comment_one).commentable_type, comments(:comment_one).commentable_id ).should be_an_instance_of(Array)
end
it "should find all comments for a particular class" do
# I happen to know that comment_one is a recipe comment
Comment.find_commentable( "Recipe", comments(:comment_one).commentable_id ).should be_an_instance_of(Recipe)
end
end
If you are confused by should all_belong_to then you should check out my previous post. With these specs out of the way we can go on to adding more new code.
it "should find approved comments by user" do
Comment.find_approved_comments_by_user( comments(:comment_one).user ).should all_be_in_state("approved")
end
it "should find pending comments by user" do
Comment.find_pending_comments_by_user( comments(:comment_one).user ).should all_be_in_state("pending")
end
end |
it "should find approved comments by user" do
Comment.find_approved_comments_by_user( comments(:comment_one).user ).should all_be_in_state("approved")
end
it "should find pending comments by user" do
Comment.find_pending_comments_by_user( comments(:comment_one).user ).should all_be_in_state("pending")
end
end
Now, normally you would write one spec at a time, but I think I would bore my readers, so I combined these two. Also take note that I am using another custom RSpec matcher all_be_in_state(). It looks a lot like all_belong_to(), so I leave its implementation as an exercise to the reader (unless I can get another blog post out of it). To get these tests to pass I add a few lines of code:
VALID_STATES.each do |_state|
# Define _state as a state
state _state
# Add Comment.find__comments methods
( class << self; self; end ).instance_eval do
define_method "find_#{_state}_comments_by_user" do |_user|
find_in_state( :all, _state, :conditions => ["user_id = ?", _user.id], :order => "created_at DESC" )
end
end
end |
VALID_STATES.each do |_state|
# Define _state as a state
state _state
# Add Comment.find__comments methods
( class << self; self; end ).instance_eval do
define_method "find_#{_state}_comments_by_user" do |_user|
find_in_state( :all, _state, :conditions => ["user_id = ?", _user.id], :order => "created_at DESC" )
end
end
end
I am not a method_missing kind of guy, and prefer the dynamic-method metaprogramming style. This lot of code defines class methods at runtime that find Comments in specific states. I am actually using whytheluckystiff’s metaid to hide some of the meta-junk, but I thought I should spell it out here for clarity.
Well, now we have a Comment class with two states and code to limit finds to cmments in a specific state. Right now, that is all I have. Here is the full code for the Comment class and the RSpec. You will see another custom RSpec matcher here, require_a().
class Comment < ActiveRecord::Base
# The first element of this array is the initial state
VALID_STATES = [ :pending, :approved ]
acts_as_state_machine :initial => VALID_STATES[0]
belongs_to :commentable, :polymorphic => true
belongs_to :user
event :approve do
transitions :from => :pending, :to => :approved
end
validates_associated :user
validates_presence_of :comment, :commentable_id, :commentable_type, :state, :user_id
VALID_STATES.each do |_state|
# Define _state as a state
state _state
# Add Comment.find_<state>_comments methods
meta_def "find_#{_state}_comments_by_user" do |_user|
find_in_state( :all, _state, :conditions => ["user_id = ?", _user.id],
:order => "created_at DESC" )
end
end
class < < self
# Helper class method to look up a commentable object
# given the commentable class name and id
def find_commentable(commentable_str, commentable_id)
commentable_str.constantize.find(commentable_id)
end
# This could be refactored into find_<state>_comments_by_user (somehow)
def find_comments_by_user(_user)
find( :all, :conditions => ["user_id = ?", _user.id],
:order => "created_at DESC" )
end
# Helper class method to look up all comments for
# commentable class name and commentable id.
def find_comments_for_commentable(commentable_str, commentable_id)
find( :all,
:conditions => [ "commentable_type = ? and commentable_id = ?",
commentable_str, commentable_id ],
:order => "created_at DESC" )
end
end
end</state> |
class Comment < ActiveRecord::Base
# The first element of this array is the initial state
VALID_STATES = [ :pending, :approved ]
acts_as_state_machine :initial => VALID_STATES[0]
belongs_to :commentable, :polymorphic => true
belongs_to :user
event :approve do
transitions :from => :pending, :to => :approved
end
validates_associated :user
validates_presence_of :comment, :commentable_id, :commentable_type, :state, :user_id
VALID_STATES.each do |_state|
# Define _state as a state
state _state
# Add Comment.find_<state>_comments methods
meta_def "find_#{_state}_comments_by_user" do |_user|
find_in_state( :all, _state, :conditions => ["user_id = ?", _user.id],
:order => "created_at DESC" )
end
end
class < < self
# Helper class method to look up a commentable object
# given the commentable class name and id
def find_commentable(commentable_str, commentable_id)
commentable_str.constantize.find(commentable_id)
end
# This could be refactored into find_<state>_comments_by_user (somehow)
def find_comments_by_user(_user)
find( :all, :conditions => ["user_id = ?", _user.id],
:order => "created_at DESC" )
end
# Helper class method to look up all comments for
# commentable class name and commentable id.
def find_comments_for_commentable(commentable_str, commentable_id)
find( :all,
:conditions => [ "commentable_type = ? and commentable_id = ?",
commentable_str, commentable_id ],
:order => "created_at DESC" )
end
end
end</state>
require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
module CommentSpecHelper
end
describe Comment do
fixtures :comments
include CommentSpecHelper
before(:each) do
@comment = Comment.new
end
it "should start out in pending state" do
@comment.state.should == "pending"
end
it "sould transition to approved" do
@comment = comments(:pending_comment)
@comment.approve!
@comment.state.should == "approved"
end
it "should require a comment" do
@comment.should require_a(:comment)
end
it "should require a commentable_id" do
@comment.should require_a(:commentable_id)
end
it "should require a commentable_type" do
@comment.should require_a(:commentable_type)
end
it "should require a state" do
@comment.should require_a(:state)
end
it "should require a user_id" do
@comment.should require_a(:user_id)
end
end
describe Comment, "class methods" do
fixtures :comments, :recipes, :users
it "should find all comments for a particular class" do
# I happen to know that comment_one is a recipe comment
Comment.find_commentable( "Recipe", comments(:comment_one).commentable_id ).should be_an_instance_of(Recipe)
end
it "should find comments by user" do
Comment.find_comments_by_user( comments(:comment_one).user ).should all_belong_to( comments(:comment_one).user )
end
it "should find approved comments by user" do
Comment.find_approved_comments_by_user( comments(:comment_one).user ).should all_be_in_state("approved")
end
it "should find pending comments by user" do
Comment.find_pending_comments_by_user( comments(:comment_one).user ).should all_be_in_state("pending")
end
# This could be more specific
it "should find comments for a particular class" do
Comment.find_comments_for_commentable( comments(:comment_one).commentable_type, comments(:comment_one).commentable_id ).should be_an_instance_of(Array)
end
end |
require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
module CommentSpecHelper
end
describe Comment do
fixtures :comments
include CommentSpecHelper
before(:each) do
@comment = Comment.new
end
it "should start out in pending state" do
@comment.state.should == "pending"
end
it "sould transition to approved" do
@comment = comments(:pending_comment)
@comment.approve!
@comment.state.should == "approved"
end
it "should require a comment" do
@comment.should require_a(:comment)
end
it "should require a commentable_id" do
@comment.should require_a(:commentable_id)
end
it "should require a commentable_type" do
@comment.should require_a(:commentable_type)
end
it "should require a state" do
@comment.should require_a(:state)
end
it "should require a user_id" do
@comment.should require_a(:user_id)
end
end
describe Comment, "class methods" do
fixtures :comments, :recipes, :users
it "should find all comments for a particular class" do
# I happen to know that comment_one is a recipe comment
Comment.find_commentable( "Recipe", comments(:comment_one).commentable_id ).should be_an_instance_of(Recipe)
end
it "should find comments by user" do
Comment.find_comments_by_user( comments(:comment_one).user ).should all_belong_to( comments(:comment_one).user )
end
it "should find approved comments by user" do
Comment.find_approved_comments_by_user( comments(:comment_one).user ).should all_be_in_state("approved")
end
it "should find pending comments by user" do
Comment.find_pending_comments_by_user( comments(:comment_one).user ).should all_be_in_state("pending")
end
# This could be more specific
it "should find comments for a particular class" do
Comment.find_comments_for_commentable( comments(:comment_one).commentable_type, comments(:comment_one).commentable_id ).should be_an_instance_of(Array)
end
end
–Dean