Skip to content

Commit

Permalink
README and a few method changes.
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Brauchli <cbrauchli@gmail.com>
  • Loading branch information
cbrauchli committed Oct 4, 2011
1 parent 095c5bf commit 9571d76
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 24 deletions.
116 changes: 114 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,32 @@ afterwards execute

and you're done.

Configuration
-------------

By default, RedisVoteable's settings are

redis_voteable_settings = {
:host => 'localhost',
:port => '6379',
:db => 0,
:key_prefix => 'vote:'
}

If you'd like to override any of the settings, just add a line like the
following one to a config file or initializer.

RedisVoteable::redis_voteable_settings = {
:db => 4,
:key_prefix => 'voterecord:'
}

Usage
-----

Note that in this example an ActiveRecord model is used, however, any other
ORM will do, as long as the object defines an `id` method. Also, for a couple
of methods, a `find` method is required.
of methods, a `find` or `get` method is required. I'll point those out below.

# Specify a voteable model.
class Option < ActiveRecord::Base
Expand All @@ -37,7 +57,99 @@ of methods, a `find` method is required.
include RedisVoteable
acts_as_voter
end

# Votes up the question by the user.
# If the user already voted the question up then an AlreadyVotedError is raised.
# If the same user already voted the question down then the vote is changed to an up vote.
user.up_vote(question)

# Votes the question up, but without raising an AlreadyVotedError when the user
# already voted the question up (it just ignores the vote and returns false).
user.up_vote!(question)

user.down_vote(question)
user.down_vote!(question)

# Clears a already done vote by an user.
# If the user didn't vote for the question then a NotVotedError is raised.
user.clear_vote(question)

# Does not raise a NotVotedError if the user didn't vote for the question
# (it just ignores the unvote and returns false).
user.clear_vote!(question)

# If you'd prefer, unvote is an alias for clear_vote.
user.unvote(question)
user.unvote!(question)

# The number of up votes for this question.
question.up_votes

# The number of down votes for this question.
question.down_votes

# The total number of votes for this question.
question.total_votes

# The number of up votes the user has cast.
user.up_votes

# The number of down votes the user has cat.
user.down_votes

# The total number of votes the user has cast.
user.total_votes

# up votes - down votes (may also be negative if there are more down votes than up votes)
question.tally

# The lower bound of the Wilson confidence interval. The default value for
# z is 1.4395314800662002, which estimates bounds with 85% confidence.
# The value can be modified in lib/voteable.rb.
# See http://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval
# and: http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
question.confidence

# The upper bound of the Wilson confidence interval.
question.confidence(:upper)

# Returns true if the question was voted by the user
user.voted?(question)

# Returns true if the question was up voted by the user, false otherwise
user.up_voted?(question)

# Returns true if the question was down voted by the user, false otherwise
user.down_voted?(question)

# Returns :up, :down, or nil depending on how the user voted
user.vote_value?(question)

# Access voted voteables through voter (slow)
voteable = user.voteables.first
voteable.up_voted?(user) # true if up voted by user, false otherwise
voteable.vote_value?(user) # returns :up, :down, or nil if user didn't vote on voteable

# Access votings through voteable (slow)
voter = question.voters.first
voter.up_voted?(question) # true if up voted question, false otherwise
voter.vote_value?(question) # returns :up, :down, or nil if voter didn't vote on question

TO DO:
------

* Add support for getting raw voteable and voter arrays, which would save time
by not instantiating every object.

* (Related to the next point.) Automatic ranking of voteables. Consider using a sorted set in redis.

More to come…
* Add some sort of namespacing/grouping support. That way, `voteables` could
be grouped and ranked within their group. An example use case would be users
voting on many options to multiple questions. The options would be grouped
by the question to which they belonged and could be easily ranked for a
question. Could use sorted set to store grouping and rankings within
groupings, but that may not be practical. Could also store the keys of all
of the voteables in a grouping in a set on Redis and rank them in Ruby,
which makes it possible to rank by Wilson confidence score.

Copyright © 2011 Chris Brauchli, released under the MIT license
11 changes: 9 additions & 2 deletions lib/redis_voteable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ module RedisVoteable
:host => 'localhost',
:port => '6379',
:db => 0,
:key_prefix => "vote:",
:key_prefix => 'vote:',
}

def redis_voteable_settings=(new_settings)
@@redis_voteable_settings.update(new_settings)
end

mattr_accessor :redis
@@redis = Redis.new(@@redis_voteable_settings)
# @@redis =

def prefixed(sid)
"#{@@redis_voteable_settings[:key_prefix]}#{sid}"
Expand All @@ -47,6 +52,7 @@ def voter?
# acts_as_voteable
# end
def acts_as_voteable
RedisVoteable.redis ||= Redis.new(RedisVoteable.redis_voteable_settings)
include Voteable
end

Expand All @@ -57,6 +63,7 @@ def acts_as_voteable
# acts_as_voter
# end
def acts_as_voter
RedisVoteable.redis ||= Redis.new(RedisVoteable.redis_voteable_settings)
include Voter
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/redis_voteable/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module RedisVoteable
VERSION = "0.1.1"
VERSION = "0.1.2"
end
41 changes: 39 additions & 2 deletions lib/redis_voteable/voteable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ def down_percentage
nil
end

# Returns true if the voter voted on the +voteable+.
def voted?(voter)
up_voted?(voter) || down_voted?(voter)
end

# Returns :up, :down, or nil.
def vote_value?(voter)
return :up if up_voted?(voter)
return :down if down_voted?(voter)
return nil
end

# Returns true if the voter up voted the +voteable+.
def up_voted?(voter)
redis.sismember prefixed("#{class_key(voter)}:#{UP_VOTES}"), "#{class_key(self)}"
end

# Returns true if the voter down voted the +voteable+.
def down_voted?(voter)
redis.sismember prefixed("#{class_key(voter)}:#{DOWN_VOTES}"), "#{class_key(self)}"
end


# Returns an array of objects that are +voter+s that voted on this
# +voteable+. This method can be very slow, as it constructs each
# object. Also, it assumes that each object has a +find(id)+ method
Expand All @@ -59,15 +82,29 @@ def up_voters
voters = redis.smembers prefixed("#{class_key(self)}:#{UP_VOTERS}")
voters.map do |voter|
tmp = voter.split(':')
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
klass = tmp[0, tmp.length-1].join(':').constantize
if klass.respond_to?('find')
klass.find(tmp.last)
elsif klass.respond_to?('get')
klass.get(tmp.last)
else
nil
end
end
end

def down_voters
voters = redis.smembers prefixed("#{class_key(self)}:#{DOWN_VOTERS}")
voters.map do |voter|
tmp = voter.split(':')
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
klass = tmp[0, tmp.length-1].join(':').constantize
if klass.respond_to?('find')
klass.find(tmp.last)
elsif klass.respond_to?('get')
klass.get(tmp.last)
else
nil
end
end
end

Expand Down
42 changes: 29 additions & 13 deletions lib/redis_voteable/voter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ def clear_vote(voteable)
raise Exceptions::NotVotedError unless r[0] == 1 || r[2] == 1
true
end

alias :unvote :clear_vote

# Clears an already done vote on a +voteable+, but doesn't raise an error if
# the voteable was not voted. It ignores the unvote then.
def clear_vote!(voteable)
Expand All @@ -111,6 +112,7 @@ def clear_vote!(voteable)
return false
end
end
alias :unvote! :clear_vote!

# Return the total number of votes a voter has cast.
def total_votes()
Expand Down Expand Up @@ -153,23 +155,37 @@ def down_voted?(voteable)
# +voteable+. This method can be very slow, as it constructs each
# object. Also, it assumes that each object has a +find(id)+ method
# defined (e.g., any ActiveRecord object).
def votings
up_votings | down_votings
def voteables
up_voteables | down_voteables
end

def up_votings
votings = redis.smembers prefixed("#{class_key(self)}:#{UP_VOTES}")
votings.map do |voting|
tmp = voting.split(':')
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
def up_voteables
voteables = redis.smembers prefixed("#{class_key(self)}:#{UP_VOTES}")
voteables.map do |voteable|
tmp = voteable.split(':')
klass = tmp[0, tmp.length-1].join(':').constantize
if klass.respond_to?('find')
klass.find(tmp.last)
elsif klass.respond_to?('get')
klass.get(tmp.last)
else
nil
end
end
end

def down_votings
votings = redis.smembers prefixed("#{class_key(self)}:#{DOWN_VOTES}")
votings.map do |voting|
tmp = voting.split(':')
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
def down_voteables
voteables = redis.smembers prefixed("#{class_key(self)}:#{DOWN_VOTES}")
voteables.map do |voteable|
tmp = voteable.split(':')
klass = tmp[0, tmp.length-1].join(':').constantize
if klass.respond_to?('find')
klass.find(tmp.last)
elsif klass.respond_to?('get')
klass.get(tmp.last)
else
nil
end
end
end

Expand Down
25 changes: 23 additions & 2 deletions spec/lib/redis_voteable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
@voter.up_votes == 0
@voter.up_vote(@voteable)
@voter.up_votes == 1
@voter.votings[0].should == @voteable
@voter.voteables[0].should == @voteable
end

it "voteable should have down vote votings" do
Expand All @@ -55,7 +55,7 @@
@voter.down_votes.should == 0
@voter.down_vote(@voteable)
@voter.down_votes.should == 1
@voter.votings[0].should == @voteable
@voter.voteables[0].should == @voteable
end

it "voteable should calculate correct percentages" do
Expand Down Expand Up @@ -149,6 +149,12 @@
@voter.up_voted?(@voteable).should be_true
@voter.down_voted?(@voteable).should be_false
end

it "should have up votings" do
@voter.up_vote(@voteable)
@voter.voteables[0].up_voted?(@voter).should be_true
@voter.voteables[0].down_voted?(@voter).should be_false
end
end

describe "down vote" do
Expand Down Expand Up @@ -216,6 +222,12 @@
@voter.up_voted?(@voteable).should be_false
@voter.down_voted?(@voteable).should be_true
end

it "should have down votings" do
@voter.down_vote(@voteable)
@voter.voteables[0].up_voted?(@voter).should be_false
@voter.voteables[0].down_voted?(@voter).should be_true
end
end

describe "clear_vote" do
Expand All @@ -227,6 +239,15 @@
@voteable.up_votes.should == 0
@voter.up_votes.should == 0
end

it "should have working aliases" do
@voter.up_vote(@voteable)
@voteable.up_votes.should == 1
@voter.up_votes.should == 1
@voter.unvote(@voteable)
@voteable.up_votes.should == 0
@voter.up_votes.should == 0
end

it "should raise an error if voter didn't vote for the voteable" do
lambda { @voter.clear_vote(@voteable) }.should raise_error(RedisVoteable::Exceptions::NotVotedError)
Expand Down
4 changes: 2 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

config.after(:each) do
DatabaseCleaner.clean
redis = Redis.new
redis.flushdb
# redis = Redis.new
# redis.flushdb
end
end

0 comments on commit 9571d76

Please sign in to comment.