recommendable 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/.gitignore +57 -0
  2. data/.travis.yml +3 -0
  3. data/Gemfile +17 -0
  4. data/Gemfile.lock +118 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.markdown +340 -0
  7. data/Rakefile +26 -0
  8. data/TODO +6 -0
  9. data/app/jobs/recommendable/recommendation_refresher.rb +12 -0
  10. data/app/models/recommendable/dislike.rb +9 -0
  11. data/app/models/recommendable/ignore.rb +9 -0
  12. data/app/models/recommendable/like.rb +9 -0
  13. data/app/models/recommendable/stashed_item.rb +9 -0
  14. data/config/routes.rb +3 -0
  15. data/db/migrate/20120124193723_create_likes.rb +17 -0
  16. data/db/migrate/20120124193728_create_dislikes.rb +17 -0
  17. data/db/migrate/20120127092558_create_ignores.rb +17 -0
  18. data/db/migrate/20120131173909_create_stashed_items.rb +17 -0
  19. data/lib/generators/recommendable/USAGE +8 -0
  20. data/lib/generators/recommendable/install_generator.rb +47 -0
  21. data/lib/generators/recommendable/templates/initializer.rb +18 -0
  22. data/lib/recommendable.rb +19 -0
  23. data/lib/recommendable/acts_as_recommendable.rb +85 -0
  24. data/lib/recommendable/acts_as_recommended_to.rb +659 -0
  25. data/lib/recommendable/engine.rb +13 -0
  26. data/lib/recommendable/exceptions.rb +4 -0
  27. data/lib/recommendable/railtie.rb +6 -0
  28. data/lib/recommendable/version.rb +3 -0
  29. data/lib/tasks/recommendable_tasks.rake +1 -0
  30. data/recommendable.gemspec +32 -0
  31. data/script/rails +8 -0
  32. data/spec/configuration_spec.rb +9 -0
  33. data/spec/dummy/README.rdoc +261 -0
  34. data/spec/dummy/Rakefile +7 -0
  35. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  36. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  37. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  38. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  39. data/spec/dummy/app/mailers/.gitkeep +0 -0
  40. data/spec/dummy/app/models/.gitkeep +0 -0
  41. data/spec/dummy/app/models/bully.rb +2 -0
  42. data/spec/dummy/app/models/movie.rb +3 -0
  43. data/spec/dummy/app/models/php_framework.rb +2 -0
  44. data/spec/dummy/app/models/user.rb +3 -0
  45. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  46. data/spec/dummy/config.ru +4 -0
  47. data/spec/dummy/config/application.rb +56 -0
  48. data/spec/dummy/config/boot.rb +10 -0
  49. data/spec/dummy/config/database.yml +25 -0
  50. data/spec/dummy/config/environment.rb +5 -0
  51. data/spec/dummy/config/environments/development.rb +37 -0
  52. data/spec/dummy/config/environments/production.rb +67 -0
  53. data/spec/dummy/config/environments/test.rb +37 -0
  54. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  55. data/spec/dummy/config/initializers/inflections.rb +15 -0
  56. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  57. data/spec/dummy/config/initializers/recommendable.rb +16 -0
  58. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  59. data/spec/dummy/config/initializers/session_store.rb +8 -0
  60. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  61. data/spec/dummy/config/locales/en.yml +5 -0
  62. data/spec/dummy/config/routes.rb +4 -0
  63. data/spec/dummy/db/migrate/20120128005553_create_likes.recommendable.rb +18 -0
  64. data/spec/dummy/db/migrate/20120128005554_create_dislikes.recommendable.rb +18 -0
  65. data/spec/dummy/db/migrate/20120128005555_create_ignores.recommendable.rb +18 -0
  66. data/spec/dummy/db/migrate/20120128020228_create_users.rb +9 -0
  67. data/spec/dummy/db/migrate/20120128020413_create_movies.rb +10 -0
  68. data/spec/dummy/db/migrate/20120128024632_create_php_frameworks.rb +9 -0
  69. data/spec/dummy/db/migrate/20120128024804_create_bullies.rb +9 -0
  70. data/spec/dummy/db/migrate/20120131195416_create_stashed_items.recommendable.rb +18 -0
  71. data/spec/dummy/db/schema.rb +89 -0
  72. data/spec/dummy/lib/assets/.gitkeep +0 -0
  73. data/spec/dummy/log/.gitkeep +0 -0
  74. data/spec/dummy/public/404.html +26 -0
  75. data/spec/dummy/public/422.html +26 -0
  76. data/spec/dummy/public/500.html +25 -0
  77. data/spec/dummy/public/favicon.ico +0 -0
  78. data/spec/dummy/recommendable_dummy_development +0 -0
  79. data/spec/dummy/recommendable_dummy_test +0 -0
  80. data/spec/dummy/script/rails +6 -0
  81. data/spec/factories.rb +16 -0
  82. data/spec/models/dislike_spec.rb +27 -0
  83. data/spec/models/ignore_spec.rb +27 -0
  84. data/spec/models/like_spec.rb +28 -0
  85. data/spec/models/stashed_item_spec.rb +27 -0
  86. data/spec/models/user_benchmark_spec.rb +49 -0
  87. data/spec/models/user_spec.rb +320 -0
  88. data/spec/spec_helper.rb +28 -0
  89. metadata +254 -0
@@ -0,0 +1,18 @@
1
+ # This migration comes from recommendable (originally 20120131173909)
2
+ class CreateStashedItems < ActiveRecord::Migration
3
+ def up
4
+ create_table :stashed_items do |t|
5
+ t.references :user
6
+ t.references :stashable, :polymorphic => true
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :stashed_items, :stashable_id
11
+ add_index :stashed_items, :stashable_type
12
+ add_index :stashed_items, [:user_id, :stashable_id, :stashable_type], :unique => true, :name => "user_stashed_constraint"
13
+ end
14
+
15
+ def down
16
+ drop_table :stashed_items
17
+ end
18
+ end
@@ -0,0 +1,89 @@
1
+ # encoding: UTF-8
2
+ # This file is auto-generated from the current state of the database. Instead
3
+ # of editing this file, please use the migrations feature of Active Record to
4
+ # incrementally modify your database, and then regenerate this schema definition.
5
+ #
6
+ # Note that this schema.rb definition is the authoritative source for your
7
+ # database schema. If you need to create the application database on another
8
+ # system, you should be using db:schema:load, not running all the migrations
9
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
11
+ #
12
+ # It's strongly recommended to check this file into your version control system.
13
+
14
+ ActiveRecord::Schema.define(:version => 20120131195416) do
15
+
16
+ create_table "bullies", :force => true do |t|
17
+ t.string "username"
18
+ t.datetime "created_at", :null => false
19
+ t.datetime "updated_at", :null => false
20
+ end
21
+
22
+ create_table "dislikes", :force => true do |t|
23
+ t.integer "user_id"
24
+ t.integer "dislikeable_id"
25
+ t.string "dislikeable_type"
26
+ t.datetime "created_at", :null => false
27
+ t.datetime "updated_at", :null => false
28
+ end
29
+
30
+ add_index "dislikes", ["dislikeable_id"], :name => "index_dislikes_on_dislikeable_id"
31
+ add_index "dislikes", ["dislikeable_type"], :name => "index_dislikes_on_dislikeable_type"
32
+ add_index "dislikes", ["user_id", "dislikeable_id", "dislikeable_type"], :name => "user_dislike_constraint", :unique => true
33
+
34
+ create_table "ignores", :force => true do |t|
35
+ t.integer "user_id"
36
+ t.integer "ignoreable_id"
37
+ t.string "ignoreable_type"
38
+ t.datetime "created_at", :null => false
39
+ t.datetime "updated_at", :null => false
40
+ end
41
+
42
+ add_index "ignores", ["ignoreable_id"], :name => "index_ignores_on_ignoreable_id"
43
+ add_index "ignores", ["ignoreable_type"], :name => "index_ignores_on_ignoreable_type"
44
+ add_index "ignores", ["user_id", "ignoreable_id", "ignoreable_type"], :name => "user_ignore_constraint", :unique => true
45
+
46
+ create_table "likes", :force => true do |t|
47
+ t.integer "user_id"
48
+ t.integer "likeable_id"
49
+ t.string "likeable_type"
50
+ t.datetime "created_at", :null => false
51
+ t.datetime "updated_at", :null => false
52
+ end
53
+
54
+ add_index "likes", ["likeable_id"], :name => "index_likes_on_likeable_id"
55
+ add_index "likes", ["likeable_type"], :name => "index_likes_on_likeable_type"
56
+ add_index "likes", ["user_id", "likeable_id", "likeable_type"], :name => "user_like_constraint", :unique => true
57
+
58
+ create_table "movies", :force => true do |t|
59
+ t.string "title"
60
+ t.integer "year"
61
+ t.datetime "created_at", :null => false
62
+ t.datetime "updated_at", :null => false
63
+ end
64
+
65
+ create_table "php_frameworks", :force => true do |t|
66
+ t.string "name"
67
+ t.datetime "created_at", :null => false
68
+ t.datetime "updated_at", :null => false
69
+ end
70
+
71
+ create_table "stashed_items", :force => true do |t|
72
+ t.integer "user_id"
73
+ t.integer "stashable_id"
74
+ t.string "stashable_type"
75
+ t.datetime "created_at", :null => false
76
+ t.datetime "updated_at", :null => false
77
+ end
78
+
79
+ add_index "stashed_items", ["stashable_id"], :name => "index_stashed_items_on_stashable_id"
80
+ add_index "stashed_items", ["stashable_type"], :name => "index_stashed_items_on_stashable_type"
81
+ add_index "stashed_items", ["user_id", "stashable_id", "stashable_type"], :name => "user_stashed_constraint", :unique => true
82
+
83
+ create_table "users", :force => true do |t|
84
+ t.string "username"
85
+ t.datetime "created_at", :null => false
86
+ t.datetime "updated_at", :null => false
87
+ end
88
+
89
+ end
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/404.html -->
21
+ <div class="dialog">
22
+ <h1>The page you were looking for doesn't exist.</h1>
23
+ <p>You may have mistyped the address or the page may have moved.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/422.html -->
21
+ <div class="dialog">
22
+ <h1>The change you wanted was rejected.</h1>
23
+ <p>Maybe you tried to change something you didn't have access to.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/500.html -->
21
+ <div class="dialog">
22
+ <h1>We're sorry, but something went wrong.</h1>
23
+ </div>
24
+ </body>
25
+ </html>
File without changes
Binary file
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
data/spec/factories.rb ADDED
@@ -0,0 +1,16 @@
1
+ Factory.define :user do |f|
2
+ f.username "test_user_%d"
3
+ end
4
+
5
+ Factory.define :movie do |f|
6
+ f.title "test_movie_%d"
7
+ f.year 2001
8
+ end
9
+
10
+ Factory.define :bully do |f|
11
+ f.username "bad_user_%d"
12
+ end
13
+
14
+ Factory.define :php_framework do |f|
15
+ f.name "CakePHP"
16
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ class DislikeSpec < MiniTest::Spec
4
+ describe Recommendable::Dislike do
5
+ before :each do
6
+ @user = User.create(:username => "dave")
7
+ end
8
+
9
+ it "should not be created for an object that does not act_as_recommendedable" do
10
+ django = PhpFramework.create(:name => "django")
11
+ proc { @user.dislike(django) }.must_raise Recommendable::RecordNotRecommendableError
12
+ end
13
+
14
+ it "should be created for an object that does act_as_recommendable" do
15
+ movie = Movie.create(:title => "Star Wars: Episode I - The Phantom Menace", :year => 1999)
16
+ @user.dislike(movie).must_equal true
17
+ end
18
+
19
+ it "should not be created twice for the same user-object pair" do
20
+ movie = Movie.create(:title => "Star Wars: Episode I - The Phantom Menace", :year => 1999)
21
+
22
+ @user.dislike(movie).must_equal true
23
+ @user.dislike(movie).must_be_nil
24
+ Recommendable::Dislike.count.must_equal 1
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ class IgnoreSpec < MiniTest::Spec
4
+ describe Recommendable::Ignore do
5
+ before :each do
6
+ @user = User.create(:username => "dave")
7
+ end
8
+
9
+ it "should not be created for an object that does not act_as_recommendedable" do
10
+ web2py = PhpFramework.create(:name => "web2py")
11
+ proc { @user.ignore(web2py) }.must_raise Recommendable::RecordNotRecommendableError
12
+ end
13
+
14
+ it "should be created for an object that does act_as_recommendable" do
15
+ movie = Movie.create(:title => "Star Wars: Episode I - The Phantom Menace", :year => 1999)
16
+ @user.ignore(movie).must_equal true
17
+ end
18
+
19
+ it "should not be created twice for the same user-object pair" do
20
+ movie = Movie.create(:title => "Star Wars: Episode I - The Phantom Menace", :year => 1999)
21
+
22
+ @user.ignore(movie).must_equal true
23
+ @user.ignore(movie).must_be_nil
24
+ Recommendable::Ignore.count.must_equal 1
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ class LikeSpec < MiniTest::Spec
4
+ describe Recommendable::Like do
5
+ before :each do
6
+ @user = User.create(:username => "dave")
7
+ end
8
+
9
+ it "should not be created for an object that does not act_as_recommendedable" do
10
+ cake = PhpFramework.create(:name => "CakePHP")
11
+ proc { @user.like(cake) }.must_raise Recommendable::RecordNotRecommendableError
12
+ end
13
+
14
+ it "should be created for an object that does act_as_recommendable" do
15
+ movie = Movie.create(:title => "2001: A Space Odyssey", :year => 1968)
16
+
17
+ @user.like(movie).must_equal true
18
+ end
19
+
20
+ it "should not be created twice for the same user-object pair" do
21
+ movie = Movie.create(:title => "2001: A Space Odyssey", :year => 1968)
22
+
23
+ @user.like(movie).must_equal true
24
+ @user.like(movie).must_be_nil
25
+ Recommendable::Like.count.must_equal 1
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ class StashedItemSpec < MiniTest::Spec
4
+ describe Recommendable::StashedItem do
5
+ before :each do
6
+ @user = User.create(:username => "dave")
7
+ end
8
+
9
+ it "should not be created for an object that does not act_as_recommendedable" do
10
+ web2py = PhpFramework.create(:name => "web2py")
11
+ proc { @user.stash(web2py) }.must_raise Recommendable::RecordNotRecommendableError
12
+ end
13
+
14
+ it "should be created for an object that does act_as_recommendable" do
15
+ movie = Movie.create(:title => "Star Wars: Episode I - The Phantom Menace", :year => 1999)
16
+ @user.stash(movie).must_equal true
17
+ end
18
+
19
+ it "should not be created twice for the same user-object pair" do
20
+ movie = Movie.create(:title => "Star Wars: Episode I - The Phantom Menace", :year => 1999)
21
+
22
+ @user.stash(movie).must_equal true
23
+ @user.stash(movie).must_be_nil
24
+ Recommendable::StashedItem.count.must_equal 1
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ class UserBenchmarkSpec < MiniTest::Unit::TestCase
4
+ def test_update_recommendations
5
+ if ENV["BENCH"] then
6
+ Recommendable.redis.flushdb
7
+ @actions = [:like, :dislike]
8
+
9
+ puts "\n"
10
+
11
+ assert_performance_exponential do |n|
12
+ @user = Factory(:user)
13
+ @users = []
14
+ @movies = []
15
+
16
+ # Make n users, 5n movies
17
+ n.times do
18
+ @users << Factory(:user)
19
+ @movies << Factory(:movie)
20
+ @movies << Factory(:movie)
21
+ @movies << Factory(:movie)
22
+ @movies << Factory(:movie)
23
+ @movies << Factory(:movie)
24
+ end
25
+
26
+ # Main user randomly likes/dislikes 1/4 of the movies
27
+ @movies.sample(n/4).each do |m|
28
+ @user.send(@actions.sample, m)
29
+ end
30
+
31
+ # Other users randomly like/dislike some movies
32
+ @movies.sample(n/2).each do |m|
33
+ @users.sample.send(@actions.sample, m)
34
+ @users.sample.send(@actions.sample, m)
35
+ @users.sample.send(@actions.sample, m)
36
+ end
37
+
38
+ @user.update_similarities
39
+ @user.update_recommendations
40
+ Recommendable.redis.flushdb
41
+
42
+ User.delete_all
43
+ Movie.delete_all
44
+ Recommendable::Like.delete_all
45
+ Recommendable::Dislike.delete_all
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,320 @@
1
+ require 'spec_helper'
2
+
3
+ class UserSpec < MiniTest::Spec
4
+ # User and Bully are skeleton classes defined in the dummy application for
5
+ # the sole purpose of running these tests. They're in spec/dummy/models/ but
6
+ # really, there's nothing there of note.
7
+
8
+ describe User do
9
+ describe "that does not act_as_recommendable" do
10
+ before :each do
11
+ @user = Factory(:bully)
12
+ @movie = Factory(:movie)
13
+ end
14
+
15
+ it "should not be able to rate, ignore, or stash an item, and trying should not be enough to get Redis keys" do
16
+ proc { @user.like(@movie) }.must_raise NoMethodError
17
+ proc { @user.dislike(@movie) }.must_raise NoMethodError
18
+ proc { @user.ignore(@movie) }.must_raise NoMethodError
19
+ proc { @user.stash(@movie) }.must_raise NoMethodError
20
+
21
+ @movie.liked_by.must_be_empty
22
+ @movie.disliked_by.must_be_empty
23
+
24
+ proc { @user.create_recommended_to_sets }.must_raise NoMethodError
25
+
26
+ proc { @user.likes_set_for(Movie) }.must_raise NoMethodError
27
+ proc { @user.dislikes_set_for(Movie) }.must_raise NoMethodError
28
+
29
+ Recommendable.redis.smembers("Bully:#{@user.id}:likes:Movie").must_be_empty
30
+ Recommendable.redis.smembers("Bully:#{@user.id}:dislikes:Movie").must_be_empty
31
+ Recommendable.redis.smembers("Bully:#{@user.id}:similarities").must_be_empty
32
+ Recommendable.redis.smembers("Bully:#{@user.id}:predictions:Movie").must_be_empty
33
+ end
34
+ end
35
+
36
+ describe "that does act_as_recommendable" do
37
+ before :each do
38
+ @user = Factory(:user)
39
+ @movie = Factory(:movie)
40
+ end
41
+
42
+ after :each do
43
+ Recommendable.redis.flushdb
44
+ end
45
+
46
+ it "should be able to like a recommendable item" do
47
+ @user.like(@movie).must_equal true
48
+ @user.send(:create_recommended_to_sets)
49
+ Recommendable.redis.smembers("User:#{@user.id}:likes:Movie").must_include @movie.id.to_s
50
+ @user.send(:destroy_recommended_to_sets)
51
+ end
52
+
53
+ it "should not dislike an item after liking it" do
54
+ @user.dislike(@movie)
55
+ @user.like(@movie).must_equal true
56
+ @user.dislikes?(@movie).must_equal false
57
+ @user.likes?(@movie).must_equal true
58
+
59
+ @user.send(:create_recommended_to_sets)
60
+ Recommendable.redis.smembers("User:#{@user.id}:dislikes:Movie").wont_include @movie.id.to_s
61
+ @user.send(:destroy_recommended_to_sets)
62
+ end
63
+
64
+ it "should not be ignoring an item after liking it" do
65
+ @user.ignore(@movie)
66
+ @user.like(@movie).must_equal true
67
+ @user.has_ignored?(@movie).must_equal false
68
+ @user.likes?(@movie).must_equal true
69
+ end
70
+
71
+ it "should not have an item stashed after liking it" do
72
+ @user.stash(@movie)
73
+ @user.like(@movie).must_equal true
74
+ @user.has_stashed?(@movie).must_equal false
75
+ @user.likes?(@movie).must_equal true
76
+ end
77
+
78
+ it "should be able to dislike a recommendable item" do
79
+ @user.dislike(@movie).must_equal true
80
+ @user.send(:create_recommended_to_sets)
81
+ Recommendable.redis.smembers("User:#{@user.id}:dislikes:Movie").must_include @movie.id.to_s
82
+ @user.send(:destroy_recommended_to_sets)
83
+ end
84
+
85
+ it "should not like an item after disliking it" do
86
+ @user.like(@movie)
87
+ @user.dislike(@movie).must_equal true
88
+ @user.likes?(@movie).must_equal false
89
+ @user.dislikes?(@movie).must_equal true
90
+
91
+ @user.send(:create_recommended_to_sets)
92
+ Recommendable.redis.smembers("User:#{@user.id}:dislikes:Movie").must_include @movie.id.to_s
93
+ Recommendable.redis.smembers("User:#{@user.id}:likes:Movie").wont_include @movie.id.to_s
94
+ @user.send(:destroy_recommended_to_sets)
95
+ end
96
+
97
+ it "should not be ignoring an item after disliking it" do
98
+ @user.ignore(@movie)
99
+ @user.dislike(@movie).must_equal true
100
+ @user.has_ignored?(@movie).must_equal false
101
+ @user.dislikes?(@movie).must_equal true
102
+ end
103
+
104
+ it "should not have an item stashed after disliking it" do
105
+ @user.stash(@movie)
106
+ @user.dislike(@movie).must_equal true
107
+ @user.has_stashed?(@movie).must_equal false
108
+ @user.dislikes?(@movie).must_equal true
109
+ end
110
+
111
+ it "should be able to ignore a recommendable item" do
112
+ @user.ignore(@movie).must_equal true
113
+ end
114
+
115
+ it "should not like an item after ignoring it" do
116
+ @user.like(@movie)
117
+ @user.ignore(@movie).must_equal true
118
+ @user.likes?(@movie).must_equal false
119
+ @user.has_ignored?(@movie).must_equal true
120
+ end
121
+
122
+ it "should not dislike an item after ignoring it" do
123
+ @user.dislike(@movie)
124
+ @user.ignore(@movie).must_equal true
125
+ @user.dislikes?(@movie).must_equal false
126
+ @user.has_ignored?(@movie).must_equal true
127
+ end
128
+
129
+ it "should not have an item stashed after ignoring it" do
130
+ @user.stash(@movie)
131
+ @user.ignore(@movie).must_equal true
132
+ @user.has_stashed?(@movie).must_equal false
133
+ @user.has_ignored?(@movie).must_equal true
134
+ end
135
+
136
+ it "should be able to stash a recommendable item" do
137
+ @user.stash(@movie).must_equal true
138
+ end
139
+
140
+ it "should not stash a liked item" do
141
+ @user.like(@movie)
142
+ @user.stash(@movie).must_be_nil
143
+ @user.likes?(@movie).must_equal true
144
+ @user.has_stashed?(@movie).must_equal false
145
+ end
146
+
147
+ it "should not stash a disliked item" do
148
+ @user.dislike(@movie)
149
+ @user.stash(@movie).must_be_nil
150
+ @user.dislikes?(@movie).must_equal true
151
+ @user.has_stashed?(@movie).must_equal false
152
+ end
153
+
154
+ it "should not have an item ignored after stashing it" do
155
+ @user.ignore(@movie)
156
+ @user.stash(@movie).must_equal true
157
+ @user.has_ignored?(@movie).must_equal false
158
+ @user.has_stashed?(@movie).must_equal true
159
+ end
160
+
161
+ it "should not be able to rate or ignore an item that is not recommendable. doing so should not be enough to create Redis keys" do
162
+ @cakephp = Factory(:php_framework)
163
+
164
+ proc { @user.like(@cakephp) }.must_raise Recommendable::RecordNotRecommendableError
165
+ proc { @user.dislike(@cakephp) }.must_raise Recommendable::RecordNotRecommendableError
166
+ proc { @user.ignore(@cakephp) }.must_raise Recommendable::RecordNotRecommendableError
167
+ proc { @user.stash(@cakephp) }.must_raise Recommendable::RecordNotRecommendableError
168
+
169
+ proc { @cakephp.liked_by }.must_raise NoMethodError
170
+ proc { @cakephp.disliked_by }.must_raise NoMethodError
171
+
172
+ @user.send(:create_recommended_to_sets)
173
+
174
+ Recommendable.redis.smembers("User:#{@user.id}:likes:PhpFramework").must_be_empty
175
+ Recommendable.redis.smembers("User:#{@user.id}:dislikes:PhpFramework").must_be_empty
176
+ Recommendable.redis.smembers("User:#{@user.id}:similarities").must_be_empty
177
+ Recommendable.redis.smembers("User:#{@user.id}:predictions:Movie").must_be_empty
178
+
179
+ @user.send(:destroy_recommended_to_sets)
180
+ end
181
+
182
+ it "should not freak out when re-rating, re-ignoring, or re-stashing items" do
183
+ @user.like(@movie)
184
+ @user.like(@movie).must_be_nil
185
+
186
+ @user.dislike(@movie)
187
+ @user.dislike(@movie).must_be_nil
188
+
189
+ @user.ignore(@movie)
190
+ @user.ignore(@movie).must_be_nil
191
+
192
+ @user.stash(@movie)
193
+ @user.stash(@movie).must_be_nil
194
+ end
195
+
196
+ it "should be able to unrate, unignore or unstash items" do
197
+ @user.like(@movie)
198
+ @user.unlike(@movie).must_equal true
199
+
200
+ @user.dislike(@movie)
201
+ @user.undislike(@movie).must_equal true
202
+
203
+ @user.ignore(@movie)
204
+ @user.unignore(@movie).must_equal true
205
+
206
+ @user.stash(@movie)
207
+ @user.unstash(@movie).must_equal true
208
+ end
209
+ end
210
+
211
+ describe "while getting recommendations" do
212
+ before :each do
213
+ @dave = Factory(:user)
214
+ @frank = Factory(:user)
215
+ @hal = Factory(:user)
216
+ @movie1 = Factory(:movie)
217
+ @movie2 = Factory(:movie)
218
+ @movie3 = Factory(:movie)
219
+ @movie4 = Factory(:movie)
220
+ @movie5 = Factory(:movie)
221
+ end
222
+
223
+ after :each do
224
+ Recommendable.redis.del "User:#{@dave.id}:similarities"
225
+ Recommendable.redis.del "User:#{@dave.id}:predictions:Movie"
226
+ Recommendable.redis.del "User:#{@frank.id}:similarities"
227
+ Recommendable.redis.del "User:#{@frank.id}:predictions:Movie"
228
+ end
229
+
230
+ it "should get populated sorted sets for similarities and recommendations" do
231
+ @dave.like(@movie1)
232
+ @frank.like(@movie1)
233
+ @frank.like(@movie2)
234
+ @dave.send :update_similarities
235
+ @dave.send :update_recommendations
236
+
237
+ @dave.similar_raters.must_include @frank
238
+ @dave.recommendations_for(Movie).must_include @movie2
239
+ end
240
+
241
+ it "should order similar users by similarity" do
242
+ @dave.like(@movie1)
243
+ @dave.like(@movie2)
244
+ @frank.like(@movie1)
245
+ @frank.dislike(@movie2)
246
+ @hal.like(@movie1)
247
+ @hal.like(@movie2)
248
+
249
+ # hal should be more similar to dave than frank
250
+ @dave.send :update_similarities
251
+ @dave.send :update_recommendations
252
+
253
+ @dave.similar_raters.must_equal [@hal, @frank]
254
+ end
255
+
256
+ it "should correctly recommend an awesome movie or two. In the correct order, of course." do
257
+ @dave.like(@movie1)
258
+ @dave.like(@movie2)
259
+ @frank.like(@movie1)
260
+ @frank.like(@movie2)
261
+ @frank.like(@movie4)
262
+ @hal.like(@movie1)
263
+ @hal.like(@movie3)
264
+
265
+ @dave.send :update_similarities
266
+ @dave.send :update_recommendations
267
+
268
+ @dave.recommendations.must_equal [@movie4, @movie3]
269
+ end
270
+
271
+ it "should not recommend rated items" do
272
+ @dave.like(@movie1)
273
+ @dave.dislike(@movie2)
274
+ @frank.like(@movie1)
275
+ @frank.like(@movie2)
276
+ @frank.like(@movie4)
277
+ @hal.like(@movie1)
278
+ @hal.like(@movie3)
279
+
280
+ @dave.send :update_similarities
281
+ @dave.send :update_recommendations
282
+
283
+ @dave.recommendations.wont_include @movie1
284
+ @dave.recommendations.wont_include @movie2
285
+ end
286
+
287
+ it "should not recommend ignored items" do
288
+ @dave.like(@movie1)
289
+ @dave.like(@movie2)
290
+ @dave.ignore(@movie4)
291
+ @frank.like(@movie1)
292
+ @frank.like(@movie2)
293
+ @frank.like(@movie4)
294
+ @hal.like(@movie1)
295
+ @hal.like(@movie3)
296
+
297
+ @dave.send :update_similarities
298
+ @dave.send :update_recommendations
299
+
300
+ @dave.recommendations.wont_include @movie4
301
+ end
302
+
303
+ it "should not recommend stashed items" do
304
+ @dave.like(@movie1)
305
+ @dave.like(@movie2)
306
+ @dave.stash(@movie4)
307
+ @frank.like(@movie1)
308
+ @frank.like(@movie2)
309
+ @frank.like(@movie4)
310
+ @hal.like(@movie1)
311
+ @hal.like(@movie3)
312
+
313
+ @dave.send :update_similarities
314
+ @dave.send :update_recommendations
315
+
316
+ @dave.recommendations.wont_include @movie4
317
+ end
318
+ end
319
+ end
320
+ end