Skip to content

Commit

Permalink
MiniTest::Spec, anyone?
Browse files Browse the repository at this point in the history
  • Loading branch information
David Celis committed Jan 28, 2012
1 parent 2108d90 commit d259863
Show file tree
Hide file tree
Showing 67 changed files with 585 additions and 99 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -54,3 +54,4 @@ tmtags

# For rubinius:
#*.rbc
.rake_tasks*
2 changes: 1 addition & 1 deletion Gemfile
Expand Up @@ -8,7 +8,7 @@ gem "resque-loner", "~> 1.2.0"
# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
gem "mysql2"
gem "sqlite3"
gem "minitest"
gem "shoulda"
gem "yard", "~> 0.6.0"
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Expand Up @@ -47,7 +47,6 @@ GEM
mime-types (1.17.2)
minitest (2.10.1)
multi_json (1.0.4)
mysql2 (0.3.11)
polyglot (0.3.3)
rack (1.4.0)
rack-cache (1.1)
Expand Down Expand Up @@ -96,6 +95,7 @@ GEM
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.5)
thor (0.14.6)
tilt (1.3.3)
treetop (1.4.10)
Expand All @@ -113,11 +113,11 @@ DEPENDENCIES
bundler (~> 1.0.0)
jeweler (~> 1.6.4)
minitest
mysql2
rails (>= 3.1.0)
rcov
redis (~> 2.2.0)
resque (~> 1.19.0)
resque-loner (~> 1.2.0)
shoulda
sqlite3
yard (~> 0.6.0)
4 changes: 2 additions & 2 deletions Rakefile
Expand Up @@ -30,8 +30,8 @@ Jeweler::RubygemsDotOrgTasks.new

require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.libs << 'lib' << 'spec'
test.pattern = 'spec/**/*_spec.rb'
test.verbose = true
end

Expand Down
5 changes: 1 addition & 4 deletions TODO
@@ -1,13 +1,10 @@
=== TODO

* Allow the option NOT to queue up on like/dislike/ignore?
* Ensure correct privacy levels for methods
* Write some fucking tests you poor excuse for a TDD
* Documentation
* A good README
* Code cleanup in RecommendationMethods
* Magic method_missing bullshit for Like/Dislike/IgnoreMethods modules

=== Next update

* Allow the option of common_likes_with and common_dislikes_with to return the actual objects so users get more use out of this
* Allow the option NOT to queue up on like/dislike/ignore?
4 changes: 4 additions & 0 deletions lib/generators/recommendable/templates/initializer.rb
Expand Up @@ -12,3 +12,7 @@

# Connect to Redis via a UNIX socket instead
<% unless options.redis_socket %># <% end %>Recommendable.redis = Redis.new(:sock => "<%= options.redis_socket %>")

# Tell Redis which database to use (usually between 0 and 15). The default of 0
# is most likely okay unless you have another application using that database.
Recommendable.redis.select "0"
1 change: 1 addition & 0 deletions lib/recommendable.rb
@@ -1,4 +1,5 @@
require 'recommendable/engine'
require 'recommendable/version'
require 'recommendable/acts_as_recommended_to'
require 'recommendable/acts_as_recommendable'
require 'recommendable/exceptions'
Expand Down
4 changes: 4 additions & 0 deletions lib/recommendable/acts_as_recommendable.rb
Expand Up @@ -18,6 +18,10 @@ def acts_as_recommendable
include LikeableMethods
include DislikeableMethods

def has_been_rated?
likes.count + dislikes.count > 0
end

def create_recommendable_sets
[create_liked_by_set, create_disliked_by_set]
end
Expand Down
53 changes: 31 additions & 22 deletions lib/recommendable/acts_as_recommended_to.rb
Expand Up @@ -34,6 +34,7 @@ module LikeMethods
# object's model to `act_as_recommendable`
def like(object)
raise RecordNotRecommendableError unless Recommendable.recommendable_classes.include?(object.class)
return if likes?(object)
undislike(object) if dislikes?(object)
unpredict(object)
likes.create!(:likeable_id => object.id, :likeable_type => object.class.to_s)
Expand Down Expand Up @@ -101,6 +102,7 @@ module DislikeMethods
# object's model to `act_as_recommendable`
def dislike(object)
raise RecordNotRecommendableError unless Recommendable.recommendable_classes.include?(object.class)
return if dislikes?(object)
unlike(object) if likes?(object)
unpredict(object)
dislikes.create!(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s)
Expand Down Expand Up @@ -169,6 +171,7 @@ module IgnoreMethods
# object's model to `act_as_recommendable`
def ignore(object)
raise RecordNotRecommendableError unless Recommendable.recommendable_classes.include?(object.class)
return if has_ignored?(object)
unlike(object) if likes?(object) || undislike(object) if dislikes?(object)
unpredict(object)
ignores.create!(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
Expand Down Expand Up @@ -257,10 +260,10 @@ def has_rated_anything?
# `rater` has several likes/dislikes that `current_user` does not.
def similarity_with(rater)
rater.create_recommended_to_sets
agreements = common_likes_with(rater).size + common_dislikes(rater).size
agreements = common_likes_with(rater).size + common_dislikes_with(rater).size
disagreements = disagreements_with(rater).size

similarity = (agreements - disagreements).to_f / (likes.count + dislikes)
similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
rater.destroy_recommended_to_sets

return similarity
Expand Down Expand Up @@ -348,12 +351,17 @@ def disagreements_with(rater, options = {})
# @param [Hash] options the options for this query
# @option options [Fixnum] :count (10) The number of raters to return
# @return [Array] An array of instances of your user class
def similar_raters(options)
def similar_raters(options = {})
defaults = { :count => 10 }
options = defaults.merge(options)

rater_ids = Recommendable.redis.zrevrange similarity_set, 0, options[:count] - 1
Recommendable.user_class.find rater_ids, order: "field(id, #{ids.join(',')})"
rater_ids = Recommendable.redis.zrevrange(similarity_set, 0, options[:count] - 1).map!(&:to_i)
raters = Recommendable.user_class.where("ID IN (?)", rater_ids)

# The query loses the ordering, so...
return raters.sort do |x, y|
rater_ids.index(x.id) <=> rater_ids.index(y.id)
end
end

# Used internally to update the similarity values between `self` and all
Expand All @@ -365,12 +373,10 @@ def update_similarities
self.create_recommended_to_sets

Recommendable.user_class.find_each do |rater|
rater.create_recommended_to_sets
next unless things_can_be_recommended_to?(rater) && self != rater

Recommendable.redis.zadd similarity_set, similarity_with(rater), "#{rater.id}"
Recommendable.redis.zadd rater.similarity_set, rater.similarity_with(self), "#{id}"
rater.destroy_recommended_to_sets
end

self.destroy_recommended_to_sets
Expand All @@ -383,7 +389,7 @@ def update_similarities
# @note Do not call this method directly. Seriously, don't do it.
def update_recommendations
Recommendable.recommendable_classes.each do |klass|
update_predictions_for(klass)
update_recommendations_for(klass)
end
end

Expand All @@ -394,9 +400,9 @@ def update_recommendations
# @note Do not call this method directly. Seriously, don't do it.
def update_recommendations_for(klass)
klass.find_each do |object|
unless has_rated?(object)
unless has_rated?(object) || !object.has_been_rated?
prediction = predict(object)
Recommendable.redis.zadd predictions_set_for(object.class), prediction, "#{object.class}:#{object.id}" if prediction
Recommendable.redis.zadd(predictions_set_for(object.class), prediction, "#{object.class}:#{object.id}") if prediction
end
end
end
Expand All @@ -417,11 +423,12 @@ def recommendations(options = {})
return [] if likes.count + dislikes.count == 0 || Recommendable.redis.zcard(unioned_predictions) == 0

recommendations = Recommendable.redis.zrevrange(unioned_predictions, 0, options[:count]).map do |object|
object.klass.find(object.likeable_id)
klass, id = object.split(":")
klass.constantize.find(id)
end

Recommendable.redis.del unioned_predictions
recommendations
return recommendations
end

# Get a list of recommendations for `self` on a single recommendable type.
Expand All @@ -439,12 +446,16 @@ def recommendations_for(klass, options = {})
options = defaults.merge options

recommendations = []
return recommendations if likes_for(klass).count + dislikes_for(klass).count == 0 || Recommendable.redis.zcard(predictions_set_for(object.class)) == 0
return recommendations if likes_for(klass).count + dislikes_for(klass).count == 0 || Recommendable.redis.zcard(predictions_set_for(klass)) == 0

until predictions.size == options[:count]
i += 1
object = klassify(klass).find(Recommendable.redis.zrevrange(predictions_set_for(object.class), i, i).first.split(":")[1])
i = 0
until recommendations.size == options[:count]
prediction = Recommendable.redis.zrevrange(predictions_set_for(klass), i, i).first
return recommendations unless prediction # User might not have enough recommendations to return

object = klassify(klass).find(prediction.split(":")[1])
recommendations << object unless has_ignored?(object)
i += 1
end

return recommendations
Expand All @@ -465,10 +476,10 @@ def predict(object)
sum = 0.0
prediction = 0.0

Recommendable.redis.smembers(liked_by).inject(sum) {|r, sum| sum += Recommendable.redis.zscore(similarity_set, r) }
Recommendable.redis.smembers(disliked_by).inject(sum) {|r, sum| sum -= Recommendable.redis.zscore(similarity_set, r) }
Recommendable.redis.smembers(liked_by).inject(sum) {|sum, r| sum += Recommendable.redis.zscore(similarity_set, r).to_f }
Recommendable.redis.smembers(disliked_by).inject(sum) {|sum, r| sum -= Recommendable.redis.zscore(similarity_set, r).to_f }

prediction = similarity_sum / rated_by.to_f
prediction = sum / rated_by.to_f

object.destroy_recommendable_sets

Expand All @@ -493,8 +504,6 @@ def probability_of_disliking(object)
-probability_of_liking(object)
end

private

def likes_set_for(klass)
"#{self.class}:#{id}:likes:#{klass}"
end
Expand All @@ -516,7 +525,7 @@ def unpredict(object)
end

def create_recommended_to_sets
Recommendable.recommendable_class.each do |klass|
Recommendable.recommendable_classes.each do |klass|
likes_for(klass).each {|like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
dislikes_for(klass).each {|dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
end
Expand Down
3 changes: 3 additions & 0 deletions lib/recommendable/version.rb
@@ -0,0 +1,3 @@
module Recommendable
VERSION = '0.0.1'
end
70 changes: 35 additions & 35 deletions recommendable.gemspec
Expand Up @@ -52,38 +52,38 @@ Gem::Specification.new do |s|
"lib/tasks/recommendable_tasks.rake",
"recommendable.gemspec",
"script/rails",
"test/dummy/README.rdoc",
"test/dummy/Rakefile",
"test/dummy/app/assets/javascripts/application.js",
"test/dummy/app/assets/stylesheets/application.css",
"test/dummy/app/controllers/application_controller.rb",
"test/dummy/app/helpers/application_helper.rb",
"test/dummy/app/mailers/.gitkeep",
"test/dummy/app/models/.gitkeep",
"test/dummy/app/views/layouts/application.html.erb",
"test/dummy/config.ru",
"test/dummy/config/application.rb",
"test/dummy/config/boot.rb",
"test/dummy/config/database.yml",
"test/dummy/config/environment.rb",
"test/dummy/config/environments/development.rb",
"test/dummy/config/environments/production.rb",
"test/dummy/config/environments/test.rb",
"test/dummy/config/initializers/backtrace_silencers.rb",
"test/dummy/config/initializers/inflections.rb",
"test/dummy/config/initializers/mime_types.rb",
"test/dummy/config/initializers/secret_token.rb",
"test/dummy/config/initializers/session_store.rb",
"test/dummy/config/initializers/wrap_parameters.rb",
"test/dummy/config/locales/en.yml",
"test/dummy/config/routes.rb",
"test/dummy/lib/assets/.gitkeep",
"test/dummy/log/.gitkeep",
"test/dummy/public/404.html",
"test/dummy/public/422.html",
"test/dummy/public/500.html",
"test/dummy/public/favicon.ico",
"test/dummy/script/rails",
"spec/dummy/README.rdoc",
"spec/dummy/Rakefile",
"spec/dummy/app/assets/javascripts/application.js",
"spec/dummy/app/assets/stylesheets/application.css",
"spec/dummy/app/controllers/application_controller.rb",
"spec/dummy/app/helpers/application_helper.rb",
"spec/dummy/app/mailers/.gitkeep",
"spec/dummy/app/models/.gitkeep",
"spec/dummy/app/views/layouts/application.html.erb",
"spec/dummy/config.ru",
"spec/dummy/config/application.rb",
"spec/dummy/config/boot.rb",
"spec/dummy/config/database.yml",
"spec/dummy/config/environment.rb",
"spec/dummy/config/environments/development.rb",
"spec/dummy/config/environments/production.rb",
"spec/dummy/config/environments/test.rb",
"spec/dummy/config/initializers/backtrace_silencers.rb",
"spec/dummy/config/initializers/inflections.rb",
"spec/dummy/config/initializers/mime_types.rb",
"spec/dummy/config/initializers/secret_token.rb",
"spec/dummy/config/initializers/session_store.rb",
"spec/dummy/config/initializers/wrap_parameters.rb",
"spec/dummy/config/locales/en.yml",
"spec/dummy/config/routes.rb",
"spec/dummy/lib/assets/.gitkeep",
"spec/dummy/log/.gitkeep",
"spec/dummy/public/404.html",
"spec/dummy/public/422.html",
"spec/dummy/public/500.html",
"spec/dummy/public/favicon.ico",
"spec/dummy/script/rails",
"test/integration/navigation_test.rb",
"test/recommendable_test.rb",
"test/test_helper.rb"
Expand All @@ -102,8 +102,8 @@ Gem::Specification.new do |s|
s.add_runtime_dependency(%q<redis>, ["~> 2.2.0"])
s.add_runtime_dependency(%q<resque>, ["~> 1.19.0"])
s.add_runtime_dependency(%q<resque-loner>, ["~> 1.2.0"])
s.add_development_dependency(%q<mysql2>, [">= 0"])
s.add_development_dependency(%q<minitest>, [">= 0"])
s.add_development_dependency(%q<minitest-rails>, [">= 0"])
s.add_development_dependency(%q<shoulda>, [">= 0"])
s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
Expand All @@ -114,8 +114,8 @@ Gem::Specification.new do |s|
s.add_dependency(%q<redis>, ["~> 2.2.0"])
s.add_dependency(%q<resque>, ["~> 1.19.0"])
s.add_dependency(%q<resque-loner>, ["~> 1.2.0"])
s.add_dependency(%q<mysql2>, [">= 0"])
s.add_dependency(%q<minitest>, [">= 0"])
s.add_dependency(%q<minitest-rails>, [">= 0"])
s.add_dependency(%q<shoulda>, [">= 0"])
s.add_dependency(%q<yard>, ["~> 0.6.0"])
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
Expand All @@ -127,8 +127,8 @@ Gem::Specification.new do |s|
s.add_dependency(%q<redis>, ["~> 2.2.0"])
s.add_dependency(%q<resque>, ["~> 1.19.0"])
s.add_dependency(%q<resque-loner>, ["~> 1.2.0"])
s.add_dependency(%q<mysql2>, [">= 0"])
s.add_dependency(%q<minitest>, [">= 0"])
s.add_dependency(%q<minitest-rails>, [">= 0"])
s.add_dependency(%q<shoulda>, [">= 0"])
s.add_dependency(%q<yard>, ["~> 0.6.0"])
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
Expand Down
9 changes: 9 additions & 0 deletions spec/configuration_spec.rb
@@ -0,0 +1,9 @@
require 'spec_helper'

class ConfigurationSpec < MiniTest::Spec
describe Redis do
it "should connect successfuly" do
Recommendable.redis.ping.must_equal "PONG"
end
end
end
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions spec/dummy/app/models/bully.rb
@@ -0,0 +1,2 @@
class Bully < ActiveRecord::Base
end
3 changes: 3 additions & 0 deletions spec/dummy/app/models/movie.rb
@@ -0,0 +1,3 @@
class Movie < ActiveRecord::Base
acts_as_recommendable
end
2 changes: 2 additions & 0 deletions spec/dummy/app/models/php_framework.rb
@@ -0,0 +1,2 @@
class PhpFramework < ActiveRecord::Base
end
3 changes: 3 additions & 0 deletions spec/dummy/app/models/user.rb
@@ -0,0 +1,3 @@
class User < ActiveRecord::Base
acts_as_recommended_to
end
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit d259863

Please sign in to comment.