recommendable 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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