Voting plugin

July 19, 2009 at 10:32 am 8 comments

I needed to create a voting functionality for my project and first used act_as_voteable plugin. However, there is a newer and better alternative (which is still supported as well!), called vote_fu, git page is here from Peter Jackson.

The link above gives all the required info on how to install use vote_fu. However, the plugin has a problem – it puts votes as boolean (true/false) in the model, and this does not allow you to use it to show the most popular items sorted by total number of positive votes minus total number of negative votes, it can only count positive votes.

What’s needed is to change the plugin to insert integers as vote value (change column vote to integer). This will allow you to do many things, like quickly calculating the sum of votes and sort results based on that. It can also give you easy functionality for allowing multi-point voting system (anything less than 0 becomes a negative post, anything over – positive).

You can use the modified migration below, line 4 is changed from the original (you can just save it to your migration directory and run it, it will overrun the existing votes table – HOWEVER all historical data will be lost!):

class VoteFuChange < ActiveRecord::Migration
  def self.up
    create_table :votes, :force => true do |t|
      t.integer    :vote, :default => -1
      t.references :voteable, :polymorphic => true, :null => false
      t.references :voter,    :polymorphic => true
      t.timestamps      
    end

    add_index :votes, ["voter_id", "voter_type"],       :name => "fk_voters"
    add_index :votes, ["voteable_id", "voteable_type"], :name => "fk_voteables"

    # If you want to enfore "One Person, One Vote" rules in the database, uncomment the index below
    #add_index :votes, ["voter_id", "voter_type", "voteable_id", "voteable_type"], :unique => true, :name => "uniq_one_vote_only"
  end

  def self.down
    drop_table :votes
  end

end

You also need to do some changes to the plugin itself (in directory your rails project/vendor/plugins/lib). What’s needed is to change the methods for calculating the totals. Instead of using count(*), you should use the sum of the vote fields of the required records. As you changed the type of the vote from boolean to integer, you need to also do minor corrections to methods for inserting new votes (instead of true it will now place +1, instead of false -1).

New acts_as_voteable.rb

# ActsAsVoteable
module Juixe
  module Acts #:nodoc:
    module Voteable #:nodoc:

      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def acts_as_voteable
          has_many :votes, :as => :voteable, :dependent => :nullify

          include Juixe::Acts::Voteable::InstanceMethods
          extend  Juixe::Acts::Voteable::SingletonMethods
        end        
      end
      
      # This module contains class methods
      module SingletonMethods
        
        # Calculate the vote counts for all voteables of my type.
        def tally(options = {})
          find(:all, options_for_tally(options.merge({:order =>"count DESC" })))
        end

        # 
        # Options:
        #  :start_at    - Restrict the votes to those created after a certain time
        #  :end_at      - Restrict the votes to those created before a certain time
        #  :conditions  - A piece of SQL conditions to add to the query
        #  :limit       - The maximum number of voteables to return
        #  :order       - A piece of SQL to order by. Eg 'votes.count desc' or 'voteable.created_at desc'
        #  :at_least    - Item must have at least X votes
        #  :at_most     - Item may not have more than X votes
        def options_for_tally (options = {})
            options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit

            scope = scope(:find)
            start_at = sanitize_sql(["#{Vote.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
            end_at = sanitize_sql(["#{Vote.table_name}.created_at <= ?", options.delete(:end_at)&#93;) if options&#91;:end_at&#93;

            type_and_context = "#{Vote.table_name}.voteable_type = #{quote_value(base_class.name)}"

            conditions = &#91;
              type_and_context,
              options&#91;:conditions&#93;,
              start_at,
              end_at
            &#93;

            conditions = conditions.compact.join(' AND ')
            conditions = merge_conditions(conditions, scope&#91;:conditions&#93;) if scope

            joins = &#91;"LEFT OUTER JOIN #{Vote.table_name} ON #{Vote.table_name}.voteable_id = #{table_name}.#{primary_key}"&#93;
            joins << scope&#91;:joins&#93; if scope && scope&#91;:joins&#93;
            at_least  = sanitize_sql(&#91;"COUNT(#{Vote.table_name}.id) >= ?", options.delete(:at_least)]) if options[:at_least]
            at_most   = sanitize_sql(["COUNT(#{Vote.table_name}.id) <= ?", options.delete(:at_most)&#93;) if options&#91;:at_most&#93;
            having    = &#91;at_least, at_most&#93;.compact.join(' AND ')
            group_by  = "#{Vote.table_name}.voteable_id"
            group_by << " AND #{having}" unless having.blank?

            { :select     => "#{table_name}.*, SUM(#{Vote.table_name}.vote) AS count", 
              :joins      => joins.join(" "),
              :conditions => conditions,
              :group      => group_by
            }.update(options)          
        end
     end
      # This module contains instance methods
      module InstanceMethods
        def votes_for
          Vote.sum(:vote, :conditions => [
            "voteable_id = ? AND voteable_type = ? AND vote > 0",
            id, self.class.name
          ])
        end
        
        def votes_against
          Vote.sum(:vote, :conditions => [
            "voteable_id = ? AND voteable_type = ? AND vote < 0",
            id, self.class.name
          &#93;)
        end

        def votes_total
          Vote.sum(:vote, :conditions => [
            "voteable_id = ? AND voteable_type = ?",
            id, self.class.name
          ])
        end
        
        # Same as voteable.votes.size
        def votes_count
          self.votes.size
        end
        
        def voters_who_voted
          voters = []
          self.votes.each { |v|
            voters << v.voter
          }
          voters
        end
        
        def voted_by?(voter)
          rtn = false
          if voter
            self.votes.each { |v|
              rtn = true if (voter.id == v.voter_id && voter.class.name == v.voter_type)
            }
          end
          rtn
        end

        def vote_by?(voter)
          rtn = 0
          if voter
            self.votes.each { |v|
              rtn = v.vote if (voter.id == v.voter_id && voter.class.name == v.voter_type)
            }
          end
          rtn
        end
        
        
      end
    end
  end
end
&#91;/sourcecode&#93;

New acts_as_voter.rb:

&#91;sourcecode language="ruby"&#93;
# ActsAsVoter
module PeteOnRails
  module Acts #:nodoc:
    module Voter #:nodoc:

      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def acts_as_voter
          has_many :votes, :as => :voter, :dependent => :nullify  # If a voting entity is deleted, keep the votes. 
          include PeteOnRails::Acts::Voter::InstanceMethods
          extend  PeteOnRails::Acts::Voter::SingletonMethods
        end
      end
      
      # This module contains class methods
      module SingletonMethods
      end
      
      # This module contains instance methods
      module InstanceMethods
        
        # Usage user.vote_count(true)  # All +1 votes
        #       user.vote_count(false) # All -1 votes
        #       user.vote_count()      # All votes
        
        def vote_count(for_or_against = "all")
          where = (for_or_against == "all") ? 
            ["voter_id = ? AND voter_type = ?", id, self.class.name ] : 
            ["voter_id = ? AND voter_type = ? AND vote = ?", id, self.class.name, for_or_against ]
                        
          Vote.count(:all, :conditions => where)

        end
                
        def voted_for?(voteable)
           0 < Vote.count(:all, :conditions => [
                   "voter_id = ? AND voter_type = ? AND vote > 0 AND voteable_id = ? AND voteable_type = ?",
                   self.id, self.class.name, voteable.id, voteable.class.name
                   ])
         end

         def voted_against?(voteable)
           0 < Vote.count(:all, :conditions => [
                   "voter_id = ? AND voter_type = ? AND vote < 0 AND voteable_id = ? AND voteable_type = ?",
                   self.id, self.class.name, voteable.id, voteable.class.name
                   &#93;)
         end

         def voted_on?(voteable)
           0 < Vote.count(:all, :conditions => [
                   "voter_id = ? AND voter_type = ? AND voteable_id = ? AND voteable_type = ?",
                   self.id, self.class.name, voteable.id, voteable.class.name
                   ])
         end
                
        def vote_for(voteable)
          self.vote(voteable, 1)
        end
    
        def vote_against(voteable)
          self.vote(voteable, -1)
        end

        def vote(voteable, vote)
          vote = Vote.new(:vote => vote, :voteable => voteable, :voter => self)
          vote.save
        end

      end
    end
  end
end

Now, just restart the server and you should use the tally method as you used before (btw, order attribute doesn’t work in the version I used, so you need to modify the code further, if your order is not the default one). Also, you have a new method votes_total, which is equal to votes_for – votes_against, but uses a single DB query.

One final note – the tally will not show any objects that do not have at least one vote, even though the left join is used. I need to research it further, but one workaround is to provide a system vote automatically on the creation of the object.

Advertisements

Entry filed under: Uncategorized. Tags: , , .

Create an object of a type (class) stored in a string Great plugin list

8 Comments Add your own

  • 1. Peter Jackson  |  July 20, 2009 at 5:21 pm

    This is a very well done article and modification.

    As it turns out, I’m working on a rewrite of VoteFu to fix a lot of the weaknesses that are in the current version. The number one item on my list is full test coverage, but the number two item on my list is changing Votes to be stored with numeric vote values. This has benefits for the has_karma module as well, since downvotes do not correctly subtract from karma.

    I’m aware of the :order bug and have tackled that in the next version as well.

    Thanks a lot for writing this up. Our code for the numeric votes is very similar, so I may end up refactoring mine a little to include some of your changes. I’ll give you a shout out in the README.

    Reply
    • 2. allaboutruby  |  July 20, 2009 at 7:13 pm

      Hello Pete, thanks for the approval. I did a minor change to your very useful plug-in. If you need to reuse any of the code pls feel free!

      Reply
  • 3. Peter Jackson  |  July 20, 2009 at 5:21 pm

    Ignore that smiley in the third paragraph. “I’m aware of the ordering bug”

    Reply
  • 4. Ravicious  |  July 31, 2009 at 7:52 pm

    ‘:order’ was changed to ‘ rder’ 😉

    Reply
  • 5. TM Lee  |  August 2, 2009 at 3:47 pm

    It seems the at_least, at_most is not working
    but i changed it to the original which is
    group_by = “#{Vote.table_name}.voteable_id HAVING COUNT(#{Vote.table_name}.id) > 0”
    and it worked..

    Reply
  • 6. kandadaboggu  |  February 20, 2010 at 12:30 pm

    I have added few similar enhancements to the vote_fu plugin
    – The data-type of `vote` column in `votes` table is changed to integer type.
    – Support for vote count caching at the `voteable` model.

    The source code is at http://github.com/kandadaboggu/vote_fu/downloads

    Reply
  • 7. pronounce  |  February 3, 2014 at 3:10 am

    I used to be recommended this blog through my cousin.
    I’m no longer certain whether or not this submit is written through him as nobody else recognize
    such targeted about my difficulty. You are amazing!
    Thank you!

    Reply
  • 8. ริ้วรอย  |  June 4, 2014 at 1:44 pm

    It’s amazing in support of me to have a site, which is useful designed for my knowledge.
    thanks admin

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Trackback this post  |  Subscribe to the comments via RSS Feed


Recent posts

Starting to learn Rails?

Kindle

Get Kindle - the best e-book reader, that I personally use, and the only one that you can read on the beach - very useful: Kindle Wireless Reading Device (6" Display, Global Wireless, Latest Generation)

%d bloggers like this: