forked from bouchard/thumbs_up
/
acts_as_voteable.rb
169 lines (139 loc) · 7.36 KB
/
acts_as_voteable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
module ThumbsUp
module ActsAsVoteable #:nodoc:
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_voteable
has_many :votes, :as => :voteable, :dependent => :destroy
include ThumbsUp::ActsAsVoteable::InstanceMethods
extend ThumbsUp::ActsAsVoteable::SingletonMethods
end
end
module SingletonMethods
# The point of this function is to return rankings based on the difference between up and down votes
# assuming equal weighting (i.e. a user with 1 up vote and 1 down vote has a Vote_Total of 0.
# First the votes table is joined twiced so that the Vote_Total can be calculated for every ID
# Then this table is joined against the specific table passed to this function to allow for
# ranking of the items within that table based on the difference between up and down votes.
# 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
# :ascending - Default false - normal order DESC (i.e. highest rank to lowest)
# :at_least - Default 1 - Item must have at least X votes
# :at_most - Item may not have more than X votes
def plusminus_tally(*args)
options = args.extract_options!
tsub0 = Vote
tsub0 = tsub0.where("vote = ?", false)
tsub0 = tsub0.where("voteable_type = ?", self.name)
tsub0 = tsub0.group("voteable_id")
tsub0 = tsub0.select("DISTINCT voteable_id, COUNT(vote) as votes_against")
tsub1 = Vote
tsub1 = tsub1.where("vote = ?", true)
tsub1 = tsub1.where("voteable_type = ?", self.name)
tsub1 = tsub1.group("voteable_id")
tsub1 = tsub1.select("DISTINCT voteable_id, COUNT(vote) as votes_for")
t = self.joins("LEFT OUTER JOIN (SELECT DISTINCT #{Vote.table_name}.*,
(COALESCE(vfor.votes_for, 0)-COALESCE(against.votes_against, 0)) AS vote_total
FROM (#{Vote.table_name} LEFT JOIN
(#{tsub0.to_sql}) AS against ON #{Vote.table_name}.voteable_id = against.voteable_id)
LEFT JOIN
(#{tsub1.to_sql}) as vfor ON #{Vote.table_name}.voteable_id = vfor.voteable_id)
AS joined_#{Vote.table_name} ON #{self.table_name}.#{self.primary_key} =
joined_#{Vote.table_name}.voteable_id")
t = t.where("joined_#{Vote.table_name}.voteable_type = '#{self.name}'")
t = t.group("joined_#{Vote.table_name}.voteable_id, joined_#{Vote.table_name}.vote_total, #{column_names_for_tally}")
t = t.where("joined_#{Vote.table_name}.created_at >= ?", options[:start_at]) if options[:start_at]
t = t.where("joined_#{Vote.table_name}.created_at <= ?", options[:end_at]) if options[:end_at]
t = options[:ascending] ? t.order("joined_#{Vote.table_name}.vote_total") : t.order("joined_#{Vote.table_name}.vote_total DESC")
t = t.having([
"COUNT(joined_#{Vote.table_name}.voteable_id) > 0",
(options[:at_least] ?
"joined_#{Vote.table_name}.vote_total >= #{sanitize(options[:at_least])}" : nil
),
(options[:at_most] ?
"joined_#{Vote.table_name}.vote_total <= #{sanitize(options[:at_most])}" : nil
)
].compact.join(' AND '))
t.select("#{self.table_name}.*, joined_#{Vote.table_name}.vote_total")
end
# #rank_tally is depreciated.
alias_method :rank_tally, :plusminus_tally
# Calculate the vote counts for all voteables of my type.
# This method returns all voteables with at least one vote.
# The vote count for each voteable is available as #vote_count.
#
# 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 'vote_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 tally(*args)
options = args.extract_options!
# Use the explicit SQL statement throughout for Postgresql compatibility.
vote_count = "COUNT(#{Vote.table_name}.voteable_id)"
t = self.where("#{Vote.table_name}.voteable_type = '#{self.name}'")
# We join so that you can order by columns on the voteable model.
t = t.joins("LEFT OUTER JOIN #{Vote.table_name} ON #{self.table_name}.#{self.primary_key} = #{Vote.table_name}.voteable_id")
t = t.group("#{Vote.table_name}.voteable_id, #{column_names_for_tally}")
t = t.limit(options[:limit]) if options[:limit]
t = t.where("#{Vote.table_name}.created_at >= ?", options[:start_at]) if options[:start_at]
t = t.where("#{Vote.table_name}.created_at <= ?", options[:end_at]) if options[:end_at]
t = t.where(options[:conditions]) if options[:conditions]
t = options[:order] ? t.order(options[:order]) : t.order("#{vote_count} DESC")
# I haven't been able to confirm this bug yet, but Arel (2.0.7) currently blows up
# with multiple 'having' clauses. So we hack them all into one for now.
# If you have a more elegant solution, a pull request on Github would be greatly appreciated.
t = t.having([
"#{vote_count} > 0",
(options[:at_least] ? "#{vote_count} >= #{sanitize(options[:at_least])}" : nil),
(options[:at_most] ? "#{vote_count} <= #{sanitize(options[:at_most])}" : nil)
].compact.join(' AND '))
# t = t.having("#{vote_count} > 0")
# t = t.having(["#{vote_count} >= ?", options[:at_least]]) if options[:at_least]
# t = t.having(["#{vote_count} <= ?", options[:at_most]]) if options[:at_most]
t.select("#{self.table_name}.*, COUNT(#{Vote.table_name}.voteable_id) AS vote_count")
end
def column_names_for_tally
column_names.map { |column| "#{self.table_name}.#{column}" }.join(', ')
end
end
module InstanceMethods
def votes_for
Vote.where(:voteable_id => id, :voteable_type => self.class.name, :vote => true).count
end
def votes_against
Vote.where(:voteable_id => id, :voteable_type => self.class.name, :vote => false).count
end
def percent_for
(votes_for.to_f * 100 / (self.votes.size + 0.0001)).round
end
def percent_against
(votes_against.to_f * 100 / (self.votes.size + 0.0001)).round
end
# You'll probably want to use this method to display how 'good' a particular voteable
# is, and/or sort based on it.
def plusminus
votes_for - votes_against
end
def votes_count
self.votes.size
end
def voters_who_voted
self.votes.map(&:voter).uniq
end
def voted_by?(voter)
0 < Vote.where(
:voteable_id => self.id,
:voteable_type => self.class.name,
:voter_type => voter.class.name,
:voter_id => voter.id
).count
end
end
end
end