recommendable 0.2.1.1 → 1.0.0

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.
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