A voting extension from scratch for Rails 3 (Part 3 – logic concerns)

After some busy days I finally had the time for part 3 of the MakeVoteable blog series. In the previous episodes we looked at setting up the basic gem structure and configuring our testing environment. Now it’s time to investigate the core logic of make_voteable.

A voting system normally has someone who votes (a voter) and something that is voted for (a voteable). Ruby makes it really easy to have well understandable, concise and beautiful APIs. So if we have a user named scarlett (yeah, I know … you wish to have) as voter and a question object named dumb_question it would totally make sense to make the voting through a call like scarlett.up_vote(dump_question) (hey, what do you expect?! … she’s blond). Most of the other MakeVoteable methods are also called from the user object. That is what I call “user centric” in MakeVoteable. For the other methods (like down_vote, unvote) have a look in the README.

But the big question is, how do we get those methods inside a user and question model? The Ruby On Rails Guides do have an extra section about that topic. Unfortunately it is a bit out of date (but still working!) and with Rails 3 there are much smoother solutions.

So instead of using the sometimes unnecessarily confusing self.included and base.send :extend we use ActiveSupport::Concern.

Let us assume a voter model (that later in a Rails app will mostly be a user model). But let us not just assume it, let us actually create it as we can use it for testing purposes. In the last part we already created empty files named spec/models.rb and spec/schema.rb. We use those files now to create the model to simulate the MakeVoteable extension on (like it will later be used in a Rails app).

# spec/models.rb
class VoterModel < ActiveRecord::Base
  make_voter
end
# spec/schema.rb
ActiveRecord::Schema.define :version => 0 do
  create_table :voter_models, :force => true do |t|
    t.string :name
  end
end

When now running our test with rake rspec or just rake it will create our dummy voter model (with just one attribute named name). Unfortunately it will result in an error as the make_voter method inside the VoterModel class is not yet defined.

We will implement that method in lib/make_voteable.rb. This file is the main entry point of our Rails extension. It will be automatically loaded by Rails when added (by using Bundler) to a Rails application. The only requirement is that it is in the lib directory and named after the extension.

  1. require 'make_voteable/voter'
  2.  
  3. module MakeVoteable
  4.   def voter?
  5.     false
  6.   end
  7.  
  8.   def make_voter
  9.     include Voter
  10.   end
  11. end
  12.  
  13. ActiveRecord::Base.extend MakeVoteable

What we do here is to extend ActiveRecord::Base (line 13) with the module named MakeVoteable (line 3). By doing this all models that inherit from ActiveRecord::Base (actually all default Rails models using ActiveRecord) automatically get the methods defined in that module as class methods. As our our VoterModel test model is such an ActiveRecord model it has those methods, too. It even already calls one of them, the make_voter method (line 8). And this make_voter method then again includes (line 9) a Voter module that was required before (line 1).

This Voter module contains most of the logic of MakeVoteable. All those voter centric methods (like up_vote, down_vote) are in there.

module MakeVoteable
  module Voter
    extend ActiveSupport::Concern
 
    module ClassMethods
      def voter?
        true
      end
    end
 
    module InstanceMethods
      def up_vote(voteable)
        ...
      end
 
      def down_vote(voteable)
        ...
      end
    end
    ...
  end
end

Note that this Voter module extends from the already mentioned ActiveSupport::Concern. What it does is automatically add the the instance methods and class methods defined in an InstanceMethods resp. ClassMethods submodule to a class that includes it (like our ActiveRecord model that calls make_voter). One could even abandon the InstanceMethods submodule and add all instance methods to the module that extends ActiveSupport::Concern directly. It would not make any difference as the module itself is included. This is how it is actually done in MakeVoteable::Voter.

The voter? method of the MakeVoteable module is for convenience. Every ActiveRecord model on which voter? is called and was not made a voter by calling make_voteable returns false. But if make_voteable was called before, it is overridden by the voter? method of the Voter module, and the method there simply returns true. So it is easy to check if the model acts as a voter or not.

That was it for now … upcoming is the Voting model and Rails generators.