redis_voteable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +19 -0
- data/Rakefile +5 -0
- data/lib/redis_voteable.rb +63 -0
- data/lib/redis_voteable/exceptions.rb +29 -0
- data/lib/redis_voteable/version.rb +3 -0
- data/lib/redis_voteable/voteable.rb +101 -0
- data/lib/redis_voteable/voter.rb +182 -0
- data/redis_voteable.gemspec +31 -0
- data/redis_voteable.sqlite3 +0 -0
- data/spec/database.yml +3 -0
- data/spec/lib/redis_voteable_spec.rb +220 -0
- data/spec/models.rb +13 -0
- data/spec/schema.rb +14 -0
- data/spec/spec_helper.rb +40 -0
- metadata +143 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Chris Brauchli
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_support/dependencies'
|
2
|
+
require 'redis'
|
3
|
+
require 'redis_voteable/version'
|
4
|
+
require 'redis_voteable/voteable'
|
5
|
+
require 'redis_voteable/voter'
|
6
|
+
require 'redis_voteable/exceptions'
|
7
|
+
|
8
|
+
module RedisVoteable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
UP_VOTERS = "up_voters"
|
12
|
+
DOWN_VOTERS = "dn_voters"
|
13
|
+
UP_VOTES = "up_votes"
|
14
|
+
DOWN_VOTES = "dn_votes"
|
15
|
+
|
16
|
+
mattr_accessor :redis_voteable_settings
|
17
|
+
@@redis_voteable_settings = {
|
18
|
+
:host => 'localhost',
|
19
|
+
:port => '6379',
|
20
|
+
:db => 0,
|
21
|
+
:key_prefix => "vote:",
|
22
|
+
}
|
23
|
+
mattr_accessor :redis
|
24
|
+
@@redis = Redis.new(@@redis_voteable_settings)
|
25
|
+
|
26
|
+
def prefixed(sid)
|
27
|
+
"#{@@redis_voteable_settings[:key_prefix]}#{sid}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def class_key(v)
|
31
|
+
"#{v.class.name}:#{v.id}"
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def voteable?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
def voter?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
# Specify a model as voteable.
|
44
|
+
#
|
45
|
+
# Example:
|
46
|
+
# class Question < ActiveRecord::Base
|
47
|
+
# acts_as_voteable
|
48
|
+
# end
|
49
|
+
def acts_as_voteable
|
50
|
+
include Voteable
|
51
|
+
end
|
52
|
+
|
53
|
+
# Specify a model as voter.
|
54
|
+
#
|
55
|
+
# Example:
|
56
|
+
# class User < ActiveRecord::Base
|
57
|
+
# acts_as_voter
|
58
|
+
# end
|
59
|
+
def acts_as_voter
|
60
|
+
include Voter
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RedisVoteable
|
2
|
+
module Exceptions
|
3
|
+
class AlreadyVotedError < StandardError
|
4
|
+
attr_reader :up_vote
|
5
|
+
|
6
|
+
def initialize(up_vote)
|
7
|
+
vote = if up_vote
|
8
|
+
"up voted"
|
9
|
+
else
|
10
|
+
"down voted"
|
11
|
+
end
|
12
|
+
|
13
|
+
super "The voteable was already #{vote} by the voter."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class NotVotedError < StandardError
|
18
|
+
def initialize
|
19
|
+
super "The voteable was not voted by the voter."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class InvalidVoteableError < StandardError
|
24
|
+
def initialize
|
25
|
+
super "Invalid voteable."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module RedisVoteable
|
2
|
+
module Voteable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def voteable?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
# private
|
15
|
+
# def build_voter(voter)
|
16
|
+
# tmp = voter.split(':')
|
17
|
+
# tmp[0, tmp.length - 1].constantize.find(tmp.last)
|
18
|
+
# end
|
19
|
+
end
|
20
|
+
|
21
|
+
module InstanceMethods
|
22
|
+
def up_votes
|
23
|
+
redis.scard prefixed("#{class_key(self)}:#{UP_VOTERS}")
|
24
|
+
end
|
25
|
+
|
26
|
+
def down_votes
|
27
|
+
redis.scard prefixed("#{class_key(self)}:#{DOWN_VOTERS}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def total_votes
|
31
|
+
up_votes + down_votes
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the difference between up and and votes.
|
35
|
+
# May be negative if there are more down than up votes.
|
36
|
+
def tally
|
37
|
+
up_votes - down_votes
|
38
|
+
end
|
39
|
+
|
40
|
+
def up_percentage
|
41
|
+
return (up_votes.to_f * 100 / total_votes) unless votes == 0
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def down_percentage
|
46
|
+
return (down_votes.to_f * 100 / total_votes) unless votes == 0
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns an array of objects that are +voter+s that voted on this
|
51
|
+
# +voteable+. This method can be very slow, as it constructs each
|
52
|
+
# object. Also, it assumes that each object has a +find(id)+ method
|
53
|
+
# defined (e.g., any ActiveRecord object).
|
54
|
+
def voters
|
55
|
+
up_voters | down_voters
|
56
|
+
end
|
57
|
+
|
58
|
+
def up_voters
|
59
|
+
voters = redis.smembers prefixed("#{class_key(self)}:#{UP_VOTERS}")
|
60
|
+
voters.map do |voter|
|
61
|
+
tmp = voter.split(':')
|
62
|
+
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def down_voters
|
67
|
+
voters = redis.smembers prefixed("#{class_key(self)}:#{DOWN_VOTERS}")
|
68
|
+
voters.map do |voter|
|
69
|
+
tmp = voter.split(':')
|
70
|
+
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Calculates the (lower) bound of the Wilson confidence interval
|
75
|
+
# See: http://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval
|
76
|
+
# and: http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
|
77
|
+
def confidence(bound = :lower)
|
78
|
+
#include Math
|
79
|
+
n = up_votes + down_votes
|
80
|
+
if n == 0
|
81
|
+
return 0 if n == 0
|
82
|
+
end
|
83
|
+
z = 1.4395314800662002 # Determines confidence to estimate.
|
84
|
+
# 1.0364333771448913 = 70%
|
85
|
+
# 1.2815515594600038 = 80%
|
86
|
+
# 1.4395314800662002 = 85%
|
87
|
+
# 1.644853646608357 = 90%
|
88
|
+
# 1.9599639715843482 = 95%
|
89
|
+
# 2.2414027073522136 = 97.5%
|
90
|
+
p_hat = 1.0*up_votes/n
|
91
|
+
left = p_hat + z*z/(2*n)
|
92
|
+
right = z * Math.sqrt( (p_hat*(1-p_hat) + z*z/(4*n)) / n )
|
93
|
+
under = 1 + z*z/n
|
94
|
+
return (left - right) / under unless bound == :upper
|
95
|
+
return (left + right) / under
|
96
|
+
#return Math.sqrt( p_hat + z * z / ( 2 * n ) - z * ( ( p_hat * ( 1 - p_hat ) + z * z / ( 4 * n ) ) / n ) ) / ( 1 + z * z / n )
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module RedisVoteable
|
2
|
+
module Voter
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
# TODO: add scope
|
7
|
+
# scope :up_voted_for, lambda { |votee| where(:_id => { '$in' => votee.up_voter_ids }) }
|
8
|
+
# scope :down_voted_for, lambda { |votee| where(:_id => { '$in' => votee.down_voter_ids }) }
|
9
|
+
# scope :voted_for, lambda { |votee| where(:_id => { '$in' => votee.voter_ids }) }
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def voter?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
def vote(voteable, direction)
|
20
|
+
if direction == :up
|
21
|
+
up_vote(voteable)
|
22
|
+
elsif direction == :down
|
23
|
+
down_vote(voteable)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def vote!(voteable, direction)
|
28
|
+
if direction == :up
|
29
|
+
up_vote!(voteable)
|
30
|
+
elsif direction == :down
|
31
|
+
down_vote!(voteable)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Up vote a +voteable+.
|
36
|
+
# Raises an AlreadyVotedError if the voter already up voted the voteable.
|
37
|
+
# Changes a down vote to an up vote if the the voter already down voted the voteable.
|
38
|
+
def up_vote(voteable)
|
39
|
+
check_voteable(voteable)
|
40
|
+
|
41
|
+
r = redis.multi do
|
42
|
+
redis.srem prefixed("#{class_key(voteable)}:#{DOWN_VOTERS}"), "#{class_key(self)}"
|
43
|
+
redis.srem prefixed("#{class_key(self)}:#{DOWN_VOTES}"), "#{class_key(voteable)}"
|
44
|
+
redis.sadd prefixed("#{class_key(voteable)}:#{UP_VOTERS}"), "#{class_key(self)}"
|
45
|
+
redis.sadd prefixed("#{class_key(self)}:#{UP_VOTES}"), "#{class_key(voteable)}"
|
46
|
+
end
|
47
|
+
raise Exceptions::AlreadyVotedError.new(true) unless r[2] == 1
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Up votes the +voteable+, but doesn't raise an error if the votelable was already up voted.
|
52
|
+
# The vote is simply ignored then.
|
53
|
+
def up_vote!(voteable)
|
54
|
+
begin
|
55
|
+
up_vote(voteable)
|
56
|
+
return true
|
57
|
+
rescue
|
58
|
+
return false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Down vote a +voteable+.
|
63
|
+
# Raises an AlreadyVotedError if the voter already down voted the voteable.
|
64
|
+
# Changes an up vote to a down vote if the the voter already up voted the voteable.
|
65
|
+
def down_vote(voteable)
|
66
|
+
check_voteable(voteable)
|
67
|
+
|
68
|
+
r = redis.multi do
|
69
|
+
redis.srem prefixed("#{class_key(voteable)}:#{UP_VOTERS}"), "#{class_key(self)}"
|
70
|
+
redis.srem prefixed("#{class_key(self)}:#{UP_VOTES}"), "#{class_key(voteable)}"
|
71
|
+
redis.sadd prefixed("#{class_key(voteable)}:#{DOWN_VOTERS}"), "#{class_key(self)}"
|
72
|
+
redis.sadd prefixed("#{class_key(self)}:#{DOWN_VOTES}"), "#{class_key(voteable)}"
|
73
|
+
end
|
74
|
+
raise Exceptions::AlreadyVotedError.new(false) unless r[2] == 1
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
# Down votes the +voteable+, but doesn't raise an error if the votelable was already down voted.
|
79
|
+
# The vote is simply ignored then.
|
80
|
+
def down_vote!(voteable)
|
81
|
+
begin
|
82
|
+
down_vote(voteable)
|
83
|
+
return true
|
84
|
+
rescue
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Clears an already done vote on a +voteable+.
|
90
|
+
# Raises a NotVotedError if the voter didn't voted for the voteable.
|
91
|
+
def clear_vote(voteable)
|
92
|
+
check_voteable(voteable)
|
93
|
+
|
94
|
+
r = redis.multi do
|
95
|
+
redis.srem prefixed("#{class_key(voteable)}:#{DOWN_VOTERS}"), "#{class_key(self)}"
|
96
|
+
redis.srem prefixed("#{class_key(self)}:#{DOWN_VOTES}"), "#{class_key(voteable)}"
|
97
|
+
redis.srem prefixed("#{class_key(voteable)}:#{UP_VOTERS}"), "#{class_key(self)}"
|
98
|
+
redis.srem prefixed("#{class_key(self)}:#{UP_VOTES}"), "#{class_key(voteable)}"
|
99
|
+
end
|
100
|
+
raise Exceptions::NotVotedError unless r[0] == 1 || r[2] == 1
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
# Clears an already done vote on a +voteable+, but doesn't raise an error if
|
105
|
+
# the voteable was not voted. It ignores the unvote then.
|
106
|
+
def clear_vote!(voteable)
|
107
|
+
begin
|
108
|
+
clear_vote(voteable)
|
109
|
+
return true
|
110
|
+
rescue
|
111
|
+
return false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Return the total number of votes a voter has cast.
|
116
|
+
def vote_count()
|
117
|
+
up_vote_count + down_vote_count
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns the number of upvotes a voter has cast.
|
121
|
+
def up_vote_count()
|
122
|
+
redis.scard prefixed("#{class_key(self)}:#{UP_VOTES}")
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns the number of downvotes a voter has cast.
|
126
|
+
def down_vote_count()
|
127
|
+
redis.scard prefixed("#{class_key(self)}:#{DOWN_VOTES}")
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns true if the voter voted for the +voteable+.
|
131
|
+
def voted?(voteable)
|
132
|
+
up_voted?(voteable) || down_voted?(voteable)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns :up, :down, or nil.
|
136
|
+
def vote_value?(voteable)
|
137
|
+
return :up if up_voted?(voteable)
|
138
|
+
return :down if down_voted?(voteable)
|
139
|
+
return nil
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns true if the voter up voted the +voteable+.
|
143
|
+
def up_voted?(voteable)
|
144
|
+
redis.sismember prefixed("#{class_key(voteable)}:#{UP_VOTERS}"), "#{class_key(self)}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns true if the voter down voted the +voteable+.
|
148
|
+
def down_voted?(voteable)
|
149
|
+
redis.sismember prefixed("#{class_key(voteable)}:#{DOWN_VOTERS}"), "#{class_key(self)}"
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns an array of objects that are +voter+s that voted on this
|
153
|
+
# +voteable+. This method can be very slow, as it constructs each
|
154
|
+
# object. Also, it assumes that each object has a +find(id)+ method
|
155
|
+
# defined (e.g., any ActiveRecord object).
|
156
|
+
def votings
|
157
|
+
up_votings | down_votings
|
158
|
+
end
|
159
|
+
|
160
|
+
def up_votings
|
161
|
+
votings = redis.smembers prefixed("#{class_key(self)}:#{UP_VOTES}")
|
162
|
+
votings.map do |voting|
|
163
|
+
tmp = voting.split(':')
|
164
|
+
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def down_votings
|
169
|
+
votings = redis.smembers prefixed("#{class_key(self)}:#{DOWN_VOTES}")
|
170
|
+
votings.map do |voting|
|
171
|
+
tmp = voting.split(':')
|
172
|
+
tmp[0, tmp.length-1].join(':').constantize.find(tmp.last)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
def check_voteable(voteable)
|
178
|
+
raise Exceptions::InvalidVoteableError unless voteable.class.respond_to?("voteable?") && voteable.class.voteable?
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "redis_voteable/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "redis_voteable"
|
7
|
+
s.version = RedisVoteable::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Chris Brauchli"]
|
10
|
+
s.email = ["cbrauchli@gmail.com"]
|
11
|
+
s.date = "2011-09-30"
|
12
|
+
s.homepage = "http://github.com/cbrauchli/redis_voteable"
|
13
|
+
s.summary = %q{Simple vote management with Redis used as the backend.}
|
14
|
+
s.description = %q{A Redis-backed voting extension for Rails applications. }
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# Dependencies
|
22
|
+
s.add_dependency "redis", "~> 2.2.0"
|
23
|
+
s.add_dependency "activesupport"
|
24
|
+
|
25
|
+
# For development only
|
26
|
+
s.add_development_dependency "activerecord", "~> 3.0.0"
|
27
|
+
s.add_development_dependency "sqlite3-ruby", "~> 1.3.0"
|
28
|
+
s.add_development_dependency "bundler", "~> 1.0.0"
|
29
|
+
s.add_development_dependency "rspec", "~> 2.0.0"
|
30
|
+
s.add_development_dependency "database_cleaner", "~> 0.6.7"
|
31
|
+
end
|
Binary file
|
data/spec/database.yml
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# spec/lib/redis_voteable_spec.rb
|
2
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
3
|
+
|
4
|
+
describe "Redis Voteable" do
|
5
|
+
before(:each) do
|
6
|
+
@voteable = VoteableModel.create(:name => "Votable 1")
|
7
|
+
@voter = VoterModel.create(:name => "Voter 1")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should create a voteable instance" do
|
11
|
+
@voteable.class.should == VoteableModel
|
12
|
+
@voteable.class.voteable?.should == true
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should create a voter instance" do
|
16
|
+
@voter.class.should == VoterModel
|
17
|
+
@voter.class.voter?.should == true
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should get correct vote summary" do
|
21
|
+
@voter.up_vote(@voteable).should == true
|
22
|
+
@voteable.total_votes.should == 1
|
23
|
+
@voteable.tally.should == 1
|
24
|
+
@voter.down_vote(@voteable).should == true
|
25
|
+
@voteable.total_votes.should == 1
|
26
|
+
@voteable.tally.should == -1
|
27
|
+
@voter.clear_vote(@voteable).should == true
|
28
|
+
@voteable.total_votes.should == 0
|
29
|
+
@voteable.tally.should == 0
|
30
|
+
end
|
31
|
+
|
32
|
+
it "voteable should have up vote votings" do
|
33
|
+
@voteable.up_votes.should == 0
|
34
|
+
@voter.up_vote(@voteable)
|
35
|
+
@voteable.up_votes.should == 1
|
36
|
+
@voteable.voters[0].up_voted?(@voteable).should be_true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "voter should have up vote votings" do
|
40
|
+
@voter.up_vote_count == 0
|
41
|
+
@voter.up_vote(@voteable)
|
42
|
+
@voter.up_vote_count == 1
|
43
|
+
@voter.votings[0].should == @voteable
|
44
|
+
end
|
45
|
+
|
46
|
+
it "voteable should have down vote votings" do
|
47
|
+
@voteable.down_votes.should == 0
|
48
|
+
@voter.down_vote(@voteable)
|
49
|
+
@voteable.down_votes.should == 1
|
50
|
+
@voteable.voters[0].up_voted?(@voteable).should be_false
|
51
|
+
@voteable.voters[0].down_voted?(@voteable).should be_true
|
52
|
+
end
|
53
|
+
|
54
|
+
it "voter should have down vote votings" do
|
55
|
+
@voter.down_vote_count.should == 0
|
56
|
+
@voter.down_vote(@voteable)
|
57
|
+
@voter.down_vote_count.should == 1
|
58
|
+
@voter.votings[0].should == @voteable
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "up vote" do
|
62
|
+
it "should increase up votes of voteable by one" do
|
63
|
+
@voteable.up_votes.should == 0
|
64
|
+
@voter.up_vote(@voteable)
|
65
|
+
@voteable.up_votes.should == 1
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should increase up votes of voter by one" do
|
69
|
+
@voter.up_vote_count.should == 0
|
70
|
+
@voter.up_vote(@voteable)
|
71
|
+
@voter.up_vote_count.should == 1
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should only allow a voter to up vote a voteable once" do
|
75
|
+
@voteable.up_votes.should == 0
|
76
|
+
@voter.up_vote(@voteable)
|
77
|
+
lambda { @voter.up_vote(@voteable) }.should raise_error(RedisVoteable::Exceptions::AlreadyVotedError)
|
78
|
+
@voteable.up_votes.should == 1
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should only allow a voter to up vote a voteable once without raising an error" do
|
82
|
+
@voteable.up_votes.should == 0
|
83
|
+
@voter.up_vote!(@voteable)
|
84
|
+
@voteable.up_votes.should == 1
|
85
|
+
lambda {
|
86
|
+
@voter.up_vote!(@voteable).should == false
|
87
|
+
}.should_not raise_error(RedisVoteable::Exceptions::AlreadyVotedError)
|
88
|
+
@voteable.total_votes.should == 1
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should change a down vote to an up vote" do
|
92
|
+
@voter.down_vote(@voteable)
|
93
|
+
@voteable.up_votes.should == 0
|
94
|
+
@voteable.down_votes.should == 1
|
95
|
+
@voteable.tally.should == -1
|
96
|
+
@voter.up_vote_count.should == 0
|
97
|
+
@voter.down_vote_count.should == 1
|
98
|
+
|
99
|
+
@voter.up_vote(@voteable)
|
100
|
+
@voteable.up_votes.should == 1
|
101
|
+
@voteable.down_votes.should == 0
|
102
|
+
@voteable.tally.should == 1
|
103
|
+
@voter.up_vote_count.should == 1
|
104
|
+
@voter.down_vote_count.should == 0
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should allow up votes from different voters" do
|
108
|
+
@voter2 = VoterModel.create(:name => "Voter 2")
|
109
|
+
@voter.up_vote(@voteable)
|
110
|
+
@voter2.up_vote(@voteable)
|
111
|
+
@voteable.up_votes.should == 2
|
112
|
+
@voteable.tally.should == 2
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should raise an error for an invalid voteable" do
|
116
|
+
invalid_voteable = InvalidVoteableModel.create
|
117
|
+
lambda { @voter.up_vote(invalid_voteable) }.should raise_error(RedisVoteable::Exceptions::InvalidVoteableError)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should check if voter up voted voteable" do
|
121
|
+
@voter.up_vote(@voteable)
|
122
|
+
@voter.voted?(@voteable).should be_true
|
123
|
+
@voter.up_voted?(@voteable).should be_true
|
124
|
+
@voter.down_voted?(@voteable).should be_false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "down vote" do
|
129
|
+
it "should decrease down votes of voteable by one" do
|
130
|
+
@voteable.down_votes.should == 0
|
131
|
+
@voter.down_vote(@voteable)
|
132
|
+
@voteable.down_votes.should == 1
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should decrease down votes of voter by one" do
|
136
|
+
@voter.down_vote_count.should == 0
|
137
|
+
@voter.down_vote(@voteable)
|
138
|
+
@voter.down_vote_count.should == 1
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should only allow a voter to down vote a voteable once" do
|
142
|
+
@voteable.down_votes.should == 0
|
143
|
+
@voter.down_vote(@voteable)
|
144
|
+
lambda { @voter.down_vote(@voteable) }.should raise_error(RedisVoteable::Exceptions::AlreadyVotedError)
|
145
|
+
@voteable.down_votes.should == 1
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should only allow a voter to down vote a voteable once without raising an error" do
|
149
|
+
@voteable.down_votes.should == 0
|
150
|
+
@voter.down_vote!(@voteable)
|
151
|
+
@voteable.down_votes.should == 1
|
152
|
+
lambda {
|
153
|
+
@voter.down_vote!(@voteable).should == false
|
154
|
+
}.should_not raise_error(RedisVoteable::Exceptions::AlreadyVotedError)
|
155
|
+
@voteable.total_votes.should == 1
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should change an up vote to a down vote" do
|
159
|
+
@voter.up_vote(@voteable)
|
160
|
+
@voteable.up_votes.should == 1
|
161
|
+
@voteable.down_votes.should == 0
|
162
|
+
@voteable.tally.should == 1
|
163
|
+
@voter.up_vote_count.should == 1
|
164
|
+
@voter.down_vote_count.should == 0
|
165
|
+
|
166
|
+
@voter.down_vote(@voteable)
|
167
|
+
@voteable.up_votes.should == 0
|
168
|
+
@voteable.down_votes.should == 1
|
169
|
+
@voteable.tally.should == -1
|
170
|
+
@voter.up_vote_count.should == 0
|
171
|
+
@voter.down_vote_count.should == 1
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should allow down votes from different voters" do
|
175
|
+
@voter2 = VoterModel.create(:name => "Voter 2")
|
176
|
+
@voter.down_vote(@voteable)
|
177
|
+
@voter2.down_vote(@voteable)
|
178
|
+
@voteable.down_votes.should == 2
|
179
|
+
@voteable.tally.should == -2
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should raise an error for an invalid voteable" do
|
183
|
+
invalid_voteable = InvalidVoteableModel.create
|
184
|
+
lambda { @voter.down_vote(invalid_voteable) }.should raise_error(RedisVoteable::Exceptions::InvalidVoteableError)
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should check if voter down voted voteable" do
|
188
|
+
@voter.down_vote(@voteable)
|
189
|
+
@voter.voted?(@voteable).should be_true
|
190
|
+
@voter.up_voted?(@voteable).should be_false
|
191
|
+
@voter.down_voted?(@voteable).should be_true
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "clear_vote" do
|
196
|
+
it "should decrease the up votes if up voted before" do
|
197
|
+
@voter.up_vote(@voteable)
|
198
|
+
@voteable.up_votes.should == 1
|
199
|
+
@voter.up_vote_count.should == 1
|
200
|
+
@voter.clear_vote(@voteable)
|
201
|
+
@voteable.up_votes.should == 0
|
202
|
+
@voter.up_vote_count.should == 0
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should raise an error if voter didn't vote for the voteable" do
|
206
|
+
lambda { @voter.clear_vote(@voteable) }.should raise_error(RedisVoteable::Exceptions::NotVotedError)
|
207
|
+
end
|
208
|
+
|
209
|
+
it "should not raise error if voter didn't vote for the voteable and clear_vote! is called" do
|
210
|
+
lambda {
|
211
|
+
@voter.clear_vote!(@voteable).should == false
|
212
|
+
}.should_not raise_error(RedisVoteable::Exceptions::NotVotedError)
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should raise an error for an invalid voteable" do
|
216
|
+
invalid_voteable = InvalidVoteableModel.create
|
217
|
+
lambda { @voter.clear_vote(invalid_voteable) }.should raise_error(RedisVoteable::Exceptions::InvalidVoteableError)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# spec/models.rb
|
2
|
+
class VoteableModel < ActiveRecord::Base
|
3
|
+
include RedisVoteable
|
4
|
+
acts_as_voteable
|
5
|
+
end
|
6
|
+
|
7
|
+
class VoterModel < ActiveRecord::Base
|
8
|
+
include RedisVoteable
|
9
|
+
acts_as_voter
|
10
|
+
end
|
11
|
+
|
12
|
+
class InvalidVoteableModel < ActiveRecord::Base
|
13
|
+
end
|
data/spec/schema.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# spec/schema.rb
|
2
|
+
ActiveRecord::Schema.define :version => 0 do
|
3
|
+
create_table :voteable_models, :force => true do |t|
|
4
|
+
t.string :name
|
5
|
+
end
|
6
|
+
|
7
|
+
create_table :voter_models, :force => true do |t|
|
8
|
+
t.string :name
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table :invalid_voteable_models, :force => true do |t|
|
12
|
+
t.string :name
|
13
|
+
end
|
14
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# spec/spec_helper.rb
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler'
|
4
|
+
require 'logger'
|
5
|
+
require 'rspec'
|
6
|
+
require 'active_record'
|
7
|
+
require 'database_cleaner'
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
|
10
|
+
require 'redis_voteable'
|
11
|
+
|
12
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
|
13
|
+
ActiveRecord::Base.configurations = YAML::load_file(File.dirname(__FILE__) + '/database.yml')
|
14
|
+
ActiveRecord::Base.establish_connection(ENV['DB'] || 'sqlite3')
|
15
|
+
|
16
|
+
ActiveRecord::Base.silence do
|
17
|
+
ActiveRecord::Migration.verbose = false
|
18
|
+
|
19
|
+
load(File.dirname(__FILE__) + '/schema.rb')
|
20
|
+
load(File.dirname(__FILE__) + '/models.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec.configure do |config|
|
24
|
+
config.filter_run :focus => true
|
25
|
+
config.run_all_when_everything_filtered = true
|
26
|
+
config.filter_run_excluding :exclude => true
|
27
|
+
|
28
|
+
config.mock_with :rspec
|
29
|
+
|
30
|
+
config.before(:suite) do
|
31
|
+
DatabaseCleaner.strategy = :truncation
|
32
|
+
DatabaseCleaner.clean
|
33
|
+
end
|
34
|
+
|
35
|
+
config.after(:each) do
|
36
|
+
DatabaseCleaner.clean
|
37
|
+
#redis = Redis.new
|
38
|
+
#redis.flushdb
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis_voteable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Brauchli
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-30 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &70161134771100 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70161134771100
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activesupport
|
27
|
+
requirement: &70161134770360 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70161134770360
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: activerecord
|
38
|
+
requirement: &70161134769280 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 3.0.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70161134769280
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sqlite3-ruby
|
49
|
+
requirement: &70161134768500 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.3.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70161134768500
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: bundler
|
60
|
+
requirement: &70161134763980 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.0.0
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70161134763980
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: &70161134763320 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 2.0.0
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70161134763320
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: database_cleaner
|
82
|
+
requirement: &70161134762000 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 0.6.7
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70161134762000
|
91
|
+
description: ! 'A Redis-backed voting extension for Rails applications. '
|
92
|
+
email:
|
93
|
+
- cbrauchli@gmail.com
|
94
|
+
executables: []
|
95
|
+
extensions: []
|
96
|
+
extra_rdoc_files: []
|
97
|
+
files:
|
98
|
+
- .gitignore
|
99
|
+
- Gemfile
|
100
|
+
- MIT-LICENSE
|
101
|
+
- Rakefile
|
102
|
+
- lib/redis_voteable.rb
|
103
|
+
- lib/redis_voteable/exceptions.rb
|
104
|
+
- lib/redis_voteable/version.rb
|
105
|
+
- lib/redis_voteable/voteable.rb
|
106
|
+
- lib/redis_voteable/voter.rb
|
107
|
+
- redis_voteable.gemspec
|
108
|
+
- redis_voteable.sqlite3
|
109
|
+
- spec/database.yml
|
110
|
+
- spec/lib/redis_voteable_spec.rb
|
111
|
+
- spec/models.rb
|
112
|
+
- spec/schema.rb
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
homepage: http://github.com/cbrauchli/redis_voteable
|
115
|
+
licenses: []
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 1.8.11
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: Simple vote management with Redis used as the backend.
|
138
|
+
test_files:
|
139
|
+
- spec/database.yml
|
140
|
+
- spec/lib/redis_voteable_spec.rb
|
141
|
+
- spec/models.rb
|
142
|
+
- spec/schema.rb
|
143
|
+
- spec/spec_helper.rb
|