Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Subclassable ratables #84

Merged
merged 2 commits into from

2 participants

@aaronroyer

Currently, when a ratable has subclasses, the likes applied to the subclasses are essentially abandoned. If the base class is "abstract" then recommendable doesn't seem to work for that ratable. Take this example.

class Book < ActiveRecord::Base
end

class FictionBook < Book
end

class NonfictionBook < Book
end

class User < ActiveRecord::Base
  recommends :books
end

user1.like(nonfiction_book1)
user1.like(nonfiction_book2)
user1.like(nonfiction_book3)

user2.like(nonfiction_book1)
user2.like(nonfiction_book2)

# We don't get a recommendation
user.recommended_books # => []

Recommendations are generated if we only work with Book objects, but not for the subclasses. The reason this happens is because the data for the subclasses are stored in Redis keys namespaced with 'fiction_book' and 'nonfiction_book', even though they're not configured ratable classes. When recommendations are generated, Recommendable iterates over Recommendable.config.ratable_classes, finds Book and looks for data at keys namespaced with 'book'. It doesn't find any of the subclass likes and they are disregarded in calculating recommendations. If only subclasses are used then no recommendations are generated at all.

This PR makes all subclasses of ratables configured with recommends use keys corresponding to the superclass (or more distant ratable ancestor) so all of the recommendations go in the same "bucket". In the above example, user2 would get nonfiction_book3 as a recommendation. If user1 also liked a fiction book, then user2 would get a recommendation for that as well - Book and all of its subclasses act as the same type as far as Recommendable is concerned.

The subclass objects also start showing up in places like Book.top where they didn't before.

If we wanted to place FictionBook in its own ratable category then we could just add recommends :books, :fiction_books and then FictionBooks would be in their own category and Books and NonfictionBooks would still be in the Books category, if that makes sense.

This change only needed a few lines additional code and some tweaks to Redis key mapping. Almost everything in this PR is adding tests to make sure the subclass ratings and recommendations work the same as the normal ones.

aaronroyer added some commits
@aaronroyer aaronroyer Support subclassing ratables
This allows ratable classes to have subclasses that properly accumulate ratings
in the same ratable category.
b01bc8b
@aaronroyer aaronroyer Clean up Redis keys a bit
Rename a few things, add comments, and factor out some other bits relating
to Redis key mapping.
7ec6d8d
@davidcelis
Owner

This is great. Thanks so much for taking the time to fix this accursed STI issue. And with so many tests!

@davidcelis davidcelis merged commit 59c0016 into from
@aaronroyer

@davidcelis Awesome, thanks for the response! Let me know if you need any help updating documentation when it's time for a release.

There was one quirk I noticed after playing with the fix for a bit. top on a subclass will return the same set of result(s) as on the base class. In the example above FictionBook.top(n) always returns the same thing as Book.top(n).

This is the only thing I've found that is unexpected as a result of the fix. It is a bit of a tricky problem, since no extra metadata about subtypes is stored in Redis you can't just pull ids for a certain subtype from there. It seems too heavy to add that extra data to Redis just for this. One thing that could be done without having to change a lot of things is to pull all of the ids (ick) and query for records with the appropriate type, with the limit being the number passed to top. But maybe this could get really messy fast with the different ORMs/databases?

Really I'm thinking that just not defining top on any subclasses could be the way to go, rather than having it do something that might be unexpected (current status) or putting a lot of code in to make it do the expected thing. Removing it might be consistent with the expectation that Recommendable gives recommendations on the type explicitly configured with recommends :type as a complete group and doesn't break it down any further than that. You can pull recommendations of the configured base type by any means, including with top, and filter them yourself if you want.

Either way, this is kind of what I meant when I offered to help with the docs, because it seems like it would be good to clarify what happens here, regardless of which way it ends up. Besides this, I think it mostly does the expected thing.

Wow, that was a lot of text for a pretty simple matter. Any thoughts?

@davidcelis
Owner

Thanks for the detailed explanation. I'm tempted to force users into using Recommendable a specific way in cases of STI. I definitely don't think it should manage complicated filtering of records by type for .top, as that was really just added in as a bit of a bonus feature... It would be nice if there was a clean way to do this. If databases were better about allowing you to order returned rows by a specific ordering of IDs, I could have .top and all of the #recommended_ methods return Relations instead of arrays and people could call .limit on them. But alas. I see two possibilities:

  1. Your suggestion of noting that, at the expense of not overusing Redis, .top will return all ratable base/subclasses in the hierarchy
  2. We rework the solution such that we go back to storing ratings in keys that reflect the actual class regardless of STI and change the logic to find ratable classes to use something like self_and_descendants (although I believe the logic for this differs between ORMs).
    • So with the books, for example, user.recommended_nonfiction_books could go directly to recommendable:users:1:recommended_nonfiction_books and pull recommendations.
    • Book.top could utilize Redis' ZUNIONSTORE command to combine recommendable:users:1:recommended_nonfiction_books and recommendable:users:1:recommended_fiction_books in a temporary zset, pull the top recommendations, and then DEL the temp zset.
    • Ditto for .top
    • Users need to be specific about what's recommended. recommends :books puts the superclass and all subclasses as ratable. recommends :nonfiction_books does not.

I think 2 would be a better UX, but the code would be more complicated.

@aaronroyer

@davidcelis Agreed on all your points. The #recommended_ methods, in addition to .top, start making it more compelling to break things back down into class/subclass specific keys to allow more specific recommendations. It is definitely better UX, in that it just does the right thing if a user tries calling those methods.

I think the benefit for adding the complexity is proportional to how often people use subclasses to categorize things they will want to pull separate recommendations for. My Book class and subclasses are a bit contrived in this sense - someone probably wouldn't do that just for categorization and there would have to be significantly diverging logic to justify the subclasses. The (more common?) alternative would be to just add a column with a category, associated tags, or some other associated category model. In these cases Recommendable would not help to narrow things down, even with the additional work put in.

So it would really be a sort of bonus feature for those using STI, but not for those categorizing other ways. Maybe that's a good thing. I'm not sure that I would want to encourage people to use STI just for that, if they aren't using it already, but it would always be an option.

Personally, I guess I would lean toward keeping it simple, unless there is a lot of demand for splitting the data out for STI. In my specific case (which motivated the fix) I don't need it. But I don't know if that's common or not.

Whatever you decide I'd be willing to help out where I can.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 26, 2013
  1. @aaronroyer

    Support subclassing ratables

    aaronroyer authored
    This allows ratable classes to have subclasses that properly accumulate ratings
    in the same ratable category.
  2. @aaronroyer

    Clean up Redis keys a bit

    aaronroyer authored
    Rename a few things, add comments, and factor out some other bits relating
    to Redis key mapping.
This page is out of date. Refresh to see the latest.
View
29 lib/recommendable/helpers/redis_key_mapper.rb
@@ -4,28 +4,45 @@ module RedisKeyMapper
class << self
%w[liked disliked hidden bookmarked recommended].each do |action|
define_method "#{action}_set_for" do |klass, id|
- [Recommendable.config.redis_namespace, Recommendable.config.user_class.to_s.tableize, id, "#{action}_#{klass.to_s.tableize}"].compact.join(':')
+ [redis_namespace, user_namespace, id, "#{action}_#{ratable_namespace(klass)}"].compact.join(':')
end
end
def similarity_set_for(id)
- [Recommendable.config.redis_namespace, Recommendable.config.user_class.to_s.tableize, id, 'similarities'].compact.join(':')
+ [redis_namespace, user_namespace, id, 'similarities'].compact.join(':')
end
def liked_by_set_for(klass, id)
- [Recommendable.config.redis_namespace, klass.to_s.tableize, id, 'liked_by'].compact.join(':')
+ [redis_namespace, ratable_namespace(klass), id, 'liked_by'].compact.join(':')
end
def disliked_by_set_for(klass, id)
- [Recommendable.config.redis_namespace, klass.to_s.tableize, id, 'disliked_by'].compact.join(':')
+ [redis_namespace, ratable_namespace(klass), id, 'disliked_by'].compact.join(':')
end
def score_set_for(klass)
- [Recommendable.config.redis_namespace, klass.to_s.tableize, 'scores'].join(':')
+ [redis_namespace, ratable_namespace(klass), 'scores'].join(':')
end
def temp_set_for(klass, id)
- [Recommendable.config.redis_namespace, klass.to_s.tableize, id, 'temp'].compact.join(':')
+ [redis_namespace, ratable_namespace(klass), id, 'temp'].compact.join(':')
+ end
+
+ private
+
+ def redis_namespace
+ Recommendable.config.redis_namespace
+ end
+
+ def user_namespace
+ Recommendable.config.user_class.to_s.tableize
+ end
+
+ # If the class or a superclass has been configured as ratable with <tt>recommends :class_name</tt>
+ # then that ratable class is used to produce the namespace. Fall back on just using the given class.
+ def ratable_namespace(klass)
+ klass = klass.ratable_class if klass.respond_to?(:ratable_class)
+ klass.to_s.tableize
end
end
end
View
8 lib/recommendable/ratable.rb
@@ -52,6 +52,14 @@ def self.top(count = 1)
Recommendable.query(self, ids).sort_by { |item| ids.index(item.id.to_s) }
end
+ # Returns the class that has been explicitly been made ratable, whether it is this
+ # class or a superclass. This allows a ratable class and all of its subclasses to be
+ # considered the same type of ratable and give recommendations from the base class
+ # or any of the subclasses.
+ def self.ratable_class
+ ancestors.find { |klass| Recommendable.config.ratable_classes.include?(klass) }
+ end
+
private
# Completely removes this item from redis. Called from a before_destroy hook.
View
3  test/dummy/app/models/boat.rb
@@ -0,0 +1,3 @@
+# STI class that is not ratable
+class Boat < Vehicle
+end
View
4 test/dummy/app/models/car.rb
@@ -0,0 +1,4 @@
+# STI ratable class whose base class is not ratable
+class Car < Vehicle
+end
+
View
3  test/dummy/app/models/documentary.rb
@@ -0,0 +1,3 @@
+# STI subclass of a ratable class
+class Documentary < Movie
+end
View
2  test/dummy/app/models/user.rb
@@ -1,4 +1,4 @@
class User < ActiveRecord::Base
attr_accessible :email if ::ActiveRecord::VERSION::MAJOR < 4
- recommends :movies, :books
+ recommends :movies, :books, :cars
end
View
3  test/dummy/app/models/vehicle.rb
@@ -0,0 +1,3 @@
+# STI base class that is not ratable
+class Vehicle < ActiveRecord::Base
+end
View
BIN  test/dummy/db/development.sqlite3
Binary file not shown
View
10 test/dummy/db/migrate/20131226071447_create_vehicles.rb
@@ -0,0 +1,10 @@
+class CreateVehicles < ActiveRecord::Migration
+ def change
+ create_table :vehicles do |t|
+ t.string :color
+ t.string :type
+
+ t.timestamps
+ end
+ end
+end
View
5 test/dummy/db/migrate/20131226165647_add_type_to_movie.rb
@@ -0,0 +1,5 @@
+class AddTypeToMovie < ActiveRecord::Migration
+ def change
+ add_column :movies, :type, :string
+ end
+end
View
36 test/dummy/db/schema.rb
@@ -9,34 +9,42 @@
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
-# It's strongly recommended to check this file into your version control system.
+# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20121007213144) do
+ActiveRecord::Schema.define(version: 20131226165647) do
- create_table "books", :force => true do |t|
+ create_table "books", force: true do |t|
t.string "title"
t.string "author"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
- create_table "movies", :force => true do |t|
+ create_table "movies", force: true do |t|
t.string "title"
t.integer "year"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "type"
end
- create_table "rocks", :force => true do |t|
+ create_table "rocks", force: true do |t|
t.string "name"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
- create_table "users", :force => true do |t|
+ create_table "users", force: true do |t|
t.string "email"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "vehicles", force: true do |t|
+ t.string "color"
+ t.string "type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
end
end
View
BIN  test/dummy/db/test.sqlite3
Binary file not shown
View
20 test/factories.rb
@@ -7,6 +7,12 @@
f.year { '200%d'.to_i }
end
+Factory.define(:documentary) do |f|
+ f.title '%{year}: A Space Documentary'
+ f.year { '200%d'.to_i }
+ f.type 'Documentary'
+end
+
Factory.define(:book) do |f|
f.title 'Harry Potter Vol. %d'
f.author 'J.K. Rowling'
@@ -15,3 +21,17 @@
Factory.define(:rock) do |f|
f.name 'Boring Specimen No. %d'
end
+
+Factory.define(:vehicle) do |f|
+ f.color 'blue'
+end
+
+Factory.define(:car) do |f|
+ f.type 'Car'
+ f.color 'red'
+end
+
+Factory.define(:boat) do |f|
+ f.type 'Boat'
+ f.color 'white'
+end
View
3  test/recommendable/helpers/calculations_test.rb
@@ -5,7 +5,8 @@ class CalculationsTest < MiniTest::Unit::TestCase
def setup
@user = Factory(:user)
5.times { |x| instance_variable_set(:"@user#{x+1}", Factory(:user)) }
- 10.times { |x| instance_variable_set(:"@movie#{x+1}", Factory(:movie)) }
+ 5.times { |x| instance_variable_set(:"@movie#{x+1}", Factory(:movie)) }
+ 5.upto(9) { |x| instance_variable_set(:"@movie#{x+1}", Factory(:documentary)) }
10.times { |x| instance_variable_set(:"@book#{x+1}", Factory(:book)) }
[@movie1, @movie2, @movie3, @book4, @book5, @book6].each { |obj| @user.like(obj) }
View
65 test/recommendable/helpers/redis_key_mapper_test.rb
@@ -37,4 +37,69 @@ def test_output_of_disliked_by_set_for
def test_output_of_score_set_for
assert_equal Recommendable::Helpers::RedisKeyMapper.score_set_for(Movie), 'recommendable:movies:scores'
end
+
+ def test_output_of_liked_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.liked_set_for(Documentary, 1), 'recommendable:users:1:liked_movies'
+ end
+
+ def test_output_of_disliked_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.disliked_set_for(Documentary, 1), 'recommendable:users:1:disliked_movies'
+ end
+
+ def test_output_of_hidden_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.hidden_set_for(Documentary, 1), 'recommendable:users:1:hidden_movies'
+ end
+
+ def test_output_of_bookmarked_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.bookmarked_set_for(Documentary, 1), 'recommendable:users:1:bookmarked_movies'
+ end
+
+ def test_output_of_recommended_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.recommended_set_for(Documentary, 1), 'recommendable:users:1:recommended_movies'
+ end
+
+ def test_output_of_liked_by_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(Documentary, 1), 'recommendable:movies:1:liked_by'
+ end
+
+ def test_output_of_disliked_by_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(Documentary, 1), 'recommendable:movies:1:disliked_by'
+ end
+
+ def test_output_of_score_set_for_subclass_of_ratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.score_set_for(Documentary), 'recommendable:movies:scores'
+ end
+
+ def test_output_of_liked_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.liked_set_for(Car, 1), 'recommendable:users:1:liked_cars'
+ end
+
+ def test_output_of_disliked_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.disliked_set_for(Car, 1), 'recommendable:users:1:disliked_cars'
+ end
+
+ def test_output_of_hidden_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.hidden_set_for(Car, 1), 'recommendable:users:1:hidden_cars'
+ end
+
+ def test_output_of_bookmarked_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.bookmarked_set_for(Car, 1), 'recommendable:users:1:bookmarked_cars'
+ end
+
+ def test_output_of_recommended_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.recommended_set_for(Car, 1), 'recommendable:users:1:recommended_cars'
+ end
+
+ def test_output_of_liked_by_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(Car, 1), 'recommendable:cars:1:liked_by'
+ end
+
+ def test_output_of_disliked_by_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(Car, 1), 'recommendable:cars:1:disliked_by'
+ end
+
+ def test_output_of_score_set_for_ratable_subclass_of_nonratable
+ assert_equal Recommendable::Helpers::RedisKeyMapper.score_set_for(Car), 'recommendable:cars:scores'
+ end
+
end
View
53 test/recommendable/ratable_test.rb
@@ -6,15 +6,24 @@ def setup
@movie = Factory(:movie)
@book = Factory(:book)
@rock = Factory(:rock)
+ @vehicle = Factory(:vehicle)
end
def test_recommendable_predicate_works
assert Movie.recommendable?
assert @movie.recommendable?
+ assert Documentary.recommendable?
+ assert Factory(:documentary).recommendable?
assert Book.recommendable?
assert @book.recommendable?
refute Rock.recommendable?
refute @rock.recommendable?
+ assert Car.recommendable?
+ assert Factory(:car).recommendable?
+ refute Vehicle.recommendable?
+ refute @vehicle.recommendable?
+ refute Boat.recommendable?
+ refute Factory(:boat).recommendable?
end
def test_rated_predicate_works
@@ -41,6 +50,23 @@ def test_top_scope_returns_best_things
assert_equal top[2], @book
end
+ def test_top_scope_returns_best_things_for_ratable_base_class
+ @movie2 = Factory(:movie)
+ @doc = Factory(:documentary)
+ @user = Factory(:user)
+ @friend = Factory(:user)
+
+ @user.like(@doc)
+ @friend.like(@doc)
+ @user.like(@movie2)
+ @user.dislike(@movie)
+
+ top = Movie.top(3)
+ assert_equal top[0], @doc
+ assert_equal top[1], @movie2
+ assert_equal top[2], @movie
+ end
+
def test_removed_from_recommendable_upon_destruction
@user = Factory(:user)
@friend = Factory(:user)
@@ -72,6 +98,33 @@ def test_removed_from_recommendable_upon_destruction
assert_empty @buddy.bookmarked_books
end
+ def test_ratable_subclass_object_removed_from_recommendable_upon_destruction
+ @doc = Factory(:documentary)
+ @user = Factory(:user)
+ @friend = Factory(:user)
+ @buddy = Factory(:user)
+ @stranger = Factory(:user)
+ @user.like(@doc)
+ @friend.dislike(@doc)
+ @buddy.hide(@doc)
+ @stranger.bookmark(@doc)
+
+ liked_by_set = Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(@doc.class, @doc.id)
+ disliked_by_set = Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(@doc.class, @doc.id)
+ [liked_by_set, disliked_by_set].each { |set| assert_equal Recommendable.redis.scard(set), 1 }
+
+ assert @user.likes?(@doc)
+ assert @friend.dislikes?(@doc)
+ assert @buddy.hides?(@doc)
+
+ @doc.destroy
+
+ [liked_by_set, disliked_by_set].each { |set| assert_equal Recommendable.redis.scard(set), 0 }
+
+ assert_empty @buddy.hidden_movies
+ assert_empty @stranger.bookmarked_books
+ end
+
def teardown
Recommendable.redis.flushdb
end
View
31 test/recommendable/rater/bookmarker_test.rb
@@ -5,6 +5,7 @@ class BookmarkerTest < MiniTest::Unit::TestCase
def setup
@user = Factory(:user)
@movie = Factory(:movie)
+ @doc = Factory(:documentary)
end
def test_that_bookmark_adds_to_bookmarked_set
@@ -13,6 +14,12 @@ def test_that_bookmark_adds_to_bookmarked_set
assert_includes @user.bookmarked_movie_ids, @movie.id
end
+ def test_that_bookmark_adds_subclass_bookmarks_to_bookmarked_set
+ refute_includes @user.bookmarked_movie_ids, @doc.id
+ @user.bookmark(@doc)
+ assert_includes @user.bookmarked_movie_ids, @doc.id
+ end
+
def test_that_cant_bookmark_already_bookmarked_object
assert @user.bookmark(@movie)
assert_nil @user.bookmark(@movie)
@@ -39,6 +46,13 @@ def test_that_unbookmark_removes_item_from_bookmarked_set
refute_includes @user.bookmarked_movie_ids, @movie.id
end
+ def test_that_unbookmark_removes_subclass_item_from_bookmarked_set
+ @user.bookmark(@doc)
+ assert_includes @user.bookmarked_movie_ids, @doc.id
+ @user.unbookmark(@doc)
+ refute_includes @user.bookmarked_movie_ids, @doc.id
+ end
+
def test_that_cant_unbookmark_item_unless_bookmarked
assert_nil @user.unbookmark(@movie)
end
@@ -47,6 +61,10 @@ def test_that_bookmarks_returns_bookmarked_records
refute_includes @user.bookmarks, @movie
@user.bookmark(@movie)
assert_includes @user.bookmarks, @movie
+
+ refute_includes @user.bookmarks, @doc
+ @user.bookmark(@doc)
+ assert_includes @user.bookmarks, @doc
end
def test_that_dynamic_bookmarked_finder_only_returns_relevant_records
@@ -65,8 +83,9 @@ def test_that_bookmarks_count_counts_all_bookmarks
@user.bookmark(@movie)
@user.bookmark(movie2)
@user.bookmark(book)
+ @user.bookmark(@doc)
- assert_equal @user.bookmarks_count, 3
+ assert_equal @user.bookmarks_count, 4
end
def test_that_dynamic_bookmarked_count_methods_only_count_relevant_bookmarks
@@ -75,9 +94,10 @@ def test_that_dynamic_bookmarked_count_methods_only_count_relevant_bookmarks
@user.bookmark(@movie)
@user.bookmark(movie2)
+ @user.bookmark(@doc)
@user.bookmark(book)
- assert_equal @user.bookmarked_movies_count, 2
+ assert_equal @user.bookmarked_movies_count, 3
assert_equal @user.bookmarked_books_count, 1
end
@@ -90,11 +110,14 @@ def test_that_bookmarks_in_common_with_returns_all_common_bookmarks
@user.bookmark(@movie)
@user.bookmark(book)
@user.bookmark(movie2)
+ @user.bookmark(@doc)
friend.bookmark(@movie)
friend.bookmark(book)
friend.bookmark(book2)
+ friend.bookmark(@doc)
assert_includes @user.bookmarks_in_common_with(friend), @movie
+ assert_includes @user.bookmarks_in_common_with(friend), @doc
assert_includes @user.bookmarks_in_common_with(friend), book
refute_includes @user.bookmarks_in_common_with(friend), movie2
refute_includes friend.bookmarks_in_common_with(@user), book2
@@ -106,14 +129,18 @@ def test_that_dynamic_bookmarked_in_common_with_only_returns_relevant_records
book = Factory(:book)
@user.bookmark(@movie)
+ @user.bookmark(@doc)
@user.bookmark(book)
friend.bookmark(@movie)
friend.bookmark(book)
+ friend.bookmark(@doc)
assert_includes @user.bookmarked_movies_in_common_with(friend), @movie
+ assert_includes @user.bookmarked_movies_in_common_with(friend), @doc
assert_includes @user.bookmarked_books_in_common_with(friend), book
refute_includes @user.bookmarked_movies_in_common_with(friend), book
refute_includes @user.bookmarked_books_in_common_with(friend), @movie
+ refute_includes @user.bookmarked_books_in_common_with(friend), @doc
end
def teardown
View
31 test/recommendable/rater/disliker_test.rb
@@ -5,6 +5,7 @@ class DislikerTest < MiniTest::Unit::TestCase
def setup
@user = Factory(:user)
@movie = Factory(:movie)
+ @doc = Factory(:documentary)
end
def test_that_dislike_adds_to_disliked_set
@@ -13,6 +14,12 @@ def test_that_dislike_adds_to_disliked_set
assert_includes @user.disliked_movie_ids, @movie.id
end
+ def test_that_dislike_adds_subclass_dislikes_to_disliked_set
+ refute_includes @user.disliked_movie_ids, @doc.id
+ @user.dislike(@doc)
+ assert_includes @user.disliked_movie_ids, @doc.id
+ end
+
def test_that_cant_dislike_already_disliked_object
assert @user.dislike(@movie)
assert_nil @user.dislike(@movie)
@@ -39,6 +46,13 @@ def test_that_undislike_removes_item_from_disliked_set
refute_includes @user.disliked_movie_ids, @movie.id
end
+ def test_that_undislike_removes_subclass_item_from_disliked_set
+ @user.dislike(@doc)
+ assert_includes @user.disliked_movie_ids, @doc.id
+ @user.undislike(@doc)
+ refute_includes @user.disliked_movie_ids, @doc.id
+ end
+
def test_that_cant_undislike_item_unless_disliked
assert_nil @user.undislike(@movie)
end
@@ -47,6 +61,10 @@ def test_that_dislikes_returns_disliked_records
refute_includes @user.dislikes, @movie
@user.dislike(@movie)
assert_includes @user.dislikes, @movie
+
+ refute_includes @user.dislikes, @doc
+ @user.dislike(@doc)
+ assert_includes @user.dislikes, @doc
end
def test_that_dynamic_disliked_finder_only_returns_relevant_records
@@ -65,8 +83,9 @@ def test_that_dislikes_count_counts_all_dislikes
@user.dislike(@movie)
@user.dislike(movie2)
@user.dislike(book)
+ @user.dislike(@doc)
- assert_equal @user.dislikes_count, 3
+ assert_equal @user.dislikes_count, 4
end
def test_that_dynamic_disliked_count_methods_only_count_relevant_dislikes
@@ -75,9 +94,10 @@ def test_that_dynamic_disliked_count_methods_only_count_relevant_dislikes
@user.dislike(@movie)
@user.dislike(movie2)
+ @user.dislike(@doc)
@user.dislike(book)
- assert_equal @user.disliked_movies_count, 2
+ assert_equal @user.disliked_movies_count, 3
assert_equal @user.disliked_books_count, 1
end
@@ -90,11 +110,14 @@ def test_that_dislikes_in_common_with_returns_all_common_dislikes
@user.dislike(@movie)
@user.dislike(book)
@user.dislike(movie2)
+ @user.dislike(@doc)
friend.dislike(@movie)
friend.dislike(book)
friend.dislike(book2)
+ friend.dislike(@doc)
assert_includes @user.dislikes_in_common_with(friend), @movie
+ assert_includes @user.dislikes_in_common_with(friend), @doc
assert_includes @user.dislikes_in_common_with(friend), book
refute_includes @user.dislikes_in_common_with(friend), movie2
refute_includes friend.dislikes_in_common_with(@user), book2
@@ -106,14 +129,18 @@ def test_that_dynamic_disliked_in_common_with_only_returns_relevant_records
book = Factory(:book)
@user.dislike(@movie)
+ @user.dislike(@doc)
@user.dislike(book)
friend.dislike(@movie)
+ friend.dislike(@doc)
friend.dislike(book)
assert_includes @user.disliked_movies_in_common_with(friend), @movie
+ assert_includes @user.disliked_movies_in_common_with(friend), @doc
assert_includes @user.disliked_books_in_common_with(friend), book
refute_includes @user.disliked_movies_in_common_with(friend), book
refute_includes @user.disliked_books_in_common_with(friend), @movie
+ refute_includes @user.disliked_books_in_common_with(friend), @doc
end
def teardown
View
28 test/recommendable/rater/hider_test.rb
@@ -5,6 +5,7 @@ class HiderTest < MiniTest::Unit::TestCase
def setup
@user = Factory(:user)
@movie = Factory(:movie)
+ @doc = Factory(:documentary)
end
def test_that_hide_adds_to_hidden_set
@@ -13,6 +14,12 @@ def test_that_hide_adds_to_hidden_set
assert_includes @user.hidden_movie_ids, @movie.id
end
+ def test_that_like_adds_subclass_hides_to_hidden_set
+ refute_includes @user.hidden_movie_ids, @doc.id
+ @user.hide(@doc)
+ assert_includes @user.hidden_movie_ids, @doc.id
+ end
+
def test_that_cant_hide_already_hidden_object
assert @user.hide(@movie)
assert_nil @user.hide(@movie)
@@ -39,6 +46,13 @@ def test_that_unhide_removes_item_from_hidden_set
refute_includes @user.hidden_movie_ids, @movie.id
end
+ def test_that_unhide_removes_subclass_item_from_hidden_set
+ @user.hide(@doc)
+ assert_includes @user.hidden_movie_ids, @doc.id
+ @user.unhide(@doc)
+ refute_includes @user.hidden_movie_ids, @doc.id
+ end
+
def test_that_cant_unhide_item_unless_hidden
assert_nil @user.unhide(@movie)
end
@@ -47,6 +61,10 @@ def test_that_hidden_returns_hidden_records
refute_includes @user.hiding, @movie
@user.hide(@movie)
assert_includes @user.hiding, @movie
+
+ refute_includes @user.hiding, @doc
+ @user.hide(@doc)
+ assert_includes @user.hiding, @doc
end
def test_that_dynamic_hidden_finder_only_returns_relevant_records
@@ -65,8 +83,9 @@ def test_that_hides_count_counts_all_hides
@user.hide(@movie)
@user.hide(movie2)
@user.hide(book)
+ @user.hide(@doc)
- assert_equal @user.hidden_count, 3
+ assert_equal @user.hidden_count, 4
end
def test_that_dynamic_hidden_count_methods_only_count_relevant_hides
@@ -90,11 +109,14 @@ def test_that_hides_in_common_with_returns_all_common_hides
@user.hide(@movie)
@user.hide(book)
@user.hide(movie2)
+ @user.hide(@doc)
friend.hide(@movie)
friend.hide(book)
friend.hide(book2)
+ friend.hide(@doc)
assert_includes @user.hiding_in_common_with(friend), @movie
+ assert_includes @user.hiding_in_common_with(friend), @doc
assert_includes @user.hiding_in_common_with(friend), book
refute_includes @user.hiding_in_common_with(friend), movie2
refute_includes friend.hiding_in_common_with(@user), book2
@@ -106,14 +128,18 @@ def test_that_dynamic_hidden_in_common_with_only_returns_relevant_records
book = Factory(:book)
@user.hide(@movie)
+ @user.hide(@doc)
@user.hide(book)
friend.hide(@movie)
+ friend.hide(@doc)
friend.hide(book)
assert_includes @user.hidden_movies_in_common_with(friend), @movie
+ assert_includes @user.hidden_movies_in_common_with(friend), @doc
assert_includes @user.hidden_books_in_common_with(friend), book
refute_includes @user.hidden_movies_in_common_with(friend), book
refute_includes @user.hidden_books_in_common_with(friend), @movie
+ refute_includes @user.hidden_books_in_common_with(friend), @doc
end
def teardown
View
31 test/recommendable/rater/liker_test.rb
@@ -5,6 +5,7 @@ class LikerTest < MiniTest::Unit::TestCase
def setup
@user = Factory(:user)
@movie = Factory(:movie)
+ @doc = Factory(:documentary)
end
def test_that_like_adds_to_liked_set
@@ -13,6 +14,12 @@ def test_that_like_adds_to_liked_set
assert_includes @user.liked_movie_ids, @movie.id
end
+ def test_that_like_adds_subclass_likes_to_liked_set
+ refute_includes @user.liked_movie_ids, @doc.id
+ @user.like(@doc)
+ assert_includes @user.liked_movie_ids, @doc.id
+ end
+
def test_that_cant_like_already_liked_object
assert @user.like(@movie)
assert_nil @user.like(@movie)
@@ -39,6 +46,13 @@ def test_that_unlike_removes_item_from_liked_set
refute_includes @user.liked_movie_ids, @movie.id
end
+ def test_that_unlike_removes_subclass_item_from_liked_set
+ @user.like(@doc)
+ assert_includes @user.liked_movie_ids, @doc.id
+ @user.unlike(@doc)
+ refute_includes @user.liked_movie_ids, @doc.id
+ end
+
def test_that_cant_unlike_item_unless_liked
assert_nil @user.unlike(@movie)
end
@@ -47,6 +61,10 @@ def test_that_likes_returns_liked_records
refute_includes @user.likes, @movie
@user.like(@movie)
assert_includes @user.likes, @movie
+
+ refute_includes @user.likes, @doc
+ @user.like(@doc)
+ assert_includes @user.likes, @doc
end
def test_that_dynamic_liked_finder_only_returns_relevant_records
@@ -65,8 +83,9 @@ def test_that_likes_count_counts_all_likes
@user.like(@movie)
@user.like(movie2)
@user.like(book)
+ @user.like(@doc)
- assert_equal @user.likes_count, 3
+ assert_equal @user.likes_count, 4
end
def test_that_dynamic_liked_count_methods_only_count_relevant_likes
@@ -75,9 +94,10 @@ def test_that_dynamic_liked_count_methods_only_count_relevant_likes
@user.like(@movie)
@user.like(movie2)
+ @user.like(@doc)
@user.like(book)
- assert_equal @user.liked_movies_count, 2
+ assert_equal @user.liked_movies_count, 3
assert_equal @user.liked_books_count, 1
end
@@ -90,11 +110,14 @@ def test_that_likes_in_common_with_returns_all_common_likes
@user.like(@movie)
@user.like(book)
@user.like(movie2)
+ @user.like(@doc)
friend.like(@movie)
friend.like(book)
friend.like(book2)
+ friend.like(@doc)
assert_includes @user.likes_in_common_with(friend), @movie
+ assert_includes @user.likes_in_common_with(friend), @doc
assert_includes @user.likes_in_common_with(friend), book
refute_includes @user.likes_in_common_with(friend), movie2
refute_includes friend.likes_in_common_with(@user), book2
@@ -106,14 +129,18 @@ def test_that_dynamic_liked_in_common_with_only_returns_relevant_records
book = Factory(:book)
@user.like(@movie)
+ @user.like(@doc)
@user.like(book)
friend.like(@movie)
+ friend.like(@doc)
friend.like(book)
assert_includes @user.liked_movies_in_common_with(friend), @movie
+ assert_includes @user.liked_movies_in_common_with(friend), @doc
assert_includes @user.liked_books_in_common_with(friend), book
refute_includes @user.liked_movies_in_common_with(friend), book
refute_includes @user.liked_books_in_common_with(friend), @movie
+ refute_includes @user.liked_books_in_common_with(friend), @doc
end
def teardown
View
3  test/recommendable/rater/recommender_test.rb
@@ -5,7 +5,8 @@ class RecommenderTest < MiniTest::Unit::TestCase
def setup
@user = Factory(:user)
5.times { |x| instance_variable_set(:"@user#{x+1}", Factory(:user)) }
- 10.times { |x| instance_variable_set(:"@movie#{x+1}", Factory(:movie)) }
+ 5.times { |x| instance_variable_set(:"@movie#{x+1}", Factory(:movie)) }
+ 5.upto(9) { |x| instance_variable_set(:"@movie#{x+1}", Factory(:documentary)) }
10.times { |x| instance_variable_set(:"@book#{x+1}", Factory(:book)) }
[@movie1, @movie2, @movie3, @book4, @book5, @book6].each { |obj| @user.like(obj) }
Something went wrong with that request. Please try again.