Our first Rails plugin- ActsAsModerated!
Disclaimer: This post is first about my experience writing a plugin and then about the plugin itself. If you just want to learn about the plugin scroll down to the “using the plugin” part.
Well, after a while documenting, updating and polishing here is our first Rails plugin (in fact it’s an ActiveRecord plugin =D ). It isn’t something big or something that you couldn’t live without, but it was something helpful on our last project and it doesn’t hurt to give something back to the community after everything that they gave us.
A little history
My last task was building a social community (yeah, another one, but this one is somewhat different than the others in their target audience) which I can’t talk about right now (once they have launched I promise to comment about it here), but one of the features was a moderation queue. The client wanted that every create/update/deletion were held for moderation, in the beginning of the project it looked good enough, we didn’t have a lot of models and I could write a simple moderation queue for those two or so objects.But it’s never as easy as it seems :).
Not too long later the client sent a document asking for a lot of other models (like jumping from two to a dozen) and I couldn’t just write a different moderation queue for every model, so, time to look for a plugin (search first, build later). I couldn’t find anything that worked like I wanted and the closest one needed a “clone” table to keep the models under moderation, so I thought it was too much for something as simple as a moderation queue. Afterall, this is Ruby, not Java =P
So, I couldn’t find anything that did what I wanted (and the way that I wanted).
On the shoulders of giants
Coming from my Java/C# background I was really expecting something “hard” or maybe “challenging” (I wrote a JavaServer Faces component last year, so all my sins are paid until 2010). First, I spent some time reading the “acts_as_taggable_on_steroids” and “attachment_fu” plugins.
After some time reading them I was starting to form an idea about how an acts_as plugin should behave. When you are building an acts_as plugin you have to imagine what new behaviour you will add to your models, my idea was that instead of saving/destroying the object, I could send it to the moderation queue, so, every model that acted as moderated would have a method called to_moderation that would send it to the moderation queue without touching it’s original state, only the moderation object should be saved here, not the moderated model itself.
So, I wrote the Moderation model containing the information that I needed, my idea was to keep it as simple and flexible as possible, I needed to store a lot of different models and I didn’t wanted a separate “clone” table for all of them, so the moderations table would have to store the model.But how could I do it without saving the model?There is an interesting feature on ActiveRecord that many people don’t know about that is the “serializable” attributes. You can “serialize” a complex object into a text column storing it in a YAML representation, so, instead of creating a new table for every model, I would serialize the attributes hash of every ActiveRecord model into the text column. Multiple tables problem solved
Going on with the development I noticed that it was really hard to test the plugin using RSpec and the way that plugins are packaged has always bothered me. In Java we did our builds using Ant or (lately) Maven and when you’re using Maven you define the version of your dependencies, so that whenever you have to switch to another machine (yep, I know this is coming in Rails 2.1), it was just a matter of installing Maven and telling it to download the dependencies again.When you’re using a Rails plugin you usually can’t find out which version it is, specially if you’re running on someone else’s code, and this isn’t really good when you just happen to fall on legacy code that you have to deal with.
Fortunately, I saw the “Plugin Patterns” e-book from Andrew Stewart and gave it a spin. Even if you are not planning to write a Rails plugin, you should absolutely check this book, there are great insights about how Rails works and you will figure out that building a plugin is as easy as anything in Ruby, it’s just a matter of knowing where to place your code.And Andrew’s book gave me the idea that was missing, bundle the plugin as a gem!
So, I set out to use Hoe (another nice tool from the Seattle.rb guys) to build my gem. Testing with RSpec became easier and I didn’t need to make crazy black magic to run my specs, it was just a spec file, a helper and a migration, “rake specs” and be done with it. Now, as I had a gem ready to go, I wrote the gem docs and also bundled a simple example application to help people figure out their way when trying to use the plugin.It wasn’t really easy because it was my first time doing it, but now that I’ve learned the quirks and have found plenty of documentation the next time it will be just another plugin.
If you find that something in your app could be moved out from it, take some of your time to move it out, you might learn a lot about your coding style and how to create reusable code in a simple and easy way.
Using the plugin
Enough talking!
Lets see how you can use this plugin to build your own moderation queue. First you have to install the gem (our Rubyforge account hasn’t been enabled yet, so you will have to get the gem here). After installing it you can start to use it in your own rails project or take a look at the example application that is available at the gem directory.Once your app is ready, you have to unpack the gem at your vendor/plugins dir, do it using the following command:
gem unpack acts_as_moderated
This should create an “acts_as_moderated-0.5.0”. After this you have to generate the moderations object migration, just type:
ruby script/generate acts_as_moderated_migration
A migration for the Moderation model object will be created. You can run your migrations to create the table:
rake db:migrations
With the table created, time to use it in your models. Imagine that you have an Article model that you want to be moderated, here’s what you have to do:
class Article
acts_as_moderated
end
I hope you haven’t typed too much
With this call, instances of the Article class will now have two new methods added: to_moderation and to_moderation!. Both of these methods will create a new moderation object containing the current state of the moderated model (in this case, the Article object). The only difference is that the one ending with an exclamation sign will throw an exception if the moderation could not be created.The to_moderation object receives an options hash where you can pass complementary attributes to the moderation object. The default to actions in moderations is “save”, so, if you want to place a destroy action on the moderation queue, you will have to add it to the to_moderation method call, like this:
@article.to_moderation :action => 'destroy'
When this moderation is applied it will remove the moderated object. The only actions accepted right now are ‘save’ and ‘destroy’.And here comes some real world action, that’s how your new controller actions would look:
def create
@article = Article.new( params[:article] )
if @article.valid?
@article.to_moderation # sends this object to the moderation queue
# the object attributes are saved with the moderation object
flash[:notice] = 'Your change was received and placed on our moderation queue'
redirect_to articles_path
else
render :action => 'new'
end
end
And then you can “moderate” a change calling the moderate method on a Moderation object (try to say this fast!):
def moderate
@moderation = Moderation.find( params[:id] )
@moderation.moderate! #moderates the change, adding/updating/removing the moderated model
flash[:notice] = 'The changes have been applied'
redirect_to moderations_path
end
Imagine now that you wanted to know which user tried to peform this change, how could you do that?The to_moderation method receives an array of attributes as a param and those attributes are sent to the Moderation object, so you could just create a new migration adding a new field to the Moderation model:
add_column :moderations, :user_id, :integer
And send the parameter to the to_moderation object, just like this:
@article.to_moderation( :user_id => current_user.id )
To enable this on your app, just create a file called moderation.rb on your app/models folder and add the belongs_to declaration (classes in Ruby are always open, remember?):
class Moderation
belongs_to :user
end
And you’re done, you have your own moderation queue working for anything and you can even add the user who performed the change. Take a look at the sample app and readme that comes bundled with the gem to learn more about how to code (and even view a “diff” of the moderation object).
Trackbacks
Use this link to trackback from your own site.
Looks excellent!
Very nicely written and useful article. Hopefully this will inspire me to write a plugin, which I want to, need to….
And this plugin is also very useful. I could have used it on small Rails app that I wrote a while back where I needed moderation on one model and I resorted to some hack. Thanks for sharing.
Glad you found the book useful!
I had to add moderation to an app recently (sadly about two weeks before reading this article) and found two plugins:
http://github.com/mmower/simply_versioned/tree/master
http://opensoul.org/2006/7/21/acts_as_audited
I started with Simply Versioned, which worked perfectly, but then I learned I also had to track who made each change. So I switched to Acts As Audited.
Anyway, I just thought you might be curious to see how they solve a similar problem.
Hi Andy,
We are the ones that should thank you for publishing such a good material about an important but not well documented feature in Rails.
And thanks for the hints, i’ll take a look at them