Permalink
Browse files

Support subclassing ratables

This allows ratable classes to have subclasses that properly accumulate ratings
in the same ratable category.
  • Loading branch information...
1 parent f2357dd commit b01bc8b41ec22b2b63beb957475ce3f180084435 @aaronroyer aaronroyer committed Dec 26, 2013
@@ -4,7 +4,12 @@ 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(':')
+ [
+ Recommendable.config.redis_namespace,
+ Recommendable.config.user_class.to_s.tableize,
+ id,
+ "#{action}_#{ratable_class(klass).to_s.tableize}"
+ ].compact.join(':')
end
end
@@ -13,19 +18,25 @@ def similarity_set_for(id)
end
def liked_by_set_for(klass, id)
- [Recommendable.config.redis_namespace, klass.to_s.tableize, id, 'liked_by'].compact.join(':')
+ [Recommendable.config.redis_namespace, ratable_class(klass).to_s.tableize, 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(':')
+ [Recommendable.config.redis_namespace, ratable_class(klass).to_s.tableize, id, 'disliked_by'].compact.join(':')
end
def score_set_for(klass)
- [Recommendable.config.redis_namespace, klass.to_s.tableize, 'scores'].join(':')
+ [Recommendable.config.redis_namespace, ratable_class(klass).to_s.tableize, 'scores'].join(':')
end
def temp_set_for(klass, id)
- [Recommendable.config.redis_namespace, klass.to_s.tableize, id, 'temp'].compact.join(':')
+ [Recommendable.config.redis_namespace, ratable_class(klass).to_s.tableize, id, 'temp'].compact.join(':')
+ end
+
+ private
+
+ def ratable_class(klass)
+ klass.respond_to?(:ratable_class) ? klass.ratable_class : klass
end
end
end
@@ -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.
@@ -0,0 +1,3 @@
+# STI class that is not ratable
+class Boat < Vehicle
+end
@@ -0,0 +1,4 @@
+# STI ratable class whose base class is not ratable
+class Car < Vehicle
+end
+
@@ -0,0 +1,3 @@
+# STI subclass of a ratable class
+class Documentary < Movie
+end
@@ -1,4 +1,4 @@
class User < ActiveRecord::Base
attr_accessible :email if ::ActiveRecord::VERSION::MAJOR < 4
- recommends :movies, :books
+ recommends :movies, :books, :cars
end
@@ -0,0 +1,3 @@
+# STI base class that is not ratable
+class Vehicle < ActiveRecord::Base
+end
Binary file not shown.
@@ -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
@@ -0,0 +1,5 @@
+class AddTypeToMovie < ActiveRecord::Migration
+ def change
+ add_column :movies, :type, :string
+ end
+end
View
@@ -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
Binary file not shown.
View
@@ -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
@@ -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) }
@@ -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
@@ -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
Oops, something went wrong.

0 comments on commit b01bc8b

Please sign in to comment.