recommendable 0.2.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.markdown CHANGED
@@ -1,8 +1,27 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- 0.2.0 (current version)
5
- -----------------------
4
+ 1.0.0 (current version)
5
+ ---------------------
6
+ * Dynamic finders now return ActiveRecord::Relations! This means you can chain other ActiveRecord query methods like so:
7
+
8
+ ```ruby
9
+ current_user.recommended_posts.where(:category => "technology")
10
+ current_user.liked_movies.limit(10)
11
+ current_user.stashed_books.where(:author => "Cormac McCarthy")
12
+ current_user.disliked_shows.joins(:cast_members).where('cast_members.name = Kim Kardashian')
13
+ ```
14
+
15
+ * You can now specify a count for `User#recommendations`:
16
+
17
+ ```ruby
18
+ current_user.recommendations(10)
19
+ ```
20
+
21
+ * Bug fixes
22
+
23
+ 0.2.0
24
+ -----
6
25
  * NOTE: This release is NOT backwards compatible. Please migrate your databases:
7
26
 
8
27
  ```
data/Gemfile CHANGED
@@ -1,17 +1,17 @@
1
- source "http://rubygems.org"
1
+ source 'http://rubygems.org'
2
2
  # Add dependencies required to use your gem here.
3
- gem "rails", ">= 3.1.0"
4
- gem "redis", "~> 2.2.0"
5
- gem "resque", "~> 1.19.0"
6
- gem "resque-loner", "~> 1.2.0"
3
+ gem 'rails', '>= 3.1.0'
4
+ gem 'redis', '>= 2.2.0'
5
+ gem 'resque', '~> 1.19.0'
6
+ gem 'resque-loner', '~> 1.2.0'
7
7
 
8
8
  # Add dependencies to develop your gem here.
9
9
  # Include everything needed to run rake, tests, features, etc.
10
10
  group :development do
11
- gem "sqlite3"
12
- gem "minitest"
13
- gem "shoulda"
14
- gem "miniskirt"
15
- gem "yard", "~> 0.6.0"
16
- gem "bundler", ">= 1.0.0"
11
+ gem 'sqlite3'
12
+ gem 'minitest'
13
+ gem 'shoulda'
14
+ gem 'miniskirt'
15
+ gem 'yard', '~> 0.6.0'
16
+ gem 'bundler', '>= 1.0.0'
17
17
  end
data/Gemfile.lock CHANGED
@@ -110,7 +110,7 @@ DEPENDENCIES
110
110
  miniskirt
111
111
  minitest
112
112
  rails (>= 3.1.0)
113
- redis (~> 2.2.0)
113
+ redis (>= 2.2.0)
114
114
  resque (~> 1.19.0)
115
115
  resque-loner (~> 1.2.0)
116
116
  shoulda
data/README.markdown CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Recommendable is an engine for Rails 3 applications to quickly add the ability for your users to Like/Dislike items and receive recommendations for new items. It uses Redis to store your recommendations and keep them sorted by how good the recommendation is.
4
4
 
5
+ Requirements
6
+ ------------
7
+ * Ruby 1.9.2 (1.9.x if using from git. May run on 1.8.7, I don't know)
8
+ * Rails 3.x
9
+
5
10
  Installation
6
11
  ------------
7
12
 
@@ -8,6 +8,7 @@ module Recommendable
8
8
 
9
9
  validates :user_id, :uniqueness => { :scope => [:ignorable_id, :ignorable_type],
10
10
  :message => "has already ignored this item" }
11
+
11
12
  def ignorable_type=(sType)
12
13
  super sType.to_s.classify.constantize.base_class.to_s
13
14
  end
@@ -8,6 +8,7 @@ module Recommendable
8
8
 
9
9
  validates :user_id, :uniqueness => { :scope => [:stashable_id, :stashable_type],
10
10
  :message => "has already stashed this item" }
11
+
11
12
  def stashable_type=(sType)
12
13
  super sType.to_s.classify.constantize.base_class.to_s
13
14
  end
@@ -24,7 +24,7 @@ module Recommendable
24
24
  include LikeableMethods
25
25
  include DislikeableMethods
26
26
 
27
- before_destroy :remove_from_scores
27
+ before_destroy :remove_from_scores, :remove_from_recommendations
28
28
 
29
29
  def self.acts_as_recommendable?() true end
30
30
 
@@ -42,10 +42,9 @@ module Recommendable
42
42
  ids = Recommendable.redis.zrevrange(self.score_set, 0, count - 1).map(&:to_i)
43
43
 
44
44
  items = self.find ids
45
+ return items.first if count == 1
45
46
 
46
- return items.sort do |x, y|
47
- ids.index(x.id) <=> ids.index(y.id)
48
- end
47
+ return items.sort { |x, y| ids.index(x.id) <=> ids.index(y.id) }
49
48
  end
50
49
 
51
50
  private
@@ -66,6 +65,12 @@ module Recommendable
66
65
  Recommendable.redis.zrem self.class.score_set, self.id
67
66
  true
68
67
  end
68
+
69
+ def remove_from_recommendations
70
+ Recommendable.user_class.find_each do |user|
71
+ user.send :completely_unrecommend, self
72
+ end
73
+ end
69
74
 
70
75
  # Used for setup purposes. Calls convenience methods to create sets
71
76
  # in redis of users that both like and dislike this object.
@@ -100,7 +105,9 @@ module Recommendable
100
105
 
101
106
  def acts_as_recommendable?() false end
102
107
 
103
- def sti?() self.base_class != self end
108
+ def sti?
109
+ self.base_class != self && self.base_class.table_name == self.table_name
110
+ end
104
111
 
105
112
  private
106
113
  end
@@ -19,15 +19,14 @@ module Recommendable
19
19
  has_many :dislikes, :class_name => "Recommendable::Dislike", :dependent => :destroy, :foreign_key => :user_id
20
20
  has_many :ignores, :class_name => "Recommendable::Ignore", :dependent => :destroy, :foreign_key => :user_id
21
21
  has_many :stashed_items, :class_name => "Recommendable::Stash", :dependent => :destroy, :foreign_key => :user_id
22
-
22
+
23
23
  include LikeMethods
24
24
  include DislikeMethods
25
25
  include StashMethods
26
26
  include IgnoreMethods
27
27
  include RecommendationMethods
28
28
 
29
- before_destroy :remove_from_similarities
30
- before_destroy :remove_recommendations
29
+ before_destroy :remove_from_similarities, :remove_recommendations
31
30
 
32
31
  def method_missing method, *args, &block
33
32
  if method.to_s =~ /^(liked|disliked)_(.+)_in_common_with$/
@@ -42,7 +41,7 @@ module Recommendable
42
41
  begin
43
42
  super unless $2.classify.constantize.acts_as_recommendable?
44
43
 
45
- self.send "#{$1}_for", $2.classify.constantize
44
+ self.send "#{$1}_for", $2.classify.constantize, *args
46
45
  rescue NameError
47
46
  super
48
47
  end
@@ -52,8 +51,7 @@ module Recommendable
52
51
  end
53
52
 
54
53
  def respond_to? method, include_private = false
55
- if method.to_s =~ /^(liked|disliked|ignored|stashed|recommended)_(.+)$/ || \
56
- method.to_s =~ /^common_(liked|disliked)_(.+)_with$/
54
+ if method.to_s =~ /^(liked|disliked|ignored|stashed|recommended)_(.+)$/ || method.to_s =~ /^common_(liked|disliked)_(.+)_with$/
57
55
  begin
58
56
  $2.classify.constantize.acts_as_recommendable?
59
57
  rescue NameError
@@ -112,7 +110,7 @@ module Recommendable
112
110
 
113
111
  # @return [Array] an array of ActiveRecord objects that self has liked
114
112
  def liked
115
- Recommendable.recommendable_classes.flat_map { |klass| liked_for klass }
113
+ Recommendable.recommendable_classes.map { |klass| liked_for klass }.flatten
116
114
  end
117
115
 
118
116
  private
@@ -121,15 +119,15 @@ module Recommendable
121
119
  # likes.
122
120
  #
123
121
  # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
124
- # @return [Array] an array of ActiveRecord objects that self has liked belonging to klass
122
+ # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has liked
125
123
  def liked_for klass
126
- liked = if klass.sti?
127
- likes.joins manual_join(klass, 'like')
124
+ ids = if klass.sti?
125
+ likes.joins(manual_join(klass, 'like')).map(&:likeable_id)
128
126
  else
129
- likes.where(:likeable_type => klass.to_s).includes(:likeable)
127
+ likes.where(:likeable_type => klass.to_s).map(&:likeable_id)
130
128
  end
131
129
 
132
- liked.map(&:likeable)
130
+ klass.where('ID IN (?)', ids)
133
131
  end
134
132
 
135
133
  # Get a list of Recommendable::Likes with a `#likeable_type` of the passed
@@ -190,7 +188,7 @@ module Recommendable
190
188
 
191
189
  # @return [Array] an array of ActiveRecord objects that self has disliked
192
190
  def disliked
193
- Recommendable.recommendable_classes.flat_map { |klass| disliked_for klass }
191
+ Recommendable.recommendable_classes.map { |klass| disliked_for klass }.flatten
194
192
  end
195
193
 
196
194
  private
@@ -199,15 +197,15 @@ module Recommendable
199
197
  # dislikes.
200
198
  #
201
199
  # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
202
- # @return [Array] an array of ActiveRecord objects that self has disliked belonging to klass
200
+ # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has disliked
203
201
  def disliked_for klass
204
- disliked = if klass.sti?
205
- dislikes.joins manual_join(klass, 'dislike')
202
+ ids = if klass.sti?
203
+ dislikes.joins(manual_join(klass, 'dislike')).map(&:dislikeable_id)
206
204
  else
207
- dislikes.where(:dislikeable_type => klass.to_s).includes(:dislikeable)
205
+ dislikes.where(:dislikeable_type => klass.to_s).map(&:dislikeable_id)
208
206
  end
209
207
 
210
- disliked.map(&:dislikeable)
208
+ klass.where('ID IN (?)', ids)
211
209
  end
212
210
 
213
211
  # Get a list of Recommendable::Dislikes with a `#dislikeable_type` of the
@@ -263,7 +261,7 @@ module Recommendable
263
261
  #
264
262
  # @return [Array] an array of ActiveRecord objects that self has stashed
265
263
  def stashed
266
- Recommendable.recommendable_classes.flat_map { |klass| stashed_for klass }
264
+ Recommendable.recommendable_classes.map { |klass| stashed_for klass }.flatten
267
265
  end
268
266
 
269
267
  private
@@ -272,15 +270,15 @@ module Recommendable
272
270
  # has stashed away for later.
273
271
  #
274
272
  # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
275
- # @return [Array] an array of ActiveRecord objects that self has stashed belonging to klass
273
+ # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has stashed
276
274
  def stashed_for klass
277
- stashed = if klass.sti?
278
- stashed_items.joins manual_join(klass, 'stash')
275
+ ids = if klass.sti?
276
+ stashed_items.joins(manual_join(klass, 'stash')).map(&:stashable_id)
279
277
  else
280
- stashed_items.where(:stashable_type => klass.to_s).includes(:stashable)
278
+ stashed_items.where(:stashable_type => klass.to_s).map(&:stashable_id)
281
279
  end
282
280
 
283
- stashed.map(&:stashable)
281
+ klass.where('ID IN (?)', ids)
284
282
  end
285
283
  end
286
284
 
@@ -321,7 +319,7 @@ module Recommendable
321
319
 
322
320
  # @return [Array] an array of ActiveRecord objects that self has ignored
323
321
  def ignored
324
- Recommendable.recommendable_classes.flat_map { |klass| ignored_for klass }
322
+ Recommendable.recommendable_classes.map { |klass| ignored_for klass }.flatten
325
323
  end
326
324
 
327
325
  private
@@ -330,15 +328,15 @@ module Recommendable
330
328
  # currently ignoring.
331
329
  #
332
330
  # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
333
- # @return [Array] an array of ActiveRecord objects that self has ignored belonging to klass
331
+ # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has ignored
334
332
  def ignored_for klass
335
- ignored = if klass.sti?
336
- ignores.joins manual_join(klass, 'ignore')
333
+ ids = if klass.sti?
334
+ ignores.joins(manual_join(klass, 'ignore')).map(&:ignoreable_id)
337
335
  else
338
- ignores.where(:ignorable_type => klass.to_s).includes(:ignorable)
336
+ ignores.where(:ignoreable_type => klass.to_s).map(&:ignoreable_id)
339
337
  end
340
338
 
341
- ignored.map(&:ignorable)
339
+ klass.where('ID IN (?)', ids)
342
340
  end
343
341
  end
344
342
 
@@ -406,45 +404,42 @@ module Recommendable
406
404
  # Recommendations are returned in a descending order with the first index
407
405
  # being the object that self has been found most likely to enjoy.
408
406
  #
409
- # @param [Hash] options the options for returning this list
410
- # @option options [Fixnum] :count (10) the number of recommendations to get
407
+ # @param [Fixnum] count the number of recmomendations to return
411
408
  # @return [Array] an array of ActiveRecord objects that are recommendable
412
- def recommendations options = {}
409
+ def recommendations count = 10
413
410
  return [] if likes.count + dislikes.count == 0
414
411
 
415
412
  unioned_predictions = "#{self.class}:#{id}:predictions"
416
413
  Recommendable.redis.zunionstore unioned_predictions, Recommendable.recommendable_classes.map { |klass| predictions_set_for klass }
417
414
 
418
- recommendations = Recommendable.redis.zrevrange(unioned_predictions, 0, 10).map do |object|
415
+ recommendations = Recommendable.redis.zrevrange(unioned_predictions, 0, count - 1).map do |object|
419
416
  klass, id = object.split(":")
420
417
  klass.constantize.find(id)
421
418
  end
422
419
 
420
+ recommendations = recommendations.first if count == 1
421
+
423
422
  Recommendable.redis.del(unioned_predictions) and return recommendations
424
423
  end
425
424
 
426
- # Get a list of 10 recommendations for self on a single recommendable type.
425
+ # Get a list of recommendations for self on a single recommendable type.
427
426
  # Recommendations are returned in a descending order with the first index
428
427
  # being the object that self has been found most likely to enjoy.
429
428
  #
430
429
  # @param [Class, String, Symbol] klass the class to receive recommendations for. Can be the class constant, or a String/Symbol representation of the class name.
431
- # @return [Array] an array of ActiveRecord objects that are recommendable
432
- def recommended_for klass
433
- return [] if likes_for(klass.base_class).count + dislikes_for(klass.base_class).count == 0 || \
434
- Recommendable.redis.zcard(predictions_set_for(klass)) == 0
435
-
436
- recommendations = []
437
- i = 0
438
- until recommendations.size == 10
430
+ # @return [ActiveRecord::Relation] an ActiveRecord::Relation of recommendations
431
+ def recommended_for klass, count = 10
432
+ return [] if likes_for(klass.base_class).count + dislikes_for(klass.base_class).count == 0 || Recommendable.redis.zcard(predictions_set_for(klass)) == 0
433
+ ids = []
434
+
435
+ (0...count).each do |i|
439
436
  prediction = Recommendable.redis.zrevrange(predictions_set_for(klass), i, i).first
440
- return recommendations unless prediction # User might not have enough recommendations to return
437
+ break unless prediction
441
438
 
442
- object = klass.to_s.classify.constantize.find(prediction.split(":")[1])
443
- recommendations << object
444
- i += 1
439
+ ids << prediction.split(":").last
445
440
  end
446
441
 
447
- return recommendations
442
+ return klass.to_s.classify.constantize.where('ID IN (?)', ids)
448
443
  end
449
444
 
450
445
  # Return the value calculated by {#predict} on self for a passed object.
@@ -481,9 +476,9 @@ module Recommendable
481
476
 
482
477
  if options[:class]
483
478
  in_common = Recommendable.redis.sinter likes_set_for(options[:class]), rater.likes_set_for(options[:class])
484
- in_common = options[:class].to_s.classify.constantize.find in_common if options[:return_records]
479
+ in_common = options[:class].to_s.classify.constantize.where('ID IN (?)', in_common) if options[:return_records]
485
480
  else
486
- in_common = Recommendable.recommendable_classes.flat_map do |klass|
481
+ in_common = Recommendable.recommendable_classes.map do |klass|
487
482
  things = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
488
483
 
489
484
  if options[:return_records]
@@ -492,9 +487,11 @@ module Recommendable
492
487
  things.map { |id| "#{klass.to_s.classify}:#{id}" }
493
488
  end
494
489
  end
490
+
491
+ in_common.flatten!
495
492
  end
496
493
 
497
- in_common
494
+ return in_common
498
495
  end
499
496
 
500
497
  # Makes a call to Redis and intersects the sets of dislikes belonging to
@@ -511,9 +508,9 @@ module Recommendable
511
508
 
512
509
  if options[:class]
513
510
  in_common = Recommendable.redis.sinter dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class])
514
- in_common = options[:class].to_s.classify.constantize.find in_common if options[:return_records]
511
+ in_common = options[:class].to_s.classify.constantize.where('ID IN (?)', in_common) if options[:return_records]
515
512
  else
516
- in_common = Recommendable.recommendable_classes.flat_map do |klass|
513
+ in_common = Recommendable.recommendable_classes.map do |klass|
517
514
  things = Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
518
515
 
519
516
  if options[:return_records]
@@ -522,6 +519,8 @@ module Recommendable
522
519
  things.map { |id| "#{klass.to_s.classify}:#{id}" }
523
520
  end
524
521
  end
522
+
523
+ in_common.flatten!
525
524
  end
526
525
 
527
526
  in_common
@@ -544,9 +543,9 @@ module Recommendable
544
543
  if options[:class]
545
544
  disagreements = Recommendable.redis.sinter(likes_set_for(options[:class]), rater.dislikes_set_for(options[:class]))
546
545
  disagreements += Recommendable.redis.sinter(dislikes_set_for(options[:class]), rater.likes_set_for(options[:class]))
547
- disagreements = options[:class].to_s.classify.constantize.find disagreements if options[:return_records]
546
+ disagreements = options[:class].to_s.classify.constantize.where('ID IN (?)', disagreements) if options[:return_records]
548
547
  else
549
- disagreements = Recommendable.recommendable_classes.flat_map do |klass|
548
+ disagreements = Recommendable.recommendable_classes.map do |klass|
550
549
  things = Recommendable.redis.sinter(likes_set_for(klass), rater.dislikes_set_for(klass))
551
550
  things += Recommendable.redis.sinter(dislikes_set_for(klass), rater.likes_set_for(klass))
552
551
 
@@ -556,6 +555,8 @@ module Recommendable
556
555
  things.map { |id| "#{options[:class].to_s.classify}:#{id}" }
557
556
  end
558
557
  end
558
+
559
+ disagreements.flatten!
559
560
  end
560
561
 
561
562
  disagreements
@@ -569,11 +570,8 @@ module Recommendable
569
570
  # param [Object] object the object to destroy Recommendable models for
570
571
  # @private
571
572
  def completely_unrecommend object
572
- unlike object
573
- undislike object
574
- unstash object
575
- unignore object
576
- unpredict object
573
+ unlike(object) || undislike(object) || unstash(object) || unignore(object)
574
+ unpredict(object)
577
575
  end
578
576
 
579
577
  # @private
@@ -2,7 +2,8 @@ module Recommendable
2
2
  module Helpers
3
3
  def manual_join(klass, action)
4
4
  table = klass.base_class.table_name
5
- "JOIN #{table} ON recommendable_#{action.pluralize}.#{action}able_id = #{table}.id AND #{table}.type = '#{klass}'"
5
+ inheritance_column = klass.base_class.inheritance_column
6
+ "JOIN #{table} ON recommendable_#{action.pluralize}.#{action}able_id = #{table}.id AND #{table}.#{inheritance_column} = '#{klass}'"
6
7
  end
7
8
  end
8
9
  end
@@ -1,3 +1,3 @@
1
1
  module Recommendable
2
- VERSION = '0.2.1.1'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -20,6 +20,19 @@ class MovieSpec < MiniTest::Spec
20
20
 
21
21
  Movie.top(2).wont_include @movie2
22
22
  end
23
+
24
+ it "should be removed from recommendations" do
25
+ @user2 = Factory(:user)
26
+ @user1.like @movie1
27
+ @user2.like @movie1
28
+ @user2.like @movie2
29
+
30
+ @user1.send :update_recommendations
31
+ @movie2.destroy
32
+
33
+ @user2.liked.size.must_equal 1
34
+ @user1.recommendations.size.must_equal 0
35
+ end
23
36
  end
24
37
 
25
38
  describe ".top" do
@@ -34,7 +34,7 @@ class UserSpec < MiniTest::Spec
34
34
  end
35
35
  end
36
36
 
37
- describe "that does not act_as_recommendable" do
37
+ describe "that does not act_as_recommended_to" do
38
38
  before :each do
39
39
  @user = Factory(:bully)
40
40
  @movie = Factory(:movie)
@@ -186,7 +186,7 @@ class UserSpec < MiniTest::Spec
186
186
  @user.stashed?(@movie).must_equal true
187
187
  end
188
188
 
189
- 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
189
+ it "should not be able to rate or ignore an item that is not recommendable." do
190
190
  @cakephp = Factory(:php_framework)
191
191
 
192
192
  proc { @user.like(@cakephp) }.must_raise Recommendable::RecordNotRecommendableError
@@ -235,6 +235,27 @@ class UserSpec < MiniTest::Spec
235
235
  @user.unstash(@movie).must_equal true
236
236
  end
237
237
  end
238
+
239
+ describe "while using finders" do
240
+ it "should return ActiveRecord::Relations" do
241
+ @user = Factory(:user)
242
+ @movie1 = Factory(:movie)
243
+ @movie2 = Factory(:movie)
244
+ @movie3 = Factory(:movie)
245
+ @movie4 = Factory(:movie)
246
+ @movie5 = Factory(:movie)
247
+
248
+ @user.like @movie1
249
+ @user.like @movie2
250
+ @user.dislike @movie3
251
+
252
+ @user.liked_movies.must_be_instance_of ActiveRecord::Relation
253
+ @user.liked_movies.where(:title => @movie1.title).must_include @movie1
254
+ @user.liked_movies.where(:title => @movie2.title).wont_include @movie1
255
+ @user.liked_movies.limit(1).size.must_equal 1
256
+ @user.disliked_movies.where(:title => @movie5.title).must_be_empty
257
+ end
258
+ end
238
259
 
239
260
  describe "while getting recommendations" do
240
261
  before :each do
@@ -255,6 +276,37 @@ class UserSpec < MiniTest::Spec
255
276
  Recommendable.redis.del "User:#{@frank.id}:predictions:Movie"
256
277
  end
257
278
 
279
+ it "should respect passed counts" do
280
+ @dave.like @movie1
281
+ @frank.like @movie1
282
+ @frank.like @movie2
283
+ @frank.like @movie3
284
+ @frank.like @movie4
285
+ @frank.like @movie5
286
+
287
+ @dave.send :update_similarities and @dave.send :update_recommendations
288
+
289
+ @dave.recommendations.size.must_equal 4
290
+ @dave.recommendations(2).size.must_equal 2
291
+ end
292
+
293
+ it "should return an ActiveRecord::Relation when using the dynamic finder" do
294
+ @dave.like @movie1
295
+ @frank.like @movie1
296
+ @frank.like @movie2
297
+ @frank.like @movie3
298
+ @frank.like @movie4
299
+ @frank.like @movie5
300
+
301
+ @dave.send :update_similarities and @dave.send :update_recommendations
302
+
303
+ @dave.recommended_movies.limit(2).size.must_equal 2
304
+ @dave.recommended_movies.where(:title => @movie3.title).must_include @movie3
305
+ @dave.recommended_movies.where(:title => @movie3.title).wont_include @movie2
306
+ @dave.recommended_movies.where(:title => @movie3.title).wont_include @movie4
307
+ @dave.recommended_movies.where(:title => @movie3.title).wont_include @movie5
308
+ end
309
+
258
310
  it "should have common likes with a friend" do
259
311
  @dave.like @movie1
260
312
  @dave.like @movie2
@@ -295,6 +347,19 @@ class UserSpec < MiniTest::Spec
295
347
  @dave.similar_raters.must_include @frank
296
348
  @dave.recommended_movies.must_include @movie2
297
349
  end
350
+
351
+ it "should return only the number of specified recommendations" do
352
+ @dave.like(@movie1)
353
+ @frank.like(@movie1)
354
+ @frank.like(@movie2)
355
+ @frank.like(@movie3)
356
+ @frank.like(@movie4)
357
+ @dave.send :update_similarities
358
+ @dave.send :update_recommendations
359
+
360
+ @dave.recommendations(2).size.must_equal 2
361
+ @dave.recommended_movies(1).size.must_equal 1
362
+ end
298
363
 
299
364
  it "should order similar users by similarity" do
300
365
  @dave.like(@movie1)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recommendable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1.1
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-12 00:00:00.000000000 Z
12
+ date: 2012-06-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sqlite3