diff --git a/.gitignore b/.gitignore
index 3ee9716..5ba2fe6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
-*.gemspec
-pkg/
\ No newline at end of file
+*.gem
+pkg/*
+Gemfile.lock
+coverage/
+test.sqlite3
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..1ade7d8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+language: ruby
+rvm:
+ - 2.6.3
+services:
+ - mysql
+ - postgresql
+before_install:
+ - "mysql -e 'create database thumbs_up_test;'"
+ - psql -c 'create database thumbs_up_test;' -U postgres
+ - bundle update
+ - gem update bundler
\ No newline at end of file
diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
deleted file mode 100644
index c09e380..0000000
--- a/CHANGELOG.markdown
+++ /dev/null
@@ -1,35 +0,0 @@
-2010-02-04
-==========
-* Remove vote.rb and votes_controller.rb from gem lib
-
-2009-02-11
-==========
-* Merge in xlash's bugfix for PostgreSQL and his has\_karma patch for multi-model support.
-
-2008-12-02
-==========
-* Merge in maddox's README typo fix and his ActiveSupport.Dependency patch
-* Merge in nagybence's updates that make the code usable as a Gem in addition to being a Rails plugin.
-* Thanks for the bugfixes and proofreading, nagybence and maddox!
-* Updated the gemplugin support to be compatible with maddox and nagybence's changes.
-* Added details on the MyQuotable reference application.
-
-2008-07-20
-==========
-* Protect against mass assignment misvotes using attr\_accessible
-* Update acts\_as mixins to use self.class.name instead of the deprecated self.type.name
-
-2008-07-15
-==========
-* Added examples directory
-* Changed this file to markdown format for GitHub goodness
-* Added a commented out unique index in the migration generator for "one person, one vote"
-* Removed votes\_controller.rb from lib/ and moved to examples
-
-2008-07-10
-==========
-
-* Added a generator class for the migration.
-* Implemented rails/init.rb
-* Implemented capability to use any model as the initiator of votes.
-* Implemented acts\_as\_voter methods.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a147679
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+2010-08-03
+==========
+* Renamed to ThumbsUp from vote\_fu.
+* Updated for Rails 3, using ActiveRecord/Arel.
+* Cleaned up some dead code, some shitty code, and made a few methods take up quite a lot less memory and time (voters\_who\_voted).
+* Removed example code.
+* Fixed karma.
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..817f62a
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,2 @@
+source 'http://rubygems.org'
+gemspec
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..58fa812
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Brady Bouchard (brady@thewellinspired.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/MIT-LICENSE b/MIT-LICENSE
deleted file mode 100644
index e8d49d5..0000000
--- a/MIT-LICENSE
+++ /dev/null
@@ -1,43 +0,0 @@
-Copyright (c) 2008 Peter Jackson (peteonrails.com)
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-Major portions of this package were adapted from ActsAsVoteable, which is subject to the same license. Here is the original copyright notice for ActsAsVoteable:
-
-Copyright (c) 2006 Cosmin Radoi
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.markdown b/README.markdown
deleted file mode 100644
index b69e846..0000000
--- a/README.markdown
+++ /dev/null
@@ -1,224 +0,0 @@
-vote_fu
-=======
-
-Allows an arbitrary number of entites (including Users) to vote on models.
-
-### Mixins
-This plugin introduces three mixins to your recipe book:
-
-1. **acts\_as\_voteable** : Intended for content objects like Posts, Comments, etc.
-2. **acts\_as\_voter** : Intended for voting entities, like Users.
-3. **has\_karma** : Intended for voting entities, or other objects that own the things you're voting on.
-
-### Inspiration
-
-This plugin started as an adaptation / update of act\_as\_voteable. It has grown different from that plugin in several ways:
-
-1. You can specify the model name that initiates votes.
-2. You can, with a little tuning, have more than one entity type vote on more than one model type.
-3. Adds "acts\_as\_voter" behavior to the initiator of votes.
-4. Introduces some newer Rails features like named\_scope and :polymorphic keywords
-5. Adds "has\_karma" mixin for identifying key content contributors
-
-### Difference between original vote_fu and our gem
-
-Generator creates model vote.rb in you application instead of keeping it in the gem lib.
-We had bad experience with extending this class in application - unexpected things where having place.
-
-Installation
-============
-Use either the plugin or the gem installation method depending on your preference. If you're not sure, the plugin method is simpler. Whichever you choose, create the migration afterward and run it to create the required model.
-
-### Via plugin
- ./script/plugin install git://github.com/objectreload/vote_fu.git
-
-### Via gem
-Add the following to your application's environment.rb:
- config.gem "objectreload-vote_fu", :lib => 'vote_fu'
-
-Install the gem:
- rake gems:install
-
-### Create vote_fu migration and vote.rb model
- ./script/generate vote_fu
-
-Run the migration:
- rake db:migrate
-
-Usage
-=====
-
-## Getting Started
-
-### Make your ActiveRecord model act as voteable.
-
-
- class Model < ActiveRecord::Base
- acts_as_voteable
- end
-
-
-### Make your ActiveRecord model(s) that vote act as voter.
-
- class User < ActiveRecord::Base
- acts_as_voter
- end
-
- class Robot < ActiveRecord::Base
- acts_as_voter
- end
-
-### To cast a vote for a Model you can do the following:
-
-#### Shorthand syntax
- voter.vote_for(voteable) # Adds a +1 vote
- voter.vote_against(voteable) # Adds a -1 vote
- voter.vote(voteable, t_or_f) # Adds either +1 or -1 vote true => +1, false => -1
-
-#### ActsAsVoteable syntax
-The old acts\_as\_voteable syntax is still supported:
-
- vote = Vote.new(:vote => true)
- m = Model.find(params[:id])
- m.votes << vote
- user.votes << vote
-
-### Querying votes
-
-#### Tallying Votes
-
-You can easily retrieve voteable object collections based on the properties of their votes:
-
- @items = Item.tally(
- { :at_least => 1,
- :at_most => 10000,
- :start_at => 2.weeks.ago,
- :end_at => 1.day.ago,
- :limit => 10,
- :order => "items.name desc"
- })
-
-This will select the Items with between 1 and 10,000 votes, the votes having been cast within the last two weeks (not including today), then display the 10 last items in an alphabetical list.
-
-##### Tally 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
-
-#### Lower level queries
-ActiveRecord models that act as voteable can be queried for the positive votes, negative votes, and a total vote count by using the votes\_for, votes\_against, and votes\_count methods respectively. Here is an example:
-
- positiveVoteCount = m.votes_for
- negativeVoteCount = m.votes_against
- totalVoteCount = m.votes_count
-
-And because the Vote Fu plugin will add the has_many votes relationship to your model you can always get all the votes by using the votes property:
-
- allVotes = m.votes
-
-The mixin also provides these methods:
-
- voter.voted_for?(voteable) # True if the voter voted for this object.
- voter.vote_count([true|false|"all"]) # returns the count of +1, -1, or all votes
-
- voteable.voted_by?(voter) # True if the voter voted for this object.
- @voters = voteable.voters_who_voted
-
-
-#### Named Scopes
-
-The Vote model has several named scopes you can use to find vote details:
-
- @pete_votes = Vote.for_voter(pete)
- @post_votes = Vote.for_voteable(post)
- @recent_votes = Vote.recent(1.day.ago)
- @descending_votes = Vote.descending
-
-You can chain these together to make interesting queries:
-
- # Show all of Pete's recent votes for a certain Post, in descending order (newest first)
- @pete_recent_votes_on_post = Vote.for_voter(pete).for_voteable(post).recent(7.days.ago).descending
-
-### Experimental: Voteable Object Owner Karma
-I have just introduced the "has\_karma" mixin to this package. It aims to assign a karma score to the owners of voteable objects. This is designed to allow you to see which users are submitting the most highly voted content. Currently, karma is only "positive". That is, +1 votes add to karma, but -1 votes do not detract from it.
-
- class User
- has_many :posts
- has_karma :posts
- end
-
- class Post
- acts_as_voteable
- end
-
- # in your view, you can then do this:
- Karma: <%= @user.karma %>
-
-This feature is in alpha, but useful enough that I'm releasing it.
-
-### One vote per user!
-If you want to limit your users to a single vote on each item, take a look in lib/vote.rb.
-
- # Uncomment this to limit users to a single vote on each item.
- # validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
-
-And if you want that enforced at the database level, look in the generated migration for your voteable:
-
- # 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"
-
-### Example Application
-
-There is now a reference application available. Due to overwhelming demand for example
-code and kickstart guides, I have open-sourced MyQuotable.com in order to provide an
-easy-to-follow example of how to use VoteFu with RESTful Authentication, JRails, and
-other popular plugins. To get the example code:
-
- git clone git://github.com/peteonrails/myquotable.git
-
-There will be a screencast coming soon too. Contact me if you want to help.
-
-Consideration
-=============
-If you like this software and use it, please consider recommending me on Working With Rails.
-
-I don't want donations: a simple up-vote would make my day. My profile is: [http://www.workingwithrails.com/person/12521-peter-jackson][4]
-
-To go directly to the "Recommend Me" screen: [http://www.workingwithrails.com/recommendation/new/person/12521-peter-jackson][5]
-
-
-Credits
-=======
-
-#### Contributors
-
-* Bence Nagy, Budapest, Hungary
-* Jon Maddox, Richmond, Virginia, USA
-
-#### Other works
-
-[Juixe - The original ActsAsVoteable plugin inspired this code.][1]
-
-[Xelipe - This plugin is heavily influenced by Acts As Commentable.][2]
-
-[1]: http://www.juixe.com/techknow/index.php/2006/06/24/acts-as-voteable-rails-plugin/
-[2]: http://github.com/jackdempsey/acts_as_commentable/tree/master
-
-More
-====
-
-Support: [Use my blog for support.][6]
-
-
-[Documentation from the original acts\_as\_voteable plugin][3]
-
-[3]: http://www.juixe.com/techknow/index.php/2006/06/24/acts-as-voteable-rails-plugin/
-[4]: http://www.workingwithrails.com/person/12521-peter-jackson
-[5]: http://www.workingwithrails.com/recommendation/new/person/12521-peter-jackson
-[6]: http://blog.peteonrails.com
-
-Copyright (c) 2008 Peter Jackson, released under the MIT license
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..34f48ef
--- /dev/null
+++ b/README.md
@@ -0,0 +1,228 @@
+ThumbsUp
+=======
+
+[](http://travis-ci.org/bouchard/thumbs_up) [](https://codeclimate.com/github/bouchard/thumbs_up)
+
+**Note: Version 0.5.x is a breaking change for #plusminus_tally and #tally, with > 50% speedups.**
+
+A ridiculously straightforward and simple package 'o' code to enable voting in your application, a la stackoverflow.com, etc.
+Allows an arbitrary number of entities (users, etc.) to vote on models.
+
+### Mixins
+This plugin introduces three mixins to your recipe book:
+
+1. **acts\_as\_voteable** : Intended for content objects like Posts, Comments, etc. * (See *Configuration* below for caveats)
+2. **acts\_as\_voter** : Intended for voting entities, like Users. * (See *Configuration* below for caveats)
+3. **has\_karma** : Adds some helpers to acts\_as\_voter models for calculating karma.
+
+### Inspiration
+
+This plugin started as an adaptation / update of vote\_fu for use with Rails 3. It adds some speed, removes some cruft, and is adapted for use with ActiveRecord / Arel in Rails 3. It maintains the awesomeness of the original vote\_fu.
+
+Installation
+============
+
+### Require the gem:
+```shell
+gem 'thumbs_up'
+```
+
+### Create and run the ThumbsUp migration:
+```shell
+rails generate thumbs_up
+rake db:migrate
+```
+
+Configuration
+=============
+
+The relationship setup by the acts_as_voteable and acts_as_voter mixins both default to `votes`. This causes one to obscure the other if you have a single class that votes on other instances of the same class. If you have this scenario:
+```ruby
+class User < ActiveRecord::Base
+ acts_as_voter # relationship :votes will be obscured by the same named relationship from acts_as_voteable :(
+ acts_as_voteable
+end
+```
+Configure alternate relationship names in an initializer at `config/initializers/thumbs_up.rb`:
+```ruby
+ThumbsUp.configuration.voteable_relationship_name = :votes_on # defaults to :votes
+ThumbsUp.configuration.voter_relationship_name = :votes_by # defaults to :votes
+```
+
+
+Usage
+=====
+
+## Getting Started
+
+### Turn your AR models into something that can be voted upon.
+```ruby
+class SomeModel < ActiveRecord::Base
+ acts_as_voteable
+end
+
+class Question < ActiveRecord::Base
+ acts_as_voteable
+end
+```
+
+### Turn your Users (or any other model) into voters.
+```ruby
+class User < ActiveRecord::Base
+ acts_as_voter
+ # The following line is optional, and tracks karma (up votes) for questions this user has submitted.
+ # Each question has a submitter_id column that tracks the user who submitted it.
+ # The option :weight value will be multiplied to any karma from that voteable model (defaults to 1).
+ # You can track any voteable model.
+ has_karma :questions, :as => :submitter, :weight => 0.5
+ # Karma by default is only calculated from upvotes. If you pass an array to the weight option, you can count downvotes as well (below, downvotes count for half as much karma against you):
+ has_karma :questions, :as => :submitter, :weight => [ 1, 0.5 ]
+end
+
+class Robot < ActiveRecord::Base
+ acts_as_voter
+end
+```
+
+### To cast a vote for a Model you can do the following:
+
+#### Shorthand syntax
+```ruby
+voter.vote_for(voteable) # Adds a +1 vote
+voter.vote_against(voteable) # Adds a -1 vote
+
+voter.vote_exclusively_for(voteable) # Removes any previous votes by that particular voter, and votes for.
+voter.vote_exclusively_against(voteable) # Removes any previous votes by that particular voter, and votes against.
+
+# Alternative method, can pass a hash that includes `:exclusive` and `:direction` options.
+voter.vote(voteable, { :exclusive => false, :direction => :up }) # Votes non-exclusively, either a +1 or -1 depending on the `:direction` value
+
+voter.unvote_for(voteable) # Clears all votes for that user
+```
+
+### Querying votes
+
+Did the first user vote for the Car with id = 2 already?
+```ruby
+u = User.first
+u.vote_for(Car.find(2))
+u.voted_on?(Car.find(2)) #=> true
+```
+
+Did the first user vote for or against the Car with id = 2?
+```ruby
+u = User.first
+u.vote_for(Car.find(2))
+u.voted_for?(Car.find(2)) #=> true
+u.voted_against?(Car.find(2)) #=> false
+```
+
+Or check directly!
+```ruby
+u = User.first
+u.vote_for(Car.find(2))
+u.voted_how?(Car.find(2)) #=> true, if voted_for
+
+u.vote_against(Car.find(3))
+u.voted_how?(Car.find(3)) #=> false, if voted_against
+
+u.vote_for(Car.find(4))
+u.voted_how?(Car.find(4)) #=> nil, if didn't vote for it
+```
+
+in case you use `--unique-voting false` (documented below):
+```ruby
+u.voted_how?(Car.find(2)) #=> [false, true, true, false]
+```
+
+#### Tallying Votes
+
+You can easily retrieve voteable object collections based on the properties of their votes:
+```ruby
+@items = Item.tally.limit(10).where('created_at > ?', 2.days.ago).having('COUNT(votes.id) < 10')
+```
+
+Or for MySQL:
+```ruby
+@items = Item.tally.limit(10).where('created_at > ?', 2.days.ago).having('vote_count < 10')
+```
+
+This will select the Items with less than 10 votes, the votes having been cast within the last two days, with a limit of 10 items. *This tallies all votes, regardless of whether they are +1 (up) or -1 (down).* The #tally method returns an ActiveRecord Relation, so you can chain the normal method calls on to it.
+
+#### Tallying Rank ("Plusminus")
+
+**You most likely want to use this over the normal tally**
+
+This is similar to tallying votes, but this will return voteable object collections based on the sum of the differences between up and down votes (ups are +1, downs are -1). For Instance, a voteable with 3 upvotes and 2 downvotes will have a plusminus_tally of 1.
+```ruby
+@items = Item.plusminus_tally.limit(10).where('created_at > ?', 2.days.ago).having('plusminus_tally > 10')
+```
+
+#### Lower level queries
+```ruby
+positiveVoteCount = voteable.votes_for
+negativeVoteCount = voteable.votes_against
+# Votes for minus votes against. If you want more than a few model instances' worth, use `plusminus_tally` instead.
+plusminus = voteable.plusminus
+
+voter.voted_for?(voteable) # True if the voter voted for this object.
+voter.vote_count(:up | :down | :all) # returns the count of +1, -1, or all votes
+
+voteable.voted_by?(voter) # True if the voter voted for this object.
+@voters = voteable.voters_who_voted
+```
+
+
+### One vote per user!
+
+ThumbsUp by default only allows one vote per user. This can be changed by removing:
+
+#### In vote.rb:
+```ruby
+validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
+```
+
+#### In the migration, the unique index:
+```ruby
+add_index :votes, ["voter_id", "voter_type", "voteable_id", "voteable_type"], :unique => true, :name => "uniq_one_vote_only"
+```
+You can also use `--unique-voting false` when running the generator command:
+```shell
+rails generate thumbs_up --unique-voting false
+```
+
+#### Testing ThumbsUp
+
+Testing is a bit more than trivial now as our #tally and #plusminus_tally queries don't function properly under SQLite. To set up for testing:
+
+* mysql
+
+```sql
+$ mysql -uroot # You may have set a password locally. Change as needed.
+ > CREATE USER 'test'@'localhost' IDENTIFIED BY 'test';
+ > CREATE DATABASE thumbs_up_test;
+ > USE thumbs_up_test;
+ > GRANT ALL PRIVILEGES ON thumbs_up_test TO 'test'@'localhost' IDENTIFIED BY 'test';
+ > exit;
+```
+* Postgres
+
+```PLpgSQL
+$ psql # You may have set a password locally. Change as needed.
+ > CREATE ROLE test;
+ > ALTER ROLE test WITH SUPERUSER;
+ > ALTER ROLE test WITH LOGIN;
+ > CREATE DATABASE thumbs_up_test;
+ > GRANT ALL PRIVILEGES ON DATABASE thumbs_up_test to test;
+ > \q
+```
+* Run tests
+
+```shell
+$ rake # Runs the test suite against all adapters.
+```
+
+Credits
+=======
+
+Basic scaffold is from Peter Jackson's work on VoteFu / ActsAsVoteable. All code updated for Rails 3, cleaned up for speed and clarity, karma calculation fixed, and (hopefully) zero introduced bugs.
diff --git a/Rakefile b/Rakefile
index 188b186..9d656ae 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,57 +1,46 @@
# encoding: UTF-8
require 'rubygems'
-require 'rake'
+require 'bundler' unless defined?(Bundler)
+
+$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
+require 'thumbs_up/version'
begin
- require 'jeweler'
- Jeweler::Tasks.new do |gem|
- gem.name = "objectreload-vote_fu"
- gem.summary = "Voting for ActiveRecord with multiple vote sources and advanced features."
- gem.description = "VoteFu provides the ability to have multiple voting entities on an arbitrary number of models in ActiveRecord."
- gem.email = "gems@objectreload.com"
- gem.homepage = "http://github.com/objectreload/vote_fu"
- gem.authors = ["Peter Jackson", "Cosmin Radoi", "Bence Nagy", "Rob Maddox", "Wojciech Wnętrzak"]
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
- end
- Jeweler::GemcutterTasks.new
-rescue LoadError
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
+ Bundler.setup(:default, :development)
+rescue Bundler::BundlerError => e
+ $stderr.puts e.message
+ $stderr.puts "Run `bundle install` to install missing gems"
+ exit e.status_code
end
+require 'rake'
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
- test.pattern = 'test/**/*_test.rb'
+ test.test_files = Dir.glob("test/**/*_test.rb")
test.verbose = true
end
-begin
- require 'rcov/rcovtask'
- Rcov::RcovTask.new do |test|
- test.libs << 'test'
- test.pattern = 'test/**/*_test.rb'
- test.verbose = true
- end
-rescue LoadError
- task :rcov do
- abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
- end
+task :build do
+ system "gem build thumbs_up.gemspec"
end
-task :test => :check_dependencies
-
-task :default => :test
+task :release => :build do
+ system "gem push thumbs_up-#{ThumbsUp::VERSION}.gem"
+ system "rm thumbs_up-#{ThumbsUp::VERSION}.gem"
+end
-require 'rake/rdoctask'
-Rake::RDocTask.new do |rdoc|
- if File.exist?('VERSION')
- version = File.read('VERSION')
- else
- version = ""
- end
+task :test_all_databases do
+ # Test MySQL, Postgres and SQLite3
+ ENV['DB'] = 'mysql'
+ puts "Testing MySQL..."
+ Rake::Task['test'].execute
+ ENV['DB'] = 'postgres'
+ puts "Testing Postgres..."
+ Rake::Task['test'].execute
+ ENV['DB'] = 'sqlite3'
+ puts "Testing SQLite3..."
+ Rake::Task['test'].execute
+end
- rdoc.rdoc_dir = 'rdoc'
- rdoc.title = "permissions_gem #{version}"
- rdoc.rdoc_files.include('README*')
- rdoc.rdoc_files.include('lib/**/*.rb')
-end
\ No newline at end of file
+task :default => :test_all_databases
diff --git a/VERSION b/VERSION
deleted file mode 100644
index 6e8bf73..0000000
--- a/VERSION
+++ /dev/null
@@ -1 +0,0 @@
-0.1.0
diff --git a/examples/routes.rb b/examples/routes.rb
deleted file mode 100644
index 3a620d6..0000000
--- a/examples/routes.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-
-map.resources :users do |user|
- user.resources :votes
- user.resources :voteable do |mv|
- mv.resources :votes
- end
-end
\ No newline at end of file
diff --git a/examples/users_controller.rb b/examples/users_controller.rb
deleted file mode 100644
index 091351c..0000000
--- a/examples/users_controller.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# I usually use the user class from restful_authentication as my principle voter class
-# There are generally no changes required to support voting in this controller.
-
-class UsersController < ApplicationController
- # Be sure to include AuthenticationSystem in Application Controller instead
- include AuthenticatedSystem
-
- # Protect these actions behind an admin login
- before_filter :admin_required, :only => [:suspend, :unsuspend, :destroy, :purge]
- before_filter :find_user, :only => [:suspend, :unsuspend, :destroy, :purge, :show]
-
- before_filter :login_required, :only => [:index]
-
- # render new.html.erb
- def new
- end
-
- # GET /users/:id
- def show
- end
-
-
- def create
- cookies.delete :auth_token
- @user = User.new(params[:user])
- @user.register! if @user.valid?
- if @user.errors.empty?
- self.current_user.forget_me if logged_in?
- cookies.delete :auth_token
- reset_session
- flash[:notice] = "Thanks for signing up!"
- else
- render :action => 'new'
- end
- end
-
- def activate
- unless params[:activation_code].blank?
- self.current_user = User.find_by_activation_code(params[:activation_code])
- if logged_in? && !current_user.active?
- current_user.activate!
- flash[:notice] = "Signup complete!"
- redirect_back_or_default('/')
- else
- flash[:error] = "Sorry, we couldn't find that activation code. Please cut and paste your activation code into the space at left."
- end
- end
- # render activate.html.erb
- end
-
- def suspend
- @user.suspend!
- redirect_to users_path
- end
-
- def unsuspend
- @user.unsuspend!
- redirect_to users_path
- end
-
- def destroy
- @user.delete!
- redirect_to users_path
- end
-
- def purge
- @user.destroy
- redirect_to users_path
- end
-
-protected
- def find_user
- @user = User.find(params[:id])
- end
-
-end
diff --git a/examples/voteable.html.erb b/examples/voteable.html.erb
deleted file mode 100644
index df35ff2..0000000
--- a/examples/voteable.html.erb
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- ..... Show some fields .....
-
-
- <%= render :partial => "votes/voteable_vote", :locals => {:voteable => @voteable} %>
-
-
diff --git a/examples/voteable.rb b/examples/voteable.rb
deleted file mode 100644
index 60e09db..0000000
--- a/examples/voteable.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class Voteable < ActiveRecord::Base
-
- belongs_to :user
-
- acts_as_voteable
-
- named_scope :descending, :order => "created_at DESC"
-
-
-end
\ No newline at end of file
diff --git a/examples/voteables_controller.rb b/examples/voteables_controller.rb
deleted file mode 100644
index 71dadd7..0000000
--- a/examples/voteables_controller.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# This example controller assumes you are using the User class from restful_authentication
-# and a nested voteable resource. See routes.rb
-
-
-class VoteablesController < ApplicationController
-
- before_filter :find_user
- before_filter :login_required, :only => [:new, :edit, :destroy, :create, :update]
- before_filter :must_own_voteable, :only => [:edit, :destroy, :update]
-
- # GET /users/:id/voteables
- # GET /users/:id/voteables.xml
- def index
- @voteable = Voteable.descending
-
- respond_to do |format|
- format.html # index.html.erb
- format.xml { render :xml => @voteables }
- end
- end
-
- # GET /users/:id/voteables/1
- # GET /users/:id/voteables/1.xml
- def show
- @voteable = Voteable.find(params[:id])
-
- respond_to do |format|
- format.html # show.html.erb
- format.xml { render :xml => @voteable }
- end
- end
-
- # GET /users/:id/voteables/new
- # GET /users/:id/voteables/new.xml
- def new
- @voteable = Voteable.new
-
- respond_to do |format|
- format.html # new.html.erb
- format.xml { render :xml => @voteable }
- end
- end
-
- # GET /users/:id/voteables/1/edit
- def edit
- @voteable ||= Voteable.find(params[:id])
- end
-
- # POST /users/:id/voteables
- # POST /users/:id/voteables.xml
- def create
- @voteable = Voteable.new(params[:voteable])
- @voteable.user = current_user
-
- respond_to do |format|
- if @voteable.save
- flash[:notice] = 'Voteable was successfully saved.'
- format.html { redirect_to([@user, @voteable]) }
- format.xml { render :xml => @voteable, :status => :created, :location => @voteable }
- else
- format.html { render :action => "new" }
- format.xml { render :xml => @voteable.errors, :status => :unprocessable_entity }
- end
- end
- end
-
- # PUT /users/:id/voteable/1
- # PUT /users/:id/voteable/1.xml
- def update
- @voteable = Voteable.find(params[:id])
-
- respond_to do |format|
- if @quote.update_attributes(params[:voteable])
- flash[:notice] = 'Voteable was successfully updated.'
- format.html { redirect_to([@user, @voteable]) }
- format.xml { head :ok }
- else
- format.html { render :action => "edit" }
- format.xml { render :xml => @voteable.errors, :status => :unprocessable_entity }
- end
- end
- end
-
- # DELETE /users/:id/voteable/1
- # DELETE /users/:id/voteable/1.xml
- def destroy
- @voteable = Voteable.find(params[:id])
- @voteable.destroy
-
- respond_to do |format|
- format.html { redirect_to(user_voteables_url) }
- format.xml { head :ok }
- end
- end
-
- private
- def find_user
- @user = User.find(params[:user_id])
- end
-
- def must_own_voteable
- @voteable ||= Voteable.find(params[:id])
- @voteable.user == current_user || ownership_violation
- end
-
- def ownership_violation
- respond_to do |format|
- flash[:notice] = 'You cannot edit or delete voteable that you do not own!'
- format.html do
- redirect_to user_path(current_user)
- end
- end
- end
-
-
-
-end
diff --git a/examples/votes/_voteable_vote.html.erb b/examples/votes/_voteable_vote.html.erb
deleted file mode 100644
index bf2bdb1..0000000
--- a/examples/votes/_voteable_vote.html.erb
+++ /dev/null
@@ -1,23 +0,0 @@
-<%
- # You can't vote if it is your quote,
- # you are not logged in,
- # or you have already voted on this item
-
- unless quote.user == current_user ||
- !logged_in? ||
- current_user.voted_on?(@voteable)
-%>
-
- <%= link_to_remote "Up",
- :url => user_voteable_votes_path(voteable.user, voteable, :vote => :true, :format => :rjs),
- :method => :post
- %>
- /
- <%= link_to_remote "Down",
- :url => user_voteable_votes_path(voteable.user, voteable, :vote => :false, :format => :rjs),
- :method => :post
- %>
-
-<% end %>
-
-Votes: <%= voteable.votes_for - voteable.votes_against %>
diff --git a/examples/votes/create.rjs b/examples/votes/create.rjs
deleted file mode 100644
index ba91c89..0000000
--- a/examples/votes/create.rjs
+++ /dev/null
@@ -1 +0,0 @@
-page.replace_html "votes_#{@voteable.id}", :partial => "voteable_vote", :locals => {:voteable => @voteable}
diff --git a/examples/votes_controller.rb b/examples/votes_controller.rb
deleted file mode 100644
index 7bccad9..0000000
--- a/examples/votes_controller.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-# An example controller for "votes" that are nested resources under users. See examples/routes.rb
-
-class VotesController < ApplicationController
-
- # First, figure out our nested scope. User or Voteable?
- before_filter :find_votes_for_my_scope, :only => [:index]
-
- before_filter :login_required, :only => [:new, :edit, :destroy, :create, :update]
- before_filter :must_own_vote, :only => [:edit, :destroy, :update]
- before_filter :not_allowed, :only => [:edit, :update, :new]
-
- # GET /users/:user_id/votes/
- # GET /users/:user_id/votes.xml
- # GET /users/:user_id/voteables/:voteable_id/votes/
- # GET /users/:user_id/voteables/:voteable_id/votes.xml
- def index
- respond_to do |format|
- format.html # index.html.erb
- format.xml { render :xml => @votes }
- end
- end
-
- # GET /users/:user_id/votes/1
- # GET /users/:user_id/votes/1.xml
- # GET /users/:user_id/voteables/:voteable_id/votes/1
- # GET /users/:user_id/voteables/:voteable_id/1.xml
- def show
- @voteable = Vote.find(params[:id])
-
- respond_to do |format|
- format.html # show.html.erb
- format.xml { render :xml => @vote }
- end
- end
-
- # GET /users/:id/votes/new
- # GET /users/:id/votes/new.xml
- # GET /users/:id/votes/new
- # GET /users/:id/votes/new.xml
- def new
- # Not generally used. Most people want to vote via AJAX calls.
- end
-
- # GET /users/:id/votes/1/edit
- def edit
- # Not generally used. Most people don't want to allow editing of votes.
- end
-
- # POST /users/:user_id/voteables/:voteable_id/votes
- # POST /users/:user_id/voteables/:voteable_id/votes.xml
- def create
- @voteable = Voteable.find(params[:quote_id])
-
- respond_to do |format|
- if current_user.vote(@voteable, params[:vote])
- format.rjs { render :action => "create", :vote => @vote }
- format.html { redirect_to([@voteable.user, @voteable]) }
- format.xml { render :xml => @voteable, :status => :created, :location => @voteable }
- else
- format.rjs { render :action => "error" }
- format.html { render :action => "new" }
- format.xml { render :xml => @vote.errors, :status => :unprocessable_entity }
- end
- end
- end
-
- # PUT /users/:id/votes/1
- # PUT /users/:id/votes/1.xml
- def update
- # Not generally used
- end
-
- # DELETE /users/:id/votes/1
- # DELETE /users/:id/votes/1.xml
- def destroy
- @vote = Vote.find(params[:id])
- @vote.destroy
-
- respond_to do |format|
- format.html { redirect_to(user_votes_url) }
- format.xml { head :ok }
- end
- end
-
- private
- def find_votes_for_my_scope
- if params[:voteable_id]
- @votes = Vote.for_voteable(Voteable.find(params[:voteable_id])).descending
- elsif params[:user_id]
- @votes = Vote.for_voter(User.find(params[:user_id])).descending
- else
- @votes = []
- end
- end
-
- def must_own_vote
- @vote ||= Vote.find(params[:id])
- @vote.user == current_user || ownership_violation
- end
-
- def ownership_violation
- respond_to do |format|
- flash[:notice] = 'You cannot edit or delete votes that you do not own!'
- format.html do
- redirect_to user_path(current_user)
- end
- end
- end
-
-end
diff --git a/generators/vote_fu/templates/migration.rb b/generators/vote_fu/templates/migration.rb
deleted file mode 100644
index 62052c9..0000000
--- a/generators/vote_fu/templates/migration.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class VoteFuMigration < ActiveRecord::Migration
- def self.up
- create_table :votes, :force => true do |t|
- t.boolean :vote, :default => false
- 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
diff --git a/generators/vote_fu/templates/vote.rb b/generators/vote_fu/templates/vote.rb
deleted file mode 100644
index 5a8bf15..0000000
--- a/generators/vote_fu/templates/vote.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-class Vote < ActiveRecord::Base
-
- named_scope :for_voter, lambda { |*args| {:conditions => ["voter_id = ? AND voter_type = ?", args.first.id, args.first.type.name]} }
- named_scope :for_voteable, lambda { |*args| {:conditions => ["voteable_id = ? AND voteable_type = ?", args.first.id, args.first.type.name]} }
- named_scope :recent, lambda { |*args| {:conditions => ["created_at > ?", (args.first || 2.weeks.ago).to_s(:db)]} }
- named_scope :descending, :order => "created_at DESC"
-
- # NOTE: Votes belong to the "voteable" interface, and also to voters
- belongs_to :voteable, :polymorphic => true
- belongs_to :voter, :polymorphic => true
-
- attr_accessible :vote, :voter, :voteable
-
- # Uncomment this to limit users to a single vote on each item.
- # validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
-end
\ No newline at end of file
diff --git a/generators/vote_fu/vote_fu_generator.rb b/generators/vote_fu/vote_fu_generator.rb
deleted file mode 100644
index e0b7e6c..0000000
--- a/generators/vote_fu/vote_fu_generator.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class VoteFuGenerator < Rails::Generator::Base
-
- def manifest
- record do |m|
- m.directory File.join('db', 'migrate')
- m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => 'vote_fu_migration'
- m.directory File.join('app', 'models')
- m.template 'vote.rb', File.join('app', 'models', 'vote.rb')
- end
- end
-end
diff --git a/lib/acts_as_voteable.rb b/lib/acts_as_voteable.rb
index 9e0e80b..bc8f353 100644
--- a/lib/acts_as_voteable.rb
+++ b/lib/acts_as_voteable.rb
@@ -1,114 +1,135 @@
-# 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
+module ThumbsUp
+ module ActsAsVoteable #:nodoc:
- #
- # 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)]) if options[:end_at]
-
- type_and_context = "#{Vote.table_name}.voteable_type = #{quote_value(base_class.name)}"
-
- conditions = [
- type_and_context,
- options[:conditions],
- start_at,
- end_at
- ]
-
- conditions = conditions.compact.join(' AND ')
- conditions = merge_conditions(conditions, scope[:conditions]) if scope
-
- joins = ["LEFT OUTER JOIN #{Vote.table_name} ON #{table_name}.#{primary_key} = #{Vote.table_name}.voteable_id"]
- joins << scope[:joins] if scope && scope[:joins]
- at_least = sanitize_sql(["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)]) if options[:at_most]
- having = [at_least, at_most].compact.join(' AND ')
- group_by = "#{Vote.table_name}.voteable_id HAVING COUNT(#{Vote.table_name}.id) > 0"
- group_by << " AND #{having}" unless having.blank?
-
- { :select => "#{table_name}.*, COUNT(#{Vote.table_name}.id) AS count",
- :joins => joins.join(" "),
- :conditions => conditions,
- :group => group_by
- }.update(options)
- end
+ def self.included(base)
+ base.extend ThumbsUp::Base
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_voteable
+ has_many ThumbsUp.configuration[:voteable_relationship_name],
+ :as => :voteable,
+ :dependent => :destroy,
+ :class_name => 'Vote'
+
+ include ThumbsUp::ActsAsVoteable::InstanceMethods
+ extend ThumbsUp::ActsAsVoteable::SingletonMethods
end
-
- # This module contains instance methods
- module InstanceMethods
- def votes_for
- Vote.count(:all, :conditions => [
- "voteable_id = ? AND voteable_type = ? AND vote = ?",
- id, self.class.name, true
- ])
- end
-
- def votes_against
- Vote.count(:all, :conditions => [
- "voteable_id = ? AND voteable_type = ? AND vote = ?",
- id, self.class.name, false
- ])
- 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
+
+ module SingletonMethods
+
+ # Calculate the plusminus for a group of voteables in one database query.
+ # This returns an Arel relation, so you can add conditions as you like chained on to
+ # this method call.
+ # i.e. Posts.tally.where('votes.created_at > ?', 2.days.ago)
+ # You can also have the upvotes and downvotes returned separately in the same query:
+ # Post.plusminus_tally(:separate_updown => true)
+ def plusminus_tally(params = {})
+ t = self.joins("LEFT OUTER JOIN #{Vote.table_name} ON #{self.table_name}.id = #{Vote.table_name}.voteable_id AND #{Vote.table_name}.voteable_type = '#{self.name}'")
+ t = t.order("plusminus_tally DESC")
+ t = t.group(column_names_for_tally)
+ t = t.select("#{self.table_name}.*")
+ t = t.select("SUM(CASE #{Vote.table_name}.vote WHEN #{quoted_true} THEN 1 WHEN #{quoted_false} THEN -1 ELSE 0 END) AS plusminus_tally")
+ if params[:separate_updown]
+ t = t.select("SUM(CASE #{Vote.table_name}.vote WHEN #{quoted_true} THEN 1 WHEN #{quoted_false} THEN 0 ELSE 0 END) AS up")
+ t = t.select("SUM(CASE #{Vote.table_name}.vote WHEN #{quoted_true} THEN 0 WHEN #{quoted_false} THEN 1 ELSE 0 END) AS down")
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
+ t = t.select("COUNT(#{Vote.table_name}.id) AS vote_count")
+ 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 (even without any votes) by default.
+ # The vote count for each voteable is available as #vote_count.
+ # This returns an Arel relation, so you can add conditions as you like chained on to
+ # this method call.
+ # i.e. Posts.tally.where('votes.created_at > ?', 2.days.ago)
+ def tally(*args)
+ t = self.joins("LEFT OUTER JOIN #{Vote.table_name} ON #{self.table_name}.id = #{Vote.table_name}.voteable_id")
+ t = t.order("vote_count DESC")
+ t = t.group(column_names_for_tally)
+ t = t.select("#{self.table_name}.*")
+ t = t.select("COUNT(#{Vote.table_name}.id) AS vote_count")
+ end
+
+ def column_names_for_tally
+ column_names.map { |column| "#{self.table_name}.#{column}" }.join(', ')
+ end
+
+ end
+
+ module InstanceMethods
+
+ # wraps the dynamic, configured, relationship name
+ def _votes_on
+ self.send(ThumbsUp.configuration[:voteable_relationship_name])
+ end
+
+ def votes_for
+ self._votes_on.where(:vote => true).count
+ end
+
+ def votes_against
+ self._votes_on.where(:vote => false).count
+ end
+
+ def percent_for
+ (votes_for.to_f * 100 / (self._votes_on.size + 0.0001)).round
+ end
+
+ def percent_against
+ (votes_against.to_f * 100 / (self._votes_on.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.
+ # If you're using this for a lot of voteables, then you'd best use the #plusminus_tally
+ # method above.
+ def plusminus
+ respond_to?(:plusminus_tally) ? plusminus_tally : (votes_for - votes_against)
+ end
+
+ # The lower bound of a Wilson Score with a default confidence interval of 95%. Gives a more accurate representation of average rating (plusminus) based on the number of positive ratings and total ratings.
+ # http://evanmiller.org/how-not-to-sort-by-average-rating.html
+ def ci_plusminus(confidence = 0.95)
+ require 'statistics2'
+ n = self._votes_on.size
+ if n == 0
+ return 0
end
-
-
+ z = Statistics2.pnormaldist(1 - (1 - confidence) / 2)
+ phat = 1.0 * votes_for / n
+ (phat + z * z / (2 * n) - z * Math.sqrt((phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n)
end
+
+ def votes_count
+ _votes_on.size
+ end
+
+ def voters_who_voted
+ _votes_on.map(&:voter).uniq
+ end
+
+ def voters_who_voted_for
+ _votes_on.where(:vote => true).map(&:voter).uniq
+ end
+
+ def voters_who_voted_against
+ _votes_on.where(:vote => false).map(&:voter).uniq
+ end
+
+ def voted_by?(voter)
+ 0 < Vote.where(
+ :voteable_id => self.id,
+ :voteable_type => self.class.base_class.name,
+ :voter_id => voter.id
+ ).count
+ end
+
end
end
end
diff --git a/lib/acts_as_voter.rb b/lib/acts_as_voter.rb
index 0f872f4..a074764 100644
--- a/lib/acts_as_voter.rb
+++ b/lib/acts_as_voter.rb
@@ -1,75 +1,137 @@
-# ActsAsVoter
-module PeteOnRails
- module Acts #:nodoc:
- module Voter #:nodoc:
+module ThumbsUp #:nodoc:
+ module ActsAsVoter #:nodoc:
- def self.included(base)
- base.extend ClassMethods
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_voter
+
+ # If a voting entity is deleted, keep the votes.
+ # If you want to nullify (and keep the votes), you'll need to remove
+ # the unique constraint on the [ voter, voteable ] index in the database.
+ # has_many :votes, :as => :voter, :dependent => :nullify
+ # Destroy voter's votes when the voter is deleted.
+ has_many ThumbsUp.configuration[:voter_relationship_name],
+ :as => :voter,
+ :dependent => :destroy,
+ :class_name => 'Vote'
+
+ include ThumbsUp::ActsAsVoter::InstanceMethods
+ extend ThumbsUp::ActsAsVoter::SingletonMethods
end
+ 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
+ # This module contains class methods
+ module SingletonMethods
+ end
+
+ # This module contains instance methods
+ module InstanceMethods
+
+ # wraps the dynamic, configured, relationship name
+ def _votes_by
+ self.send(ThumbsUp.configuration[:voter_relationship_name])
+ end
+
+ # Usage user.vote_count(:up) # All +1 votes
+ # user.vote_count(:down) # All -1 votes
+ # user.vote_count() # All votes
+
+ def vote_count(for_or_against = :all)
+ v = Vote.where(:voter_id => id).where(:voter_type => self.class.base_class.name)
+ v = case for_or_against
+ when :all then v
+ when :up then v.where(:vote => true)
+ when :down then v.where(:vote => false)
end
+ v.count
end
-
- # This module contains class methods
- module SingletonMethods
+
+ def voted_for?(voteable)
+ voted_which_way?(voteable, :up)
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 = ? AND voteable_id = ? AND voteable_type = ?",
- self.id, self.class.name, true, voteable.id, voteable.class.name
- ])
- end
-
- def voted_against?(voteable)
- 0 < Vote.count(:all, :conditions => [
- "voter_id = ? AND voter_type = ? AND vote = ? AND voteable_id = ? AND voteable_type = ?",
- self.id, self.class.name, false, voteable.id, voteable.class.name
- ])
- 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, true)
- end
-
- def vote_against(voteable)
- self.vote(voteable, false)
- end
+ def voted_against?(voteable)
+ voted_which_way?(voteable, :down)
+ end
+
+ def voted_on?(voteable)
+ 0 < Vote.where(
+ :voter_id => self.id,
+ :voter_type => self.class.base_class.name,
+ :voteable_id => voteable.id,
+ :voteable_type => voteable.class.base_class.name
+ ).count
+ end
+
+ def vote_for(voteable)
+ self.vote(voteable, { :direction => :up, :exclusive => false })
+ end
+
+ def vote_against(voteable)
+ self.vote(voteable, { :direction => :down, :exclusive => false })
+ end
+
+ def vote_exclusively_for(voteable)
+ self.vote(voteable, { :direction => :up, :exclusive => true })
+ end
+
+ def vote_exclusively_against(voteable)
+ self.vote(voteable, { :direction => :down, :exclusive => true })
+ end
- def vote(voteable, vote)
- vote = Vote.new(:vote => vote, :voteable => voteable, :voter => self)
- vote.save
+ def vote(voteable, options = {})
+ raise ArgumentError, "you must specify :up or :down in order to vote" unless options[:direction] && [:up, :down].include?(options[:direction].to_sym)
+ if options[:exclusive]
+ self.unvote_for(voteable)
end
+ direction = (options[:direction].to_sym == :up)
+ # create! does not return the created object
+ v = Vote.new(:vote => direction, :voteable => voteable, :voter => self)
+ v.save!
+ v
+ end
+
+ def unvote_for(voteable)
+ Vote.where(
+ :voter_id => self.id,
+ :voter_type => self.class.base_class.name,
+ :voteable_id => voteable.id,
+ :voteable_type => voteable.class.base_class.name
+ ).map(&:destroy)
+ end
+
+ alias_method :clear_votes, :unvote_for
+ def voted_which_way?(voteable, direction)
+ raise ArgumentError, "expected :up or :down" unless [:up, :down].include?(direction)
+ 0 < Vote.where(
+ :voter_id => self.id,
+ :voter_type => self.class.base_class.name,
+ :vote => direction == :up ? true : false,
+ :voteable_id => voteable.id,
+ :voteable_type => voteable.class.base_class.name
+ ).count
end
+
+ def voted_how?(voteable)
+ votes = Vote.where(
+ :voter_id => self.id,
+ :voter_type => self.class.base_class.name,
+ :voteable_id => voteable.id,
+ :voteable_type => voteable.class.base_class.name
+ ).map(&:vote) #in case votes is premitted to be duplicated
+ if votes.count == 1
+ votes.first
+ elsif votes.count == 0
+ nil
+ else
+ votes
+ end
+ end
+
end
end
end
diff --git a/lib/generators/thumbs_up/templates/migration.rb b/lib/generators/thumbs_up/templates/migration.rb
new file mode 100644
index 0000000..8be4c55
--- /dev/null
+++ b/lib/generators/thumbs_up/templates/migration.rb
@@ -0,0 +1,25 @@
+class ThumbsUpMigration < ActiveRecord::Migration
+ def self.up
+ create_table :votes, :force => true do |t|
+
+ t.boolean :vote, :default => false, :null => false
+ t.references :voteable, :polymorphic => true, :null => false
+ t.references :voter, :polymorphic => true
+ t.timestamps
+
+ end
+
+ add_index :votes, [:voter_id, :voter_type]
+ add_index :votes, [:voteable_id, :voteable_type]
+
+<% if options[:unique_voting] == true %>
+ # Comment out the line below to allow multiple votes per voter on a single entity.
+ add_index :votes, [:voter_id, :voter_type, :voteable_id, :voteable_type], :unique => true, :name => 'fk_one_vote_per_user_per_entity'
+<% end %>
+ end
+
+ def self.down
+ drop_table :votes
+ end
+
+end
\ No newline at end of file
diff --git a/lib/generators/thumbs_up/templates/vote.rb b/lib/generators/thumbs_up/templates/vote.rb
new file mode 100644
index 0000000..bbcc2a9
--- /dev/null
+++ b/lib/generators/thumbs_up/templates/vote.rb
@@ -0,0 +1,17 @@
+class Vote < ActiveRecord::Base
+
+ scope :for_voter, lambda { |*args| where(["voter_id = ? AND voter_type = ?", args.first.id, args.first.class.base_class.name]) }
+ scope :for_voteable, lambda { |*args| where(["voteable_id = ? AND voteable_type = ?", args.first.id, args.first.class.base_class.name]) }
+ scope :recent, lambda { |*args| where(["created_at > ?", (args.first || 2.weeks.ago)]) }
+ scope :descending, lambda { order("created_at DESC") }
+
+ belongs_to :voteable, :polymorphic => true
+ belongs_to :voter, :polymorphic => true
+
+ attr_accessible :vote, :voter, :voteable if ActiveRecord::VERSION::MAJOR < 4
+
+<% if options[:unique_voting] == true %>
+ # Comment out the line below to allow multiple votes per user.
+ validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
+<% end %>
+end
diff --git a/lib/generators/thumbs_up/thumbs_up_generator.rb b/lib/generators/thumbs_up/thumbs_up_generator.rb
new file mode 100644
index 0000000..1df4e32
--- /dev/null
+++ b/lib/generators/thumbs_up/thumbs_up_generator.rb
@@ -0,0 +1,29 @@
+require 'rails/generators/active_record'
+
+class ThumbsUpGenerator < Rails::Generators::Base
+
+ include Rails::Generators::Migration
+
+ source_root File.expand_path('../templates', __FILE__)
+
+ class_option :unique_voting, :type => :boolean, :default => true, :desc => 'Do you want only one vote allowed per voter? (default: true)'
+
+ # Implement the required interface for Rails::Generators::Migration.
+ def self.next_migration_number(dirname) #:nodoc:
+ next_migration_number = current_migration_number(dirname) + 1
+ if ActiveRecord::Base.timestamped_migrations
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
+ else
+ "%.3d" % next_migration_number
+ end
+ end
+
+ def create_thumbs_up_migration
+ migration_template 'migration.rb', File.join('db', 'migrate', 'thumbs_up_migration.rb')
+ end
+
+ def move_vote_model
+ template 'vote.rb', File.join('app', 'models', 'vote.rb')
+ end
+
+end
diff --git a/lib/has_karma.rb b/lib/has_karma.rb
index 53473bd..73f4f2e 100644
--- a/lib/has_karma.rb
+++ b/lib/has_karma.rb
@@ -1,68 +1,48 @@
-# Has Karma
+module ThumbsUp #:nodoc:
+ module Karma #:nodoc:
-module PeteOnRails
- module VoteFu #:nodoc:
- module Karma #:nodoc:
+ def self.included(base)
+ base.extend ClassMethods
+ class << base
+ attr_accessor :karmic_objects
+ end
+ end
- def self.included(base)
- base.extend ClassMethods
- class << base
- attr_accessor :karmatic_objects
- end
+ module ClassMethods
+ def has_karma(voteable_type, options = {})
+ include ThumbsUp::Karma::InstanceMethods
+ extend ThumbsUp::Karma::SingletonMethods
+ self.karmic_objects ||= {}
+ self.karmic_objects[voteable_type.to_s.classify.constantize] = [ (options[:as] ? options[:as].to_s.foreign_key : self.name.foreign_key), [ (options[:weight] || 1) ].flatten.map(&:to_f) ]
end
+ end
- module ClassMethods
- def has_karma(voteable_type)
- self.class_eval <<-RUBY
- def karma_voteable
- #{voteable_type.to_s.classify}
- end
- RUBY
- include PeteOnRails::VoteFu::Karma::InstanceMethods
- extend PeteOnRails::VoteFu::Karma::SingletonMethods
- if self.karmatic_objects.nil?
- self.karmatic_objects = [eval(voteable_type.to_s.classify)]
+ module SingletonMethods
+
+ # Not yet implemented. Don't use it!
+ # Find the most popular users
+ # def find_most_karmic
+ # self.all
+ # end
+
+ end
+
+ module InstanceMethods
+ def karma(options = {})
+ self.class.base_class.karmic_objects.collect do |object, attr|
+ v = object.where(["#{self.class.base_class.table_name}.#{self.class.base_class.primary_key} = ?", self.id])
+ v = v.joins("INNER JOIN #{Vote.table_name} ON #{Vote.table_name}.voteable_type = '#{object.to_s}' AND #{Vote.table_name}.voteable_id = #{object.table_name}.#{object.primary_key}")
+ v = v.joins("INNER JOIN #{self.class.base_class.table_name} ON #{self.class.base_class.table_name}.#{self.class.base_class.primary_key} = #{object.table_name}.#{attr[0]}")
+ upvotes = v.where(["#{Vote.table_name}.vote = ?", true])
+ downvotes = v.where(["#{Vote.table_name}.vote = ?", false])
+ if attr[1].length == 1 # Only count upvotes, not downvotes.
+ (upvotes.count.to_f * attr[1].first).round
else
- self.karmatic_objects.push(eval(voteable_type.to_s.classify))
+ (upvotes.count.to_f * attr[1].first - downvotes.count.to_f * attr[1].last).round
end
- end
- end
-
- # This module contains class methods
- module SingletonMethods
-
- ## Not yet implemented. Don't use it!
- # Find the most popular users
- def find_most_karmic
- find(:all)
- end
-
+ end.sum
end
-
- # This module contains instance methods
- module InstanceMethods
- def karma(options = {})
- #FIXME cannot have 2 models imapcting the karma simultaneously
- # count the total number of votes on all of the voteable objects that are related to this object
- #2009-01-30 GuillaumeNM The following line is not SQLite3 compatible, because boolean are stored as 'f' or 't', not '1', or '0'
- #self.karma_voteable.sum(:vote, options_for_karma(options))
- #self.karma_voteable.find(:all, options_for_karma(options)).length
- karma_value = 0
- self.class.karmatic_objects.each do |object|
- karma_value += object.find(:all, options_for_karma(object, options)).length
- end
- return karma_value
- end
-
- def options_for_karma (object, options = {})
- #GuillaumeNM : 2009-01-30 Adding condition for SQLite3
- conditions = ["u.id = ? AND vote = ?" , self[:id] , true]
- joins = ["inner join votes v on #{object.table_name}.id = v.voteable_id", "inner join #{self.class.table_name} u on u.id = #{object.name.tableize}.#{self.class.name.foreign_key}"]
- { :joins => joins.join(" "), :conditions => conditions }.update(options)
- end
-
- end
-
end
+
end
end
diff --git a/lib/thumbs_up.rb b/lib/thumbs_up.rb
new file mode 100644
index 0000000..b1a738a
--- /dev/null
+++ b/lib/thumbs_up.rb
@@ -0,0 +1,38 @@
+require 'acts_as_voteable'
+require 'acts_as_voter'
+require 'has_karma'
+require 'thumbs_up/configuration'
+require 'thumbs_up/base'
+require 'thumbs_up/version'
+
+module ThumbsUp
+
+ class << self
+
+ # An ThumbsUp::Configuration object. Must act like a hash and return sensible
+ # values for all ThumbsUp::Configuration::OPTIONS. See ThumbsUp::Configuration.
+ attr_writer :configuration
+
+ # Call this method to modify defaults in your initializers.
+ #
+ # @example
+ # ThumbsUp.configure do |config|
+ # config.voteable_relationship_name = :votes_on
+ # config.voter_relationship_name = :votes_by
+ # end
+ def configure
+ yield(configuration)
+ end
+
+ # The configuration object.
+ # @see ThumbsUp::Configuration
+ def configuration
+ @configuration ||= Configuration.new
+ end
+ end
+
+end
+
+ActiveRecord::Base.send(:include, ThumbsUp::ActsAsVoteable)
+ActiveRecord::Base.send(:include, ThumbsUp::ActsAsVoter)
+ActiveRecord::Base.send(:include, ThumbsUp::Karma)
diff --git a/lib/thumbs_up/base.rb b/lib/thumbs_up/base.rb
new file mode 100644
index 0000000..4d570c4
--- /dev/null
+++ b/lib/thumbs_up/base.rb
@@ -0,0 +1,11 @@
+module ThumbsUp
+ module Base
+ def quoted_true
+ ActiveRecord::Base.connection.quoted_true
+ end
+
+ def quoted_false
+ ActiveRecord::Base.connection.quoted_false
+ end
+ end
+end
diff --git a/lib/thumbs_up/configuration.rb b/lib/thumbs_up/configuration.rb
new file mode 100644
index 0000000..cb3331f
--- /dev/null
+++ b/lib/thumbs_up/configuration.rb
@@ -0,0 +1,51 @@
+module ThumbsUp
+ class Configuration
+
+ OPTIONS = [:voteable_relationship_name, :voter_relationship_name].freeze
+
+ # Specify the name of the relationship from voted on things to voters.
+ # Default is votes
+ # In order to have a model that votes on itself,
+ # e.g. Users vote on Users,
+ # must change :voteable_relationship_name or :voter_relationship_name
+ # to a non-default value
+ attr_accessor :voteable_relationship_name
+
+ # Specify the name of the relationship from voters to voted on things
+ # Default is votes
+ # In order to have a model that votes on itself,
+ # e.g. Users vote on Users,
+ # must change :voteable_relationship_name or :voter_relationship_name
+ # to a non-default value
+ attr_accessor :voter_relationship_name
+
+ def initialize
+ # these defaults can be overridden in the ThumbsUp.config block
+ @voteable_relationship_name = :votes
+ @voter_relationship_name = :votes
+ end
+
+ # Allows config options to be read like a hash
+ #
+ # @param [Symbol] option Key for a given attribute
+ def [](option)
+ send(option)
+ end
+
+ # Returns a hash of all configurable options
+ def to_hash
+ OPTIONS.inject({}) do |hash, option|
+ hash[option.to_sym] = self.send(option)
+ hash
+ end
+ end
+
+ # Returns a hash of all configurable options merged with +hash+
+ #
+ # @param [Hash] hash A set of configuration options that will take precedence over the defaults
+ def merge(hash)
+ to_hash.merge(hash)
+ end
+
+ end
+end
diff --git a/lib/thumbs_up/version.rb b/lib/thumbs_up/version.rb
new file mode 100644
index 0000000..bf0fd50
--- /dev/null
+++ b/lib/thumbs_up/version.rb
@@ -0,0 +1,3 @@
+module ThumbsUp
+ VERSION = '0.6.10'
+end
\ No newline at end of file
diff --git a/lib/vote_fu.rb b/lib/vote_fu.rb
deleted file mode 100644
index fb61d4a..0000000
--- a/lib/vote_fu.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'acts_as_voteable'
-require 'acts_as_voter'
-require 'has_karma'
-
-ActiveRecord::Base.send(:include, Juixe::Acts::Voteable)
-ActiveRecord::Base.send(:include, PeteOnRails::Acts::Voter)
-ActiveRecord::Base.send(:include, PeteOnRails::VoteFu::Karma)
-RAILS_DEFAULT_LOGGER.info "** vote_fu: initialized properly."
diff --git a/rails/init.rb b/rails/init.rb
index 7cbe35b..6a72190 100644
--- a/rails/init.rb
+++ b/rails/init.rb
@@ -1,10 +1,10 @@
-RAILS_DEFAULT_LOGGER.info "** vote_fu: setting up load paths"
+RAILS_DEFAULT_LOGGER.info "** thumbs_up: setting up load paths **"
%w{ models controllers helpers }.each do |dir|
path = File.join(File.dirname(__FILE__) , 'lib', dir)
$LOAD_PATH << path
- ActiveSupport::Dependencies.load_paths << path
- ActiveSupport::Dependencies.load_once_paths.delete(path)
+ ActiveSupport::Dependencies.autoload_paths << path
+ ActiveSupport::Dependencies.autoload_once_paths.delete(path)
end
-require 'vote_fu'
\ No newline at end of file
+require 'thumbs_up'
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..be3ae18
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,190 @@
+require 'simplecov'
+require 'minitest/autorun'
+SimpleCov.start
+
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'active_record'
+
+config = {
+ :database => 'thumbs_up_test',
+ :username => 'test'
+}
+
+case ENV['DB']
+ when 'mysql'
+ config = {
+ :adapter => 'mysql2',
+ :database => 'thumbs_up_test',
+ :username => 'test',
+ :password => 'test',
+ :socket => '/tmp/mysql.sock'
+ }
+ if ENV['TRAVIS']
+ config = {
+ :adapter => 'mysql2',
+ :database => 'thumbs_up_test',
+ :username => 'root'
+ }
+ end
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.connection.drop_database(config[:database]) rescue nil
+ ActiveRecord::Base.connection.create_database(config[:database])
+ when 'postgres'
+ config = {
+ :adapter => 'postgresql',
+ :database => 'thumbs_up_test',
+ :username => 'test',
+ }
+ if ENV['TRAVIS']
+ config = {
+ :adapter => 'postgresql',
+ :database => 'thumbs_up_test',
+ :username => 'postgres',
+ }
+ end
+ ActiveRecord::Base.establish_connection(config.merge({ :database => 'postgres' }))
+ ActiveRecord::Base.connection.drop_database(config[:database])
+ ActiveRecord::Base.connection.create_database(config[:database])
+ when 'sqlite3'
+ config = {
+ :adapter => 'sqlite3',
+ :database => 'test.sqlite3',
+ :username => 'test',
+ }
+end
+
+ActiveRecord::Base.establish_connection(config)
+
+ActiveRecord::Migration.verbose = false
+
+ActiveRecord::Schema.define do
+ create_table :votes, :force => true do |t|
+ t.boolean :vote, :default => false
+ t.references :voteable, :polymorphic => true, :null => false
+ t.references :voter, :polymorphic => true
+ t.timestamps :null => false
+ end
+
+ add_index :votes, [:voter_id, :voter_type]
+ add_index :votes, [:voteable_id, :voteable_type]
+
+ # Comment out the line below to allow multiple votes per voter on a single entity.
+ add_index :votes, [:voter_id, :voter_type, :voteable_id, :voteable_type], :unique => true, :name => 'fk_one_vote_per_user_per_entity'
+
+ create_table :users, :force => true do |t|
+ t.string :name
+ t.timestamps :null => false
+ end
+
+ create_table :items, :force => true do |t|
+ t.integer :user_id
+ t.string :name
+ t.string :description
+ end
+
+ create_table :other_items, :force => true do |t|
+ t.integer :user_id
+ t.string :name
+ t.string :description
+ end
+
+ create_table :user_customs, :force => true do |t|
+ t.string :name
+ t.timestamps :null => false
+ end
+
+ create_table :item_customs, :force => true do |t|
+ t.integer :user_id
+ t.string :name
+ t.string :description
+ end
+
+ create_table :other_item_customs, :force => true do |t|
+ t.integer :user_id
+ t.string :name
+ t.string :description
+ end
+
+end
+
+require 'thumbs_up'
+
+class Vote < ActiveRecord::Base
+
+ scope :for_voter, lambda { |*args| where(["voter_id = ? AND voter_type = ?", args.first.id, args.first.class.name]) }
+ scope :for_voteable, lambda { |*args| where(["voteable_id = ? AND voteable_type = ?", args.first.id, args.first.class.name]) }
+ scope :recent, lambda { |*args| where(["created_at > ?", (args.first || 2.weeks.ago)]) }
+ scope :descending, lambda { order("created_at DESC") }
+
+ belongs_to :voteable, :polymorphic => true
+ belongs_to :voter, :polymorphic => true
+
+ attr_accessible :vote, :voter, :voteable if ActiveRecord::VERSION::MAJOR < 4
+
+ # Comment out the line below to allow multiple votes per user.
+ validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
+end
+
+class Item < ActiveRecord::Base
+ # This is default, however because the setting is app-wide, and changed elsewhere, we need to be explicit
+ ThumbsUp.configuration.voteable_relationship_name = :votes
+ ThumbsUp.configuration.voter_relationship_name = :votes
+ acts_as_voteable
+ belongs_to :user
+end
+
+class OtherItem < ActiveRecord::Base
+ # This is default, however because the setting is app-wide, and changed elsewhere, we need to be explicit
+ ThumbsUp.configuration.voteable_relationship_name = :votes
+ ThumbsUp.configuration.voter_relationship_name = :votes
+ acts_as_voteable
+ belongs_to :user
+end
+
+class User < ActiveRecord::Base
+ # This is default, however because the setting is app-wide, and changed elsewhere, we need to be explicit
+ ThumbsUp.configuration.voteable_relationship_name = :votes
+ ThumbsUp.configuration.voter_relationship_name = :votes
+ acts_as_voter
+ has_many :items
+ has_karma :items
+
+ def self.default_karma
+ self.karmic_objects = nil
+ has_karma :items, :weight => 1
+ end
+
+ def self.weighted_has_karma
+ self.karmic_objects = nil
+ has_karma :items, :weight => [ 10, 15 ]
+ end
+
+ def self.upvote_only_has_karma
+ self.karmic_objects = nil
+ has_karma :items, :weight => 1.3
+ end
+end
+
+class ItemCustom < ActiveRecord::Base
+ ThumbsUp.configuration.voteable_relationship_name = :votes_on
+ ThumbsUp.configuration.voter_relationship_name = :votes_by
+ acts_as_voteable
+ belongs_to :user
+end
+
+class OtherItemCustom < ActiveRecord::Base
+ ThumbsUp.configuration.voteable_relationship_name = :votes_on
+ ThumbsUp.configuration.voter_relationship_name = :votes_by
+ acts_as_voteable
+ belongs_to :user
+end
+
+class UserCustom < ActiveRecord::Base
+ ThumbsUp.configuration.voteable_relationship_name = :votes_on
+ ThumbsUp.configuration.voter_relationship_name = :votes_by
+ acts_as_voter
+ has_many :items
+ has_karma :items
+end
diff --git a/test/thumbs_up_test.rb b/test/thumbs_up_test.rb
new file mode 100644
index 0000000..ca8fc31
--- /dev/null
+++ b/test/thumbs_up_test.rb
@@ -0,0 +1,578 @@
+require File.join(File.expand_path(File.dirname(__FILE__)), 'test_helper')
+
+class TestThumbsUp < Minitest::Test
+ def setup
+ Vote.delete_all
+ User.delete_all
+ Item.delete_all
+ end
+
+ def test_acts_as_voter_instance_methods
+ # Because these are set in several places we need to ensure the defaults are set here.
+ ThumbsUp.configuration.voteable_relationship_name = :votes
+ ThumbsUp.configuration.voter_relationship_name = :votes
+
+ user_for = User.create(:name => 'david')
+ user_against = User.create(:name => 'brady')
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item2= Item.create(:name => 'PS3', :description => 'Playstation 3')
+
+ refute_nil user_for.vote_for(item)
+ assert_raises(ActiveRecord::RecordInvalid) do
+ user_for.vote_for(item)
+ end
+ assert_equal true, user_for.voted_for?(item)
+ assert_equal false, user_for.voted_against?(item)
+ assert_equal true, user_for.voted_on?(item)
+ assert_equal 1, user_for.vote_count
+ assert_equal 1, user_for.vote_count(:up)
+ assert_equal 0, user_for.vote_count(:down)
+ assert_equal true, user_for.voted_which_way?(item, :up)
+ assert_equal false, user_for.voted_which_way?(item, :down)
+ assert_equal true, user_for.voted_how?(item)
+ assert_equal 1, user_for.votes.where(:voteable_type => 'Item').count
+ assert_equal 0, user_for.votes.where(:voteable_type => 'AnotherItem').count
+ assert_raises(ArgumentError) do
+ user_for.voted_which_way?(item, :foo)
+ end
+
+ refute_nil user_against.vote_against(item)
+ assert_raises(ActiveRecord::RecordInvalid) do
+ user_against.vote_against(item)
+ end
+ assert_equal false, user_against.voted_for?(item)
+ assert_equal true, user_against.voted_against?(item)
+ assert_equal false, user_against.voted_how?(item)
+ assert_equal true, user_against.voted_on?(item)
+ assert_equal 1, user_against.vote_count
+ assert_equal 0, user_against.vote_count(:up)
+ assert_equal 1, user_against.vote_count(:down)
+ assert_equal false, user_against.voted_which_way?(item, :up)
+ assert_equal true, user_against.voted_which_way?(item, :down)
+ assert_raises(ArgumentError) do
+ user_against.voted_which_way?(item, :foo)
+ end
+
+ refute_nil user_against.vote_exclusively_for(item)
+ assert_equal true, user_against.voted_for?(item)
+
+ refute_nil user_for.vote_exclusively_against(item)
+ assert_equal true, user_for.voted_against?(item)
+
+ user_for.unvote_for(item)
+ assert_equal 0, user_for.vote_count
+
+ user_against.unvote_for(item)
+ assert_equal 0, user_against.vote_count
+
+ assert_raises(ArgumentError) do
+ user_for.vote(item, {:direction => :foo})
+ end
+
+ vote = user_against.vote(item, :exclusive => true, :direction => :down)
+ assert_equal true, user_against.voted_against?(item)
+ # Make sure the vote record was returned by the :vote method
+ assert_equal true, vote.is_a?(Vote)
+
+ vote = user_for.vote(item, :exclusive => true, :direction => :up)
+ assert_equal true, user_for.voted_for?(item)
+ # Make sure the vote record was returned by the :vote method
+ assert_equal true, vote.is_a?(Vote)
+
+ assert_nil user_for.voted_how?(item2)
+ end
+
+ def test_acts_as_voteable_instance_methods
+ # Because these are set in several places we need to ensure the defaults are set here.
+ ThumbsUp.configuration.voteable_relationship_name = :votes
+ ThumbsUp.configuration.voter_relationship_name = :votes
+
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+
+ assert_equal 0, item.ci_plusminus
+
+ user_for = User.create(:name => 'david')
+ another_user_for = User.create(:name => 'name')
+ user_against = User.create(:name => 'brady')
+ another_user_against = User.create(:name => 'name')
+
+ user_for.vote_for(item)
+ another_user_for.vote_for(item)
+ # Use #reload to force reloading of votes from the database,
+ # otherwise these tests fail after "assert_equal 0, item.ci_plusminus" caches
+ # the votes. We hack this as caching is the correct behavious, per-request,
+ # in production.
+ item.reload
+
+ assert_equal 2, item.votes_for
+ assert_equal 0, item.votes_against
+ assert_equal 2, item.plusminus
+ assert_in_delta 0.34, item.ci_plusminus, 0.01
+
+ user_against.vote_against(item)
+
+ assert_equal 1, item.votes_against
+ assert_equal 1, item.plusminus
+ assert_in_delta 0.20, item.ci_plusminus, 0.01
+
+ assert_equal 3, item.votes_count
+
+ assert_equal 67, item.percent_for
+ assert_equal 33, item.percent_against
+
+ voters_who_voted = item.voters_who_voted
+ assert_equal 3, voters_who_voted.size
+ assert voters_who_voted.include?(user_for)
+ assert voters_who_voted.include?(another_user_for)
+ assert voters_who_voted.include?(user_against)
+
+ voters_who_voted_for = item.voters_who_voted_for
+ assert_equal 2, voters_who_voted_for.size
+ assert voters_who_voted_for.include?(user_for)
+ assert voters_who_voted_for.include?(another_user_for)
+
+ another_user_against.vote_against(item)
+
+ voters_who_voted_against = item.voters_who_voted_against
+ assert_equal 2, voters_who_voted_against.size
+ assert voters_who_voted_against.include?(user_against)
+ assert voters_who_voted_against.include?(another_user_against)
+
+ non_voting_user = User.create(:name => 'random')
+
+ assert_equal true, item.voted_by?(user_for)
+ assert_equal true, item.voted_by?(another_user_for)
+ assert_equal true, item.voted_by?(user_against)
+ assert_equal false, item.voted_by?(non_voting_user)
+ end
+
+ def test_acts_as_voter_configuration
+ ThumbsUp.configuration.voteable_relationship_name = :votes_on
+ ThumbsUp.configuration.voter_relationship_name = :votes_by
+
+ user_for = UserCustom.create(:name => 'david')
+ user_against = UserCustom.create(:name => 'brady')
+ item = ItemCustom.create(:name => 'XBOX', :description => 'XBOX console')
+
+ # We have changed the name of the relationship, so `votes` is not defined.
+ assert_raises(NoMethodError) do
+ user_for.votes
+ end
+
+ refute_nil user_for.vote_for(item)
+ assert_raises(ActiveRecord::RecordInvalid) do
+ user_for.vote_for(item)
+ end
+ assert_equal true, user_for.voted_for?(item)
+ assert_equal false, user_for.voted_against?(item)
+ assert_equal true, user_for.voted_on?(item)
+ assert_equal 1, user_for.vote_count
+ assert_equal 1, user_for.vote_count(:up)
+ assert_equal 0, user_for.vote_count(:down)
+ assert_equal true, user_for.voted_which_way?(item, :up)
+ assert_equal false, user_for.voted_which_way?(item, :down)
+ assert_equal 1, user_for.votes_by.where(:voteable_type => 'ItemCustom').count
+ assert_equal 0, user_for.votes_by.where(:voteable_type => 'AnotherItem').count
+ assert_raises(ArgumentError) do
+ user_for.voted_which_way?(item, :foo)
+ end
+
+ refute_nil user_against.vote_against(item)
+ assert_raises(ActiveRecord::RecordInvalid) do
+ user_against.vote_against(item)
+ end
+ assert_equal false, user_against.voted_for?(item)
+ assert_equal true, user_against.voted_against?(item)
+ assert_equal true, user_against.voted_on?(item)
+ assert_equal 1, user_against.vote_count
+ assert_equal 0, user_against.vote_count(:up)
+ assert_equal 1, user_against.vote_count(:down)
+ assert_equal false, user_against.voted_which_way?(item, :up)
+ assert_equal true, user_against.voted_which_way?(item, :down)
+ assert_raises(ArgumentError) do
+ user_against.voted_which_way?(item, :foo)
+ end
+
+ refute_nil user_against.vote_exclusively_for(item)
+ assert_equal true, user_against.voted_for?(item)
+
+ refute_nil user_for.vote_exclusively_against(item)
+ assert_equal true, user_for.voted_against?(item)
+
+ user_for.unvote_for(item)
+ assert_equal 0, user_for.vote_count
+
+ user_against.unvote_for(item)
+ assert_equal 0, user_against.vote_count
+
+ assert_raises(ArgumentError) do
+ user_for.vote(item, {:direction => :foo})
+ end
+ end
+
+ def test_acts_as_voteable_configuration
+ ThumbsUp.configuration.voteable_relationship_name = :votes_on
+ ThumbsUp.configuration.voter_relationship_name = :votes_by
+
+ item = ItemCustom.create(:name => 'XBOX', :description => 'XBOX console')
+
+ assert_equal 0, item.ci_plusminus
+
+ user_for = UserCustom.create(:name => 'david')
+ another_user_for = UserCustom.create(:name => 'name')
+ user_against = UserCustom.create(:name => 'brady')
+ another_user_against = UserCustom.create(:name => 'name')
+
+ # We have changed the name of the relationship, so `votes` is not defined.
+ assert_raises(NoMethodError) do
+ item.votes
+ end
+
+ user_for.vote_for(item)
+ another_user_for.vote_for(item)
+ # Use #reload to force reloading of votes from the database,
+ # otherwise these tests fail after "assert_equal 0, item.ci_plusminus" caches
+ # the votes. We hack this as caching is the correct behavious, per-request,
+ # in production.
+ item.reload
+
+ assert_equal 2, item.votes_for
+ assert_equal 0, item.votes_against
+ assert_equal 2, item.plusminus
+ assert_in_delta 0.34, item.ci_plusminus, 0.01
+
+ user_against.vote_against(item)
+
+ assert_equal 1, item.votes_against
+ assert_equal 1, item.plusminus
+ assert_in_delta 0.20, item.ci_plusminus, 0.01
+
+ assert_equal 3, item.votes_count
+
+ assert_equal 67, item.percent_for
+ assert_equal 33, item.percent_against
+
+ voters_who_voted = item.voters_who_voted
+ assert_equal 3, voters_who_voted.size
+ assert voters_who_voted.include?(user_for)
+ assert voters_who_voted.include?(another_user_for)
+ assert voters_who_voted.include?(user_against)
+
+ voters_who_voted_for = item.voters_who_voted_for
+ assert_equal 2, voters_who_voted_for.size
+ assert voters_who_voted_for.include?(user_for)
+ assert voters_who_voted_for.include?(another_user_for)
+
+ another_user_against.vote_against(item)
+
+ voters_who_voted_against = item.voters_who_voted_against
+ assert_equal 2, voters_who_voted_against.size
+ assert voters_who_voted_against.include?(user_against)
+ assert voters_who_voted_against.include?(another_user_against)
+
+ non_voting_user = UserCustom.create(:name => 'voteable_configuration')
+
+ assert_equal true, item.voted_by?(user_for)
+ assert_equal true, item.voted_by?(another_user_for)
+ assert_equal true, item.voted_by?(user_against)
+ assert_equal false, item.voted_by?(non_voting_user)
+ end
+
+ # Duplicated method name, why?
+ def test_tally_empty
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ # COUNT(#{Vote.table_name}.id) is equivalent to aliased column `vote_count` - Postgres
+ # requires the non-aliased name in a HAVING clause.
+ assert_equal 0, Item.tally.having("COUNT(#{Vote.table_name}.id) > 0").length
+ end
+
+ def test_tally_has_id
+ item1 = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item2 = Item.create(:name => 'XBOX2', :description => 'XBOX2 console')
+ user = User.create(:name => 'david')
+
+ user.vote_for(item2)
+
+ refute_nil Item.tally.first.id
+ end
+
+ def test_tally_starts_at
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ user = User.create(:name => 'david')
+
+ vote = user.vote_for(item)
+ vote.created_at = 3.days.ago
+ vote.save
+
+ assert_equal 0, Item.tally.where('created_at > ?', 2.days.ago).length
+ assert_equal 1, Item.tally.where('created_at > ?', 4.days.ago).length
+ end
+
+ def test_tally_end_at
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ user = User.create(:name => 'david')
+
+ vote = user.vote_for(item)
+ vote.created_at = 3.days.from_now
+ vote.save
+
+ assert_equal 0, Item.tally.where('created_at < ?', 2.days.from_now).length
+ assert_equal 1, Item.tally.where('created_at < ?', 4.days.from_now).length
+ end
+
+ def test_tally_between_start_at_end_at
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ another_item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ user = User.create(:name => 'david')
+
+ vote = user.vote_for(item)
+ vote.created_at = 2.days.ago
+ vote.save
+
+ vote = user.vote_for(another_item)
+ vote.created_at = 3.days.from_now
+ vote.save
+
+ assert_equal 1, Item.tally.where('created_at > ?', 3.days.ago).where('created_at < ?', 2.days.from_now).length
+ assert_equal 2, Item.tally.where('created_at > ?', 3.days.ago).where('created_at < ?', 4.days.from_now).length
+ end
+
+ def test_tally_count
+ Item.tally.except(:order).to_a.count
+ end
+
+ def test_tally_any
+ Item.tally.except(:order).any?
+ end
+
+ def test_plusminus_tally_not_empty_without_conditions
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ assert_equal 1, Item.plusminus_tally.length
+ end
+
+ def test_plusminus_tally_empty
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ # COUNT(#{Vote.table_name}.id) is equivalent to aliased column `vote_count` - Postgres
+ # requires the non-aliased name in a HAVING clause.
+ assert_equal 0, Item.plusminus_tally.having("COUNT(#{Vote.table_name}.id) > 0").length
+ end
+
+ def test_plusminus_tally_starts_at
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ user = User.create(:name => 'david')
+
+ vote = user.vote_for(item)
+ vote.created_at = 3.days.ago
+ vote.save
+
+ assert_equal 0, Item.plusminus_tally.where('created_at > ?', 2.days.ago).length
+ assert_equal 1, Item.plusminus_tally.where('created_at > ?', 4.days.ago).length
+ end
+
+ def test_plusminus_tally_end_at
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ user = User.create(:name => 'david')
+
+ vote = user.vote_for(item)
+ vote.created_at = 3.days.from_now
+ vote.save
+
+ assert_equal 0, Item.plusminus_tally.where('created_at < ?', 2.days.from_now).length
+ assert_equal 1, Item.plusminus_tally.where('created_at < ?', 4.days.from_now).length
+ end
+
+ def test_plusminus_tally_between_start_at_end_at
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ another_item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ user = User.create(:name => 'david')
+
+ vote = user.vote_for(item)
+ vote.created_at = 2.days.ago
+ vote.save
+
+ vote = user.vote_for(another_item)
+ vote.created_at = 3.days.from_now
+ vote.save
+
+ assert_equal 1, Item.plusminus_tally.where('created_at > ?', 3.days.ago).where('created_at < ?', 2.days.from_now).length
+ assert_equal 2, Item.plusminus_tally.where('created_at > ?', 3.days.ago).where('created_at < ?', 4.days.from_now).length
+ end
+
+ def test_plusminus_tally_inclusion
+ user = User.create(:name => 'david')
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item_not_included = Item.create(:name => 'Playstation', :description => 'Playstation console')
+
+ refute_nil user.vote_for(item)
+
+ if ActiveRecord::Base.connection.adapter_name == 'MySQL'
+ assert(Item.plusminus_tally.having('vote_count > 0').include?(item))
+ assert(!Item.plusminus_tally.having('vote_count > 0').include?(item_not_included))
+ else
+ assert(Item.plusminus_tally.having('COUNT(votes.id) > 0').include?(item))
+ assert(!Item.plusminus_tally.having('COUNT(votes.id) > 0').include?(item_not_included))
+ end
+ end
+
+ def test_plusminus_tally_up
+ user = User.create(:name => 'david')
+ item1 = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item2 = Item.create(:name => 'Playstation', :description => 'Playstation console')
+ item3 = Item.create(:name => 'Wii', :description => 'Wii console')
+
+ refute_nil user.vote_for(item1)
+ refute_nil user.vote_against(item2)
+
+ assert_equal [1, 0, 0], Item.plusminus_tally(:separate_updown => true).map(&:up).map(&:to_i)
+ end
+
+ def test_plusminus_tally_down
+ user = User.create(:name => 'david')
+ item1 = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item2 = Item.create(:name => 'Playstation', :description => 'Playstation console')
+ item3 = Item.create(:name => 'Wii', :description => 'Wii console')
+
+ refute_nil user.vote_for(item1)
+ refute_nil user.vote_against(item2)
+
+ assert_equal [0, 0, 1], Item.plusminus_tally(:separate_updown => true).map(&:down).map(&:to_i)
+ end
+
+ def test_plusminus_tally_vote_count
+ user = User.create(:name => 'david')
+ item1 = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item2 = Item.create(:name => 'Playstation', :description => 'Playstation console')
+ item3 = Item.create(:name => 'Wii', :description => 'Wii console')
+
+ refute_nil user.vote_for(item1)
+ refute_nil user.vote_against(item2)
+
+ assert_equal [1, 0, -1], Item.plusminus_tally.map(&:plusminus_tally).map(&:to_i)
+ end
+
+ def test_plusminus_tally_voting_for
+ user1 = User.create(:name => 'david')
+ item = Item.create(:name => 'Playstation', :description => 'Playstation console')
+
+ refute_nil user1.vote_for(item)
+
+ # https://github.com/rails/rails/issues/1718
+ assert_equal 1, Item.plusminus_tally[0].vote_count.to_i
+ assert_equal 1, Item.plusminus_tally[0].plusminus.to_i
+ end
+
+ def test_plusminus_tally_voting_against
+ user1 = User.create(:name => 'david')
+ user2 = User.create(:name => 'john')
+ item = Item.create(:name => 'Playstation', :description => 'Playstation console')
+
+ refute_nil user1.vote_against(item)
+ refute_nil user2.vote_against(item)
+
+ # https://github.com/rails/rails/issues/1718
+ assert_equal 2, Item.plusminus_tally[0].vote_count.to_i
+ assert_equal -2, Item.plusminus_tally[0].plusminus.to_i
+ end
+
+ def test_plusminus_tally_default_ordering
+ user1 = User.create(:name => 'david')
+ user2 = User.create(:name => 'john')
+ item_twice_for = Item.create(:name => 'XBOX2', :description => 'XBOX2 console')
+ item_for = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item_against = Item.create(:name => 'Playstation', :description => 'Playstation console')
+
+ refute_nil user1.vote_for(item_for)
+ refute_nil user1.vote_for(item_twice_for)
+ refute_nil user2.vote_for(item_twice_for)
+ refute_nil user1.vote_against(item_against)
+
+ assert_equal item_twice_for, Item.plusminus_tally[0]
+ assert_equal item_for, Item.plusminus_tally[1]
+ assert_equal item_against, Item.plusminus_tally[2]
+ end
+
+ def test_plusminus_tally_limit
+ users = (0..9).map{ |u| User.create(:name => "User #{u}") }
+ items = (0..9).map{ |u| Item.create(:name => "Item #{u}", :description => "Item #{u}") }
+ users.each{ |u| items.each { |i| u.vote_for(i) } }
+ assert_equal 10, Item.plusminus_tally.length
+ assert_equal 2, Item.plusminus_tally.limit(2).length
+ end
+
+ def test_plusminus_tally_ascending_ordering
+ user = User.create(:name => 'david')
+ item_for = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ item_against = Item.create(:name => 'Playstation', :description => 'Playstation console')
+
+ refute_nil user.vote_for(item_for)
+ refute_nil user.vote_against(item_against)
+
+ assert_equal item_for, Item.plusminus_tally.reorder('plusminus_tally ASC')[1]
+ assert_equal item_against, Item.plusminus_tally.reorder('plusminus_tally ASC')[0]
+ end
+
+ def test_plusminus_tally_limit_with_where_and_having
+ users = (0..9).map{ |u| User.create(:name => "User #{u}") }
+ items = (0..9).map{ |u| Item.create(:name => "Item #{u}", :description => "Item #{u}") }
+ users.each{ |u| items[0..8].each { |i| u.vote_for(i) } }
+
+ # Postgresql doesn't accept aliases in HAVING clauses, so you'll need to copy and paste the whole statement from the #plusminus_tally method if you want to use HAVING('plusminus_tally > 10'), for example.
+ assert_equal 0, Item.plusminus_tally.limit(5).where('created_at > ?', 2.days.ago).having("SUM(CASE #{Vote.table_name}.vote WHEN #{ActiveRecord::Base.connection.quoted_true} THEN 1 WHEN #{ActiveRecord::Base.connection.quoted_false} THEN -1 ELSE 0 END) > 10").length
+ assert_equal 5, Item.plusminus_tally.limit(5).where('created_at > ?', 2.days.ago).having("SUM(CASE #{Vote.table_name}.vote WHEN #{ActiveRecord::Base.connection.quoted_true} THEN 1 WHEN #{ActiveRecord::Base.connection.quoted_false} THEN -1 ELSE 0 END) > 9").length
+ assert_equal 9, Item.plusminus_tally.limit(10).where('created_at > ?', 2.days.ago).having("SUM(CASE #{Vote.table_name}.vote WHEN #{ActiveRecord::Base.connection.quoted_true} THEN 1 WHEN #{ActiveRecord::Base.connection.quoted_false} THEN -1 ELSE 0 END) > 9").length
+ assert_equal 0, Item.plusminus_tally.limit(10).where('created_at > ?', 1.day.from_now).having("SUM(CASE #{Vote.table_name}.vote WHEN #{ActiveRecord::Base.connection.quoted_true} THEN 1 WHEN #{ActiveRecord::Base.connection.quoted_false} THEN -1 ELSE 0 END) > 9").length
+ end
+
+ def test_plusminus_tally_count
+ Item.plusminus_tally.except(:order).to_a.count
+ end
+
+ def test_plusminus_tally_any
+ Item.plusminus_tally.except(:order).any?
+ end
+
+ def test_karma
+ User.default_karma
+ users = (0..1).map{ |u| User.create(:name => "User #{u}") }
+ items = (0..1).map{ |u| users[0].items.create(:name => "Item #{u}", :description => "Item #{u}") }
+ users.each{ |u| items.each { |i| u.vote_for(i) } }
+
+ assert_equal 4, users[0].karma
+ assert_equal 0, users[1].karma
+ end
+
+ def test_karma_with_upvote_weights
+ User.upvote_only_has_karma
+ users = (0..1).map{ |u| User.create(:name => "User #{u}") }
+ items = (0..1).map{ |u| users[0].items.create(:name => "Item #{u}", :description => "Item #{u}") }
+ users.each{ |u| items.each { |i| u.vote_for(i) } }
+
+ assert_equal (4 * 1.3).round, users[0].karma
+ assert_equal 0, users[1].karma
+ end
+
+ def test_karma_with_both_upvote_and_downvote_weights
+ User.weighted_has_karma
+ for_users = (0..1).map{ |u| User.create(:name => "For User #{u}") }
+ against_users = (0..2).map{ |u| User.create(:name => "Against User #{u}") }
+ items = (0..1).map{ |u| for_users[0].items.create(:name => "Item #{u}", :description => "Item #{u}") }
+ for_users.each{ |u| items.each { |i| u.vote_for(i) } }
+ against_users.each{ |u| items.each { |i| u.vote_against(i) } }
+
+ assert_equal 2 * (10 * 2 - 15 * 3).round, for_users[0].karma
+ assert_equal 0, for_users[1].karma
+ end
+
+ def test_plusminus_tally_scopes_by_voteable_type
+ user = User.create(:name => 'david')
+ item = Item.create(:name => 'XBOX', :description => 'XBOX console')
+ another_item = OtherItem.create(:name => 'Playstation', :description => 'Playstation console')
+
+ user.vote_for(item)
+ user.vote_for(another_item)
+
+ assert_equal 1, Item.plusminus_tally.to_a.sum(&:plusminus_tally).to_i
+ assert_equal 1, OtherItem.plusminus_tally.to_a.sum(&:plusminus_tally).to_i
+ end
+
+end
diff --git a/test/vote_fu_test.rb b/test/vote_fu_test.rb
deleted file mode 100644
index 83bc088..0000000
--- a/test/vote_fu_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'test/unit'
-
-class VoteFuTest < Test::Unit::TestCase
- # Replace this with your real tests.
- def test_this_plugin
- flunk
- end
-end
diff --git a/thumbs_up.gemspec b/thumbs_up.gemspec
new file mode 100644
index 0000000..7b07fbd
--- /dev/null
+++ b/thumbs_up.gemspec
@@ -0,0 +1,31 @@
+# -*- encoding: utf-8 -*-
+
+lib = File.expand_path('../lib', __FILE__)
+$:.unshift lib unless $:.include?(lib)
+require 'thumbs_up/version'
+
+Gem::Specification.new do |s|
+ s.name = 'thumbs_up'
+ s.version = ThumbsUp::VERSION
+
+ s.homepage = 'http://github.com/bouchard/thumbs_up'
+ s.license = 'MIT'
+ s.summary = 'Voting for ActiveRecord with multiple vote sources and karma calculation.'
+ s.description = 'ThumbsUp provides dead-simple voting capabilities to ActiveRecord models with karma calculation, a la stackoverflow.com.'
+ s.authors = ['Brady Bouchard', 'Peter Jackson', 'Cosmin Radoi', 'Bence Nagy', 'Rob Maddox', 'Wojciech Wnetrzak']
+ s.email = ['brady@thewellinspired.com']
+ s.files = Dir.glob('{lib,rails,test}/**/*') + %w(CHANGELOG.md Gemfile LICENSE README.md Rakefile)
+ s.require_paths = ['lib']
+
+ s.add_runtime_dependency('activerecord', '> 4.2', '< 7')
+ s.add_runtime_dependency('statistics2')
+ s.add_development_dependency('minitest')
+ s.add_development_dependency('simplecov')
+ s.add_development_dependency('bundler')
+ s.add_development_dependency('mysql2', '> 0.3.20')
+ s.add_development_dependency('pg')
+ s.add_development_dependency('sqlite3')
+ s.add_development_dependency('rake')
+
+end
+