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
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.
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/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.
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).
Voter module contains most of the logic of MakeVoteable. All those voter centric methods (like
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
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.
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
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.