acts_as_voteable
Posted by ben, Wed Jan 17 16:31:00 UTC 2007
I realize that there are a couple of acts_as_voteable plugins out there, I have seen two others, but I needed the experience writing a plugin in rails and personally I like mine better :)
I will walk you through the process of writing a ruby on rails plugin and show you how I wrote mine.
Step 1 – setup plugin skeleton
script/generate plugin acts_as_voteable
that will setup the folder structure for your new plugin named ‘acts_as_voteable’
Step 2 – add any additional models we will need
You can add any additional model you will need in the lib folder. We will be adding our vote class so we will add vote.rb to the lib folder.Since the vote will need to have an association with the user that made the vote and the object being voted for we will add those.
belongs_to :voteable, :polymorphic => true
belongs_to :voter, :polymorphic => true
You should have noticed something a little different about these associations from the ones you normally use, we included olymorphic => true. Polymorphic associations are a pretty slick feature in rails, they allow you to join to another object without having to know its typeahead of time. Rails handles all of the connection code for you and you can use it like a normal association. The only difference is in the migration you need to make 2 columns for the association.
t.column :voteable_id, :integer
t.column :voteable_type, :string
The first one you should be used to from any other belongs_to but the second one tells rails what the object type is. This is fairly transparent to you when you are working with the class because you just assign an object to voteable and rails splits the two parts up when it saves. We will look at the other side of that polymorphic association in the next section.
Step 3 – extend ActiveRecord functionality
we will be writing an ActiveRecord plugin (which is one of the easier plugins) so we will want to edit the lib/acts_as_voteable.rb file in our plugin directory to extend that class. Since we are writing an acts_as plugin we will want to put our mixin code where rails keeps the rest of the acts_as functionality, ActiveRecord::Acts, we do this with the following code
module ActiveRecord
module Acts #:nodoc:
module Voteable #:nodoc:
# acts_as_voteable code here
end
end
end
The first 2 lines are to insert a mixin into the already existing module ActiveRecord::Acts and the third creates your module Voteable. So once you have created the plugin you could reference your module with ActiveRecord::Acts::Voteable. The #:nodoc: tells ruby not to include that module in the RDoc documentation.
We will want to separate our class methods, instance methods & singleton methods into separate modules so that we can include them separately into the ActiveRecord where we want them.
module ClassMethods
def acts_as_voteable(options = {})
write_inheritable_attribute(:acts_as_voteable_options, {
:voteable_type => ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
})
class_inheritable_reader :acts_as_voteable_options
has_many :votes, :as => :voteable, :dependent => true
include ActiveRecord::Acts::Voteable::InstanceMethods
end
end
module InstanceMethods
def vote_for(voter)
votes << Vote.new({:voteable => self, :voter => voter, :score => 1}) unless voted?(voter)
end
...
def voted?(voter)
votes.any? { |vote| vote.voter == voter }
end
end
There are a couple of things to notice in this section of code …
The only method available to an ActiveRecord class is the acts_as_voteable method until that method is called then it includes the instance methods once it is called.
I told you we would discuss the other side of polymorphic associations and here it is. When you make an association to a polymorphic association you must use the :as to tell the association what field that association belongs to. In the case of the votes object we are telling the has_many association that the ActiveRecord extended by acts_as_voteable will have many votes and the ActiveRecord class should be stored in the voteable field.
In the InstanceMethods module, I abreviated the snippet here for readability, you will notice that many of the methods take a parameter called voter. This is intended to be a user but it can be anything that is casting that particular vote. The votes are restricted to one per voter. Voter is also stored in a polymorphic association so you can place any kind of object in here.
You will also notice a section with write_inheritable_attribute and class_inheritable_reader. This is just there so that I have to ability to extend the plugin with options passed to the acts_as_voteable call if I choose to do so in the future.
Step 4 – wire this up in the init.rb
require 'acts_as_voteable'
ActiveRecord::Base.send(:include, ActiveRecord::Acts::Voteable)
require File.dirname(__FILE__) + '/lib/vote'
The first line is fairly obvious we want to include the ‘acts_as_voteable’ class with all of our functionality in it. The last line is to include the model we created to assist this plugin. The second line is the magic line. This tells the ActiveRecord::Base class to include all of the modules and methods from the ActiveRecord::Acts::Voteable module. That is only half of the magic though. The other half is added in the ActiveRecord::Acts::Voteable module with the following code.
def self.included(base)
base.extend(ClassMethods)
end
This is a callback function for the include method that passed the object that include was called on. So base will be the ActiveRecord::Base and the methods in the ClassMethods module will be added to base. Now you can call acts_as_voteable from any ActiveRecord object. And once you do, as you can see from the previous section, all of the instance methods are added to that ActiveRecord class.
And that’s about all there is to creating a acts_as plugin in rails. Once I get the code into SVN I will update this post with instructions for installing the plugin. Here are the instructions for installing the plugin