recommendable 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.markdown +19 -1
- data/README.markdown +8 -23
- data/lib/generators/recommendable/USAGE +1 -1
- data/lib/generators/recommendable/install_generator.rb +1 -8
- data/lib/generators/recommendable/templates/initializer.rb +1 -4
- data/lib/recommendable/acts_as_recommendable.rb +20 -18
- data/lib/recommendable/acts_as_recommended_to.rb +169 -189
- data/lib/recommendable/railtie.rb +0 -9
- data/lib/recommendable/version.rb +1 -1
- data/lib/recommendable.rb +1 -5
- data/spec/models/user_spec.rb +12 -12
- metadata +23 -23
data/CHANGELOG.markdown
CHANGED
@@ -1,8 +1,26 @@
|
|
1
1
|
Changelog
|
2
2
|
=========
|
3
3
|
|
4
|
-
0.1.
|
4
|
+
0.1.4 (current version)
|
5
5
|
-----------------------
|
6
|
+
* `acts_as_recommendable` is no longer needed in your models
|
7
|
+
* Instead of declaring `acts_as_recommended_to` in your User class, please use `recommends` instead, passing in a list of your recommendable models as a list of symbols (e.g. `recommends :movies, :books`)
|
8
|
+
* Your initializer should no longer declare the user class. This is no longer necessary and is deprecated.
|
9
|
+
* Fix an issue that caused the unnecessary need for eager loading models in development
|
10
|
+
* Removed aliases: `liked_records`, `liked_records_for`, `disliked_records`, and `disliked_records_for`
|
11
|
+
* Renamed methods:
|
12
|
+
* `has_ignored?` => `ignored?`
|
13
|
+
* `has_stashed?` => `stashed?`
|
14
|
+
* Code quality tweaks
|
15
|
+
|
16
|
+
0.1.3 (current version)
|
17
|
+
-----------------------
|
18
|
+
|
19
|
+
* Improvements to speed of similarity calculations.
|
20
|
+
* Added an instance method to items that act_as_recommendable, `rated_by`. This returns an array of users that like or dislike the item.
|
21
|
+
|
22
|
+
0.1.2
|
23
|
+
-----
|
6
24
|
|
7
25
|
* Fix an issue that could cause similarity values between users to be incorrect.
|
8
26
|
* `User#common_likes_with` and `User#common_dislikes_with` now return the actual Model instances by default. Accordingly, these methods are no longer private.
|
data/README.markdown
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# recommendable [![Build Status](https://secure.travis-ci.org/davidcelis/recommendable.png)](http://travis-ci.org/davidcelis/recommendable)
|
2
2
|
|
3
3
|
Recommendable is a Rails Engine to add Like/Dislike functionality to your
|
4
|
-
application. It uses Redis to generate recommendations quickly through a
|
5
|
-
collaborative filtering algorithm that I modified myself. Your users' tastes
|
4
|
+
application. It uses Redis to generate recommendations quickly through [a
|
5
|
+
collaborative filtering algorithm that I modified myself][6]. Your users' tastes
|
6
6
|
are compared with one another and used to give them great recommendations!
|
7
7
|
Yes, Redis is required. Scroll to the end of the README for more info on that.
|
8
8
|
|
@@ -30,7 +30,7 @@ Add the following to your Rails application's `Gemfile`:
|
|
30
30
|
After your `bundle install`, you can then run:
|
31
31
|
|
32
32
|
``` bash
|
33
|
-
$ rails g recommendable:install
|
33
|
+
$ rails g recommendable:install
|
34
34
|
```
|
35
35
|
|
36
36
|
After running the installation generator, you should double check
|
@@ -59,29 +59,13 @@ In your Rails model that represents your application's user:
|
|
59
59
|
|
60
60
|
``` ruby
|
61
61
|
class User < ActiveRecord::Base
|
62
|
-
|
62
|
+
recommends :movies, :shows, :other_things
|
63
63
|
|
64
64
|
# ...
|
65
65
|
end
|
66
66
|
```
|
67
67
|
|
68
|
-
|
69
|
-
|
70
|
-
``` ruby
|
71
|
-
class Movie < ActiveRecord::Base
|
72
|
-
acts_as_recommendable
|
73
|
-
|
74
|
-
# ...
|
75
|
-
end
|
76
|
-
|
77
|
-
class Show < ActiveRecord::Base
|
78
|
-
acts_as_recommendable
|
79
|
-
|
80
|
-
# ...
|
81
|
-
end
|
82
|
-
````
|
83
|
-
|
84
|
-
And that's it!
|
68
|
+
Just pass in a list of classes and that's it!
|
85
69
|
|
86
70
|
### Liking/Disliking
|
87
71
|
|
@@ -122,7 +106,7 @@ wish to return a set of liked or disliked objects for only one of those
|
|
122
106
|
models.
|
123
107
|
|
124
108
|
``` ruby
|
125
|
-
current_user.liked_for(Movie)
|
109
|
+
current_user.liked_for(Movie) # or "movie", or :movie
|
126
110
|
#=> [#<Movie name: '2001: A Space Odyssey', year: 1968>, #<Movie name: 'Back to the Future', :year => 1985>]
|
127
111
|
current_user.disliked_for(Show)
|
128
112
|
#=> []
|
@@ -163,7 +147,7 @@ current_user.like(movie_to_watch_later)
|
|
163
147
|
#=> true
|
164
148
|
current_user.stash(movie_to_watch_later)
|
165
149
|
#=> nil
|
166
|
-
current_user.
|
150
|
+
current_user.stashed?(movie_to_watch_later)
|
167
151
|
#=> false
|
168
152
|
```
|
169
153
|
|
@@ -344,3 +328,4 @@ further details.
|
|
344
328
|
[3]: http://wiki.github.com/defunkt/resque/contributing
|
345
329
|
[4]: http://help.github.com/forking/
|
346
330
|
[5]: http://help.github.com/pull-requests/
|
331
|
+
[6]: http://davidcelis.com/blog/2012/02/07/collaborative-filtering-with-likes-and-dislikes/
|
@@ -2,7 +2,7 @@ Description:
|
|
2
2
|
This generator will install Recommendable's initializer.rb file and migrate the Like and Dislike tables into your database unless specified.
|
3
3
|
|
4
4
|
Example:
|
5
|
-
rails generate recommendable:install
|
5
|
+
rails generate recommendable:install
|
6
6
|
|
7
7
|
This will create:
|
8
8
|
config/initializers/recommendable.rb
|
@@ -3,7 +3,6 @@ require 'rails/generators'
|
|
3
3
|
module Recommendable
|
4
4
|
module Generators
|
5
5
|
class InstallGenerator < Rails::Generators::Base
|
6
|
-
argument :user_model, :type => :string, :default => "User", :desc => "Your user model that will be liking and disliking objects."
|
7
6
|
argument :redis_host, :type => :string, :default => "localhost", :desc => "The hostname your redis server is running on."
|
8
7
|
argument :redis_port, :type => :string, :default => "6379", :desc => "The port your redis server is running on."
|
9
8
|
class_option :redis_socket, :type => :string, :desc => "Indicates the UNIX socket your redis server is running on (if it is)."
|
@@ -36,12 +35,6 @@ module Recommendable
|
|
36
35
|
def finished
|
37
36
|
puts "Done! Recommendable has been successfully installed. Please configure it in config/intializers/recommendable.rb"
|
38
37
|
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def user_class
|
43
|
-
user_model.camelize
|
44
|
-
end
|
45
38
|
end
|
46
39
|
end
|
47
|
-
end
|
40
|
+
end
|
@@ -2,9 +2,6 @@ require "redis"
|
|
2
2
|
require "resque"
|
3
3
|
require "resque-loner"
|
4
4
|
|
5
|
-
# What class will be liking/disliking objects and receiving recommendations?
|
6
|
-
Recommendable.user_class = "<%= user_class %>"
|
7
|
-
|
8
5
|
# Recommendable requires a connection to a running redis-server. Either create
|
9
6
|
# a new instance based on a host/port or UNIX socket, or pass in an existing
|
10
7
|
# Redis client instance.
|
@@ -15,4 +12,4 @@ Recommendable.user_class = "<%= user_class %>"
|
|
15
12
|
|
16
13
|
# Tell Redis which database to use (usually between 0 and 15). The default of 0
|
17
14
|
# is most likely okay unless you have another application using that database.
|
18
|
-
Recommendable.redis.select "0"
|
15
|
+
Recommendable.redis.select "0"
|
@@ -17,9 +17,10 @@ module Recommendable
|
|
17
17
|
include LikeableMethods
|
18
18
|
include DislikeableMethods
|
19
19
|
|
20
|
-
def self.acts_as_recommendable?
|
20
|
+
def self.acts_as_recommendable?() true end
|
21
21
|
|
22
|
-
|
22
|
+
|
23
|
+
def been_rated?
|
23
24
|
likes.count + dislikes.count > 0
|
24
25
|
end
|
25
26
|
|
@@ -28,13 +29,8 @@ module Recommendable
|
|
28
29
|
def rated_by
|
29
30
|
liked_by + disliked_by
|
30
31
|
end
|
31
|
-
|
32
|
-
|
33
|
-
# @return [Array] an array of user IDs
|
34
|
-
# @private
|
35
|
-
def rates_by
|
36
|
-
likes.map(&:user_id) + dislikes.map(&:user_id)
|
37
|
-
end
|
32
|
+
|
33
|
+
private
|
38
34
|
|
39
35
|
# Used for setup purposes. Calls convenience methods to create sets
|
40
36
|
# in redis of users that both like and dislike this object.
|
@@ -52,22 +48,30 @@ module Recommendable
|
|
52
48
|
Recommendable.redis.del "#{self.class}:#{id}:disliked_by"
|
53
49
|
end
|
54
50
|
|
55
|
-
|
56
|
-
|
51
|
+
# Returns an array of IDs of users that have liked or disliked this item.
|
52
|
+
# @return [Array] an array of user IDs
|
53
|
+
# @private
|
54
|
+
def rates_by
|
55
|
+
likes.map(&:user_id) + dislikes.map(&:user_id)
|
56
|
+
end
|
57
|
+
|
58
|
+
private :likes, :dislikes, :ignores, :stashes
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
60
|
-
def acts_as_recommendable?
|
62
|
+
def acts_as_recommendable?() false end
|
61
63
|
end
|
62
64
|
|
63
65
|
# Instance methods.
|
64
|
-
def recommendable?
|
66
|
+
def recommendable?() self.class.acts_as_recommendable? end
|
65
67
|
|
66
|
-
def redis_key
|
68
|
+
def redis_key() "#{self.class}:#{id}" end
|
67
69
|
|
68
70
|
protected :redis_key
|
69
71
|
|
70
72
|
module LikeableMethods
|
73
|
+
private
|
74
|
+
|
71
75
|
# Used for setup purposes. Creates a set in redis containing users that
|
72
76
|
# have liked this object.
|
73
77
|
# @private
|
@@ -77,11 +81,11 @@ module Recommendable
|
|
77
81
|
liked_by.each {|rater| Recommendable.redis.sadd set, rater.id}
|
78
82
|
return set
|
79
83
|
end
|
80
|
-
|
81
|
-
private :create_liked_by_set
|
82
84
|
end
|
83
85
|
|
84
86
|
module DislikeableMethods
|
87
|
+
private
|
88
|
+
|
85
89
|
# Used for setup purposes. Creates a set in redis containing users that
|
86
90
|
# have disliked this object.
|
87
91
|
# @private
|
@@ -91,8 +95,6 @@ module Recommendable
|
|
91
95
|
disliked_by.each {|rater| Recommendable.redis.sadd set, rater.id}
|
92
96
|
return set
|
93
97
|
end
|
94
|
-
|
95
|
-
private :create_disliked_by_set
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
@@ -5,8 +5,15 @@ module Recommendable
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
module ClassMethods
|
8
|
+
def recommends(*things)
|
9
|
+
acts_as_recommended_to
|
10
|
+
things.each { |thing| thing.to_s.classify.constantize.acts_as_recommendable }
|
11
|
+
end
|
12
|
+
|
8
13
|
def acts_as_recommended_to
|
9
14
|
class_eval do
|
15
|
+
Recommendable.user_class = self
|
16
|
+
|
10
17
|
has_many :likes, :class_name => "Recommendable::Like", :dependent => :destroy
|
11
18
|
has_many :dislikes, :class_name => "Recommendable::Dislike", :dependent => :destroy
|
12
19
|
has_many :ignores, :class_name => "Recommendable::Ignore", :dependent => :destroy
|
@@ -18,18 +25,11 @@ module Recommendable
|
|
18
25
|
include IgnoreMethods
|
19
26
|
include RecommendationMethods
|
20
27
|
|
21
|
-
def self.acts_as_recommended_to? ; true ; end
|
22
|
-
|
23
28
|
private :likes, :dislikes, :ignores, :stashed_items
|
24
29
|
end
|
25
30
|
end
|
26
|
-
|
27
|
-
def acts_as_recommended_to? ; false ; end
|
28
31
|
end
|
29
32
|
|
30
|
-
# Instance method.
|
31
|
-
def can_rate? ; self.class.acts_as_recommended_to? ; end
|
32
|
-
|
33
33
|
module LikeMethods
|
34
34
|
# Creates a Recommendable::Like to associate self to a passed object. If
|
35
35
|
# self is currently found to have disliked object, the corresponding
|
@@ -74,29 +74,26 @@ module Recommendable
|
|
74
74
|
likes.map {|like| like.likeable}
|
75
75
|
end
|
76
76
|
|
77
|
-
alias_method :liked_records, :liked
|
78
|
-
|
79
|
-
# Get a list of Recommendable::Likes with a `#likeable_type` of the passed
|
80
|
-
# class.
|
81
|
-
#
|
82
|
-
# @param [Class, String, Symbol] klass the class for which you would like to return self's likes. Can be the class constant, or a String/Symbol representation of the class name.
|
83
|
-
# @note You should not need to use this method. (see {#liked_for})
|
84
|
-
# @private
|
85
|
-
def likes_for(klass)
|
86
|
-
likes.where(:likeable_type => klassify(klass).to_s)
|
87
|
-
end
|
88
|
-
|
89
77
|
# Get a list of records belonging to a passed class that self currently
|
90
78
|
# likes.
|
91
79
|
#
|
92
80
|
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
93
81
|
# @return [Array] an array of ActiveRecord objects that self has liked belonging to klass
|
94
82
|
def liked_for(klass)
|
95
|
-
|
83
|
+
klass.to_s.classify.constantize.find likes_for(klass).map(&:likeable_id)
|
96
84
|
end
|
97
85
|
|
98
|
-
|
99
|
-
|
86
|
+
private
|
87
|
+
|
88
|
+
# Get a list of Recommendable::Likes with a `#likeable_type` of the passed
|
89
|
+
# class.
|
90
|
+
#
|
91
|
+
# @param [Class, String, Symbol] klass the class for which you would like to return self's likes. Can be the class constant, or a String/Symbol representation of the class name.
|
92
|
+
# @note You should not need to use this method. (see {#liked_for})
|
93
|
+
# @private
|
94
|
+
def likes_for(klass)
|
95
|
+
likes.where(:likeable_type => klass.to_s.classify)
|
96
|
+
end
|
100
97
|
end
|
101
98
|
|
102
99
|
module DislikeMethods
|
@@ -143,7 +140,16 @@ module Recommendable
|
|
143
140
|
dislikes.map {|dislike| dislike.dislikeable}
|
144
141
|
end
|
145
142
|
|
146
|
-
|
143
|
+
# Get a list of records belonging to a passed class that self currently
|
144
|
+
# dislikes.
|
145
|
+
#
|
146
|
+
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
147
|
+
# @return [Array] an array of ActiveRecord objects that self has disliked belonging to klass
|
148
|
+
def disliked_for(klass)
|
149
|
+
klass.to_s.classify.constantize.find dislikes_for(klass).map(&:dislikeable_id)
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
147
153
|
|
148
154
|
# Get a list of Recommendable::Dislikes with a `#dislikeable_type` of the
|
149
155
|
# passed class.
|
@@ -152,20 +158,8 @@ module Recommendable
|
|
152
158
|
# @note You should not need to use this method. (see {#disliked_for})
|
153
159
|
# @private
|
154
160
|
def dislikes_for(klass)
|
155
|
-
dislikes.where(:dislikeable_type =>
|
156
|
-
end
|
157
|
-
|
158
|
-
# Get a list of records belonging to a passed class that self currently
|
159
|
-
# dislikes.
|
160
|
-
#
|
161
|
-
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
162
|
-
# @return [Array] an array of ActiveRecord objects that self has disliked belonging to klass
|
163
|
-
def disliked_for(klass)
|
164
|
-
klassify(klass).find dislikes_for(klass).map(&:dislikeable_id)
|
161
|
+
dislikes.where(:dislikeable_type => klass.to_s.classify)
|
165
162
|
end
|
166
|
-
|
167
|
-
alias_method :disliked_records_for, :disliked_for
|
168
|
-
private :dislikes_for
|
169
163
|
end
|
170
164
|
|
171
165
|
module StashMethods
|
@@ -179,7 +173,7 @@ module Recommendable
|
|
179
173
|
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
180
174
|
def stash(object)
|
181
175
|
raise RecordNotRecommendableError unless object.recommendable?
|
182
|
-
return if
|
176
|
+
return if rated?(object) || stashed?(object)
|
183
177
|
unignore(object)
|
184
178
|
unpredict(object)
|
185
179
|
stashed_items.create!(:stashable_id => object.id, :stashable_type => object.class.to_s)
|
@@ -190,7 +184,7 @@ module Recommendable
|
|
190
184
|
#
|
191
185
|
# @param [Object] object the object you want to check
|
192
186
|
# @return true if self has stashed object, false if not
|
193
|
-
def
|
187
|
+
def stashed?(object)
|
194
188
|
stashed_items.exists?(:stashable_id => object.id, :stashable_type => object.class.to_s)
|
195
189
|
end
|
196
190
|
|
@@ -203,35 +197,32 @@ module Recommendable
|
|
203
197
|
end
|
204
198
|
|
205
199
|
# Get a list of records that self has currently stashed for later
|
206
|
-
|
200
|
+
#
|
207
201
|
# @return [Array] an array of ActiveRecord objects that self has stashed
|
208
202
|
def stashed
|
209
203
|
stashed_items.map {|item| item.stashable}
|
210
204
|
end
|
211
205
|
|
212
|
-
alias_method :stashed_records, :stashed
|
213
|
-
|
214
|
-
# Get a list of Recommendable::StashedItems with a stashable_type of the
|
215
|
-
# passed class.
|
216
|
-
#
|
217
|
-
# @param [Class, String, Symbol] klass the class for which you would like to return self's stashed items. Can be the class constant, or a String/Symbol representation of the class name.
|
218
|
-
# @note You should not need to use this method. (see {#stashed_for})
|
219
|
-
# @private
|
220
|
-
def stash_for(klass)
|
221
|
-
stashed_items.where(:stashable_type => klassify(klass).to_s)
|
222
|
-
end
|
223
|
-
|
224
206
|
# Get a list of records belonging to a passed class that self currently
|
225
207
|
# has stashed away for later.
|
226
208
|
#
|
227
209
|
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
228
210
|
# @return [Array] an array of ActiveRecord objects that self has stashed belonging to klass
|
229
211
|
def stashed_for(klass)
|
230
|
-
|
212
|
+
klass.to_s.classify.constantize.find stash_for(klass).map(&:stashable_id)
|
231
213
|
end
|
232
214
|
|
233
|
-
|
234
|
-
|
215
|
+
private
|
216
|
+
|
217
|
+
# Get a list of Recommendable::StashedItems with a stashable_type of the
|
218
|
+
# passed class.
|
219
|
+
#
|
220
|
+
# @param [Class, String, Symbol] klass the class for which you would like to return self's stashed items. Can be the class constant, or a String/Symbol representation of the class name.
|
221
|
+
# @note You should not need to use this method. (see {#stashed_for})
|
222
|
+
# @private
|
223
|
+
def stash_for(klass)
|
224
|
+
stashed_items.where(:stashable_type => klass.to_s.classify)
|
225
|
+
end
|
235
226
|
end
|
236
227
|
|
237
228
|
module IgnoreMethods
|
@@ -245,7 +236,7 @@ module Recommendable
|
|
245
236
|
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
246
237
|
def ignore(object)
|
247
238
|
raise RecordNotRecommendableError unless object.recommendable?
|
248
|
-
return if
|
239
|
+
return if ignored?(object)
|
249
240
|
completely_unrecommend(object)
|
250
241
|
ignores.create!(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
|
251
242
|
true
|
@@ -255,7 +246,7 @@ module Recommendable
|
|
255
246
|
#
|
256
247
|
# @param [Object] object the object you want to check
|
257
248
|
# @return true if self has ignored object, false if not
|
258
|
-
def
|
249
|
+
def ignored?(object)
|
259
250
|
ignores.exists?(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
|
260
251
|
end
|
261
252
|
|
@@ -274,7 +265,16 @@ module Recommendable
|
|
274
265
|
ignores.map {|ignore| ignore.ignoreable}
|
275
266
|
end
|
276
267
|
|
277
|
-
|
268
|
+
# Get a list of records belonging to a passed class that self is
|
269
|
+
# currently ignoring.
|
270
|
+
#
|
271
|
+
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
272
|
+
# @return [Array] an array of ActiveRecord objects that self has ignored belonging to klass
|
273
|
+
def ignored_for(klass)
|
274
|
+
klass.to_s.classify.constantize.find ignores_for(klass).map(&:ignoreable_id)
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
278
|
|
279
279
|
# Get a list of Recommendable::Ignores with a `#ignoreable_type` of the
|
280
280
|
# passed class.
|
@@ -283,20 +283,8 @@ module Recommendable
|
|
283
283
|
# @note You should not need to use this method. (see {#ignored_for})
|
284
284
|
# @private
|
285
285
|
def ignores_for(klass)
|
286
|
-
ignores.where(:ignoreable_type =>
|
286
|
+
ignores.where(:ignoreable_type => klass.to_s.classify)
|
287
287
|
end
|
288
|
-
|
289
|
-
# Get a list of records belonging to a passed class that self is
|
290
|
-
# currently ignoring.
|
291
|
-
#
|
292
|
-
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
293
|
-
# @return [Array] an array of ActiveRecord objects that self has ignored belonging to klass
|
294
|
-
def ignored_for(klass)
|
295
|
-
klassify(klass).find ignores_for(klass).map(&:ignoreable_id)
|
296
|
-
end
|
297
|
-
|
298
|
-
alias_method :ignored_records_for, :ignored_for
|
299
|
-
private :ignores_for
|
300
288
|
end
|
301
289
|
|
302
290
|
module RecommendationMethods
|
@@ -304,14 +292,14 @@ module Recommendable
|
|
304
292
|
#
|
305
293
|
# @param [Object] object the object you want to check
|
306
294
|
# @return true if self has liked or disliked object, false if not
|
307
|
-
def
|
295
|
+
def rated?(object)
|
308
296
|
likes?(object) || dislikes?(object)
|
309
297
|
end
|
310
298
|
|
311
299
|
# Checks to see if self has liked or disliked any objects yet.
|
312
300
|
#
|
313
301
|
# @return true if self has liked or disliked anything, false if not
|
314
|
-
def
|
302
|
+
def rated_anything?
|
315
303
|
likes.count > 0 || dislikes.count > 0
|
316
304
|
end
|
317
305
|
|
@@ -378,7 +366,7 @@ module Recommendable
|
|
378
366
|
prediction = Recommendable.redis.zrevrange(predictions_set_for(klass), i, i).first
|
379
367
|
return recommendations unless prediction # User might not have enough recommendations to return
|
380
368
|
|
381
|
-
object =
|
369
|
+
object = klass.to_s.classify.constantize.find(prediction.split(":")[1])
|
382
370
|
recommendations << object
|
383
371
|
i += 1
|
384
372
|
end
|
@@ -404,28 +392,6 @@ module Recommendable
|
|
404
392
|
-probability_of_liking(object)
|
405
393
|
end
|
406
394
|
|
407
|
-
# Checks how similar a passed rater is with self. This method calculates
|
408
|
-
# a numeric similarity value that can fall between -1.0 and 1.0. A value of
|
409
|
-
# 1.0 indicates that rater has the exact same likes and dislikes as self
|
410
|
-
# while a value of -1.0 indicates that rater dislikes every object that self
|
411
|
-
# likes and likes every object that self dislikes. A value of 0.0 would
|
412
|
-
# indicate that the two users share no likes or dislikes.
|
413
|
-
#
|
414
|
-
# @param [Object] rater an ActiveRecord object declared to `act_as_recommendable_to`
|
415
|
-
# @return [Float] the numeric similarity between self and rater
|
416
|
-
# @note The returned value relies on which user the method is called on. current_user.similarity_with(rater) will not equal rater.similarity_with(current_user) unless their sets of likes and dislikes are identical. current_user.similarity_with(rater) will return 1.0 even if rater has several likes/dislikes that `current_user` does not.
|
417
|
-
# @private
|
418
|
-
def similarity_with(rater)
|
419
|
-
rater.create_recommended_to_sets
|
420
|
-
agreements = common_likes_with(rater, :return_records => false).size
|
421
|
-
agreements += common_dislikes_with(rater, :return_records => false).size
|
422
|
-
disagreements = disagreements_with(rater, :return_records => false).size
|
423
|
-
|
424
|
-
similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
|
425
|
-
rater.destroy_recommended_to_sets
|
426
|
-
|
427
|
-
return similarity
|
428
|
-
end
|
429
395
|
# Makes a call to Redis and intersects the sets of likes belonging to self
|
430
396
|
# and rater.
|
431
397
|
#
|
@@ -441,15 +407,15 @@ module Recommendable
|
|
441
407
|
|
442
408
|
if options[:class]
|
443
409
|
in_common = Recommendable.redis.sinter likes_set_for(options[:class]), rater.likes_set_for(options[:class])
|
444
|
-
|
410
|
+
options[:class].to_s.classify.constantize.find in_common if options[:return_records]
|
445
411
|
else
|
446
412
|
Recommendable.recommendable_classes.flat_map do |klass|
|
447
413
|
in_common = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
|
448
414
|
|
449
415
|
if options[:return_records]
|
450
|
-
|
416
|
+
klass.to_s.classify.constantize.find in_common
|
451
417
|
else
|
452
|
-
in_common.map {|id| "#{
|
418
|
+
in_common.map {|id| "#{klass.to_s.classify}:#{id}"}
|
453
419
|
end
|
454
420
|
end
|
455
421
|
end
|
@@ -470,15 +436,15 @@ module Recommendable
|
|
470
436
|
|
471
437
|
if options[:class]
|
472
438
|
in_common = Recommendable.redis.sinter dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class])
|
473
|
-
|
439
|
+
options[:class].to_s.classify.constantize.find in_common if options[:return_records]
|
474
440
|
else
|
475
441
|
Recommendable.recommendable_classes.flat_map do |klass|
|
476
442
|
in_common = Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
|
477
443
|
|
478
444
|
if options[:return_records]
|
479
|
-
|
445
|
+
klass.to_s.classify.constantize.find in_common
|
480
446
|
else
|
481
|
-
in_common.map {|id| "#{
|
447
|
+
in_common.map {|id| "#{klass.to_s.classify}:#{id}"}
|
482
448
|
end
|
483
449
|
end
|
484
450
|
end
|
@@ -502,21 +468,116 @@ module Recommendable
|
|
502
468
|
if options[:class]
|
503
469
|
disagreements = Recommendable.redis.sinter(likes_set_for(options[:class]), rater.likes_set_for(options[:class]))
|
504
470
|
disagreements += Recommendable.redis.sinter(dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class]))
|
505
|
-
|
471
|
+
options[:class].to_s.classify.constantize.find disagreements if options[:return_records]
|
506
472
|
else
|
507
473
|
Recommendable.recommendable_classes.flat_map do |klass|
|
508
474
|
disagreements = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
|
509
475
|
disagreements += Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
|
510
476
|
|
511
477
|
if options[:return_records]
|
512
|
-
|
478
|
+
klass.to_s.classify.constantize.find disagreements
|
513
479
|
else
|
514
|
-
disagreements.map {|id| "#{
|
480
|
+
disagreements.map {|id| "#{klass.to_s.classify}:#{id}"}
|
515
481
|
end
|
516
482
|
end
|
517
483
|
end
|
518
484
|
end
|
485
|
+
|
486
|
+
# Used internally during liking/disliking/stashing/ignoring objects. This
|
487
|
+
# will prep an object to be liked, disliked, etc. by making sure that self
|
488
|
+
# doesn't already have this item in their list of likes, dislikes, stashed
|
489
|
+
# items or ignored items.
|
490
|
+
#
|
491
|
+
# param [Object] object the object to destroy Recommendable models for
|
492
|
+
# @private
|
493
|
+
def completely_unrecommend(object)
|
494
|
+
unlike(object)
|
495
|
+
undislike(object)
|
496
|
+
unstash(object)
|
497
|
+
unignore(object)
|
498
|
+
unpredict(object)
|
499
|
+
end
|
500
|
+
|
501
|
+
protected
|
502
|
+
|
503
|
+
# @private
|
504
|
+
def likes_set_for(klass)
|
505
|
+
"#{self.class}:#{id}:likes:#{klass}"
|
506
|
+
end
|
519
507
|
|
508
|
+
# @private
|
509
|
+
def dislikes_set_for(klass)
|
510
|
+
"#{self.class}:#{id}:dislikes:#{klass}"
|
511
|
+
end
|
512
|
+
|
513
|
+
# Used for setup purposes. Creates and populates sets in redis containing
|
514
|
+
# self's likes and dislikes.
|
515
|
+
# @private
|
516
|
+
def create_recommended_to_sets
|
517
|
+
Recommendable.recommendable_classes.each do |klass|
|
518
|
+
likes_for(klass).each {|like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
|
519
|
+
dislikes_for(klass).each {|dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
# Used for teardown purposes. Destroys the redis sets containing self's
|
524
|
+
# likes and dislikes, as they are only used during the process of
|
525
|
+
# updating recommendations and similarity values.
|
526
|
+
# @private
|
527
|
+
def destroy_recommended_to_sets
|
528
|
+
Recommendable.recommendable_classes.each do |klass|
|
529
|
+
Recommendable.redis.del likes_set_for(klass)
|
530
|
+
Recommendable.redis.del dislikes_set_for(klass)
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
private
|
535
|
+
|
536
|
+
# Checks how similar a passed rater is with self. This method calculates
|
537
|
+
# a numeric similarity value that can fall between -1.0 and 1.0. A value of
|
538
|
+
# 1.0 indicates that rater has the exact same likes and dislikes as self
|
539
|
+
# while a value of -1.0 indicates that rater dislikes every object that self
|
540
|
+
# likes and likes every object that self dislikes. A value of 0.0 would
|
541
|
+
# indicate that the two users share no likes or dislikes.
|
542
|
+
#
|
543
|
+
# @param [Object] rater an ActiveRecord object declared to `act_as_recommendable_to`
|
544
|
+
# @return [Float] the numeric similarity between self and rater
|
545
|
+
# @note The returned value relies on which user the method is called on. current_user.similarity_with(rater) will not equal rater.similarity_with(current_user) unless their sets of likes and dislikes are identical. current_user.similarity_with(rater) will return 1.0 even if rater has several likes/dislikes that `current_user` does not.
|
546
|
+
# @private
|
547
|
+
def similarity_with(rater)
|
548
|
+
rater.create_recommended_to_sets
|
549
|
+
agreements = common_likes_with(rater, :return_records => false).size
|
550
|
+
agreements += common_dislikes_with(rater, :return_records => false).size
|
551
|
+
disagreements = disagreements_with(rater, :return_records => false).size
|
552
|
+
|
553
|
+
similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
|
554
|
+
rater.destroy_recommended_to_sets
|
555
|
+
|
556
|
+
return similarity
|
557
|
+
end
|
558
|
+
|
559
|
+
# Used internally to update self's prediction values across all
|
560
|
+
# recommendable types. This is called in the Resque job to refresh
|
561
|
+
# recommendations.
|
562
|
+
#
|
563
|
+
# @private
|
564
|
+
def update_recommendations
|
565
|
+
Recommendable.recommendable_classes.each {|klass| update_recommendations_for klass}
|
566
|
+
end
|
567
|
+
|
568
|
+
# Used internally to update self's prediction values across a single
|
569
|
+
# recommendable type. Convenience method for {#update_recommendations}
|
570
|
+
#
|
571
|
+
# @param [Class] klass the recommendable type to update predictions for
|
572
|
+
# @private
|
573
|
+
def update_recommendations_for(klass)
|
574
|
+
klass.find_each do |object|
|
575
|
+
next if rated?(object) || !object.been_rated? || ignored?(object) || stashed?(object)
|
576
|
+
prediction = predict(object)
|
577
|
+
Recommendable.redis.zadd(predictions_set_for(object.class), prediction, object.redis_key) if prediction
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
520
581
|
# Predict how likely it is that self will like a passed in object. This
|
521
582
|
# probability is not based on percentage. 0.0 indicates that self will
|
522
583
|
# neither like nor dislike the passed object. Values that approach Infinity
|
@@ -548,52 +609,23 @@ module Recommendable
|
|
548
609
|
#
|
549
610
|
# @private
|
550
611
|
def update_similarities(rater_ids = nil)
|
551
|
-
return unless
|
612
|
+
return unless rated_anything?
|
552
613
|
create_recommended_to_sets
|
553
614
|
rater_ids ||= Recommendable.user_class.select(:id).map!(&:id)
|
554
615
|
|
555
616
|
Recommendable.user_class.find(rater_ids).each do |rater|
|
556
|
-
next if self == rater
|
617
|
+
next if self == rater
|
557
618
|
Recommendable.redis.zadd similarity_set, similarity_with(rater), rater.id
|
558
619
|
end
|
559
620
|
|
560
621
|
destroy_recommended_to_sets
|
561
622
|
end
|
562
623
|
|
563
|
-
# Used internally to update self's prediction values across all
|
564
|
-
# recommendable types. This is called in the Resque job to refresh
|
565
|
-
# recommendations.
|
566
|
-
#
|
567
|
-
# @private
|
568
|
-
def update_recommendations
|
569
|
-
Recommendable.recommendable_classes.each do |klass|
|
570
|
-
update_recommendations_for(klass)
|
571
|
-
end
|
572
|
-
end
|
573
|
-
|
574
|
-
# Used internally to update self's prediction values across a single
|
575
|
-
# recommendable type. Convenience method for {#update_recommendations}
|
576
|
-
#
|
577
|
-
# @param [Class] klass the recommendable type to update predictions for
|
578
624
|
# @private
|
579
|
-
def
|
580
|
-
|
581
|
-
next if has_rated?(object) || !object.has_been_rated? || has_ignored?(object) || has_stashed?(object)
|
582
|
-
prediction = predict(object)
|
583
|
-
Recommendable.redis.zadd(predictions_set_for(object.class), prediction, object.redis_key) if prediction
|
584
|
-
end
|
625
|
+
def unpredict(object)
|
626
|
+
Recommendable.redis.zrem predictions_set_for(object.class), object.redis_key
|
585
627
|
end
|
586
628
|
|
587
|
-
# @private
|
588
|
-
def likes_set_for(klass)
|
589
|
-
"#{self.class}:#{id}:likes:#{klass}"
|
590
|
-
end
|
591
|
-
|
592
|
-
# @private
|
593
|
-
def dislikes_set_for(klass)
|
594
|
-
"#{self.class}:#{id}:dislikes:#{klass}"
|
595
|
-
end
|
596
|
-
|
597
629
|
# @private
|
598
630
|
def similarity_set
|
599
631
|
"#{self.class}:#{id}:similarities"
|
@@ -603,58 +635,6 @@ module Recommendable
|
|
603
635
|
def predictions_set_for(klass)
|
604
636
|
"#{self.class}:#{id}:predictions:#{klass}"
|
605
637
|
end
|
606
|
-
|
607
|
-
# @private
|
608
|
-
def unpredict(object)
|
609
|
-
Recommendable.redis.zrem predictions_set_for(object.class), object.redis_key
|
610
|
-
end
|
611
|
-
|
612
|
-
# Used for setup purposes. Creates and populates sets in redis containing
|
613
|
-
# self's likes and dislikes.
|
614
|
-
# @private
|
615
|
-
def create_recommended_to_sets
|
616
|
-
Recommendable.recommendable_classes.each do |klass|
|
617
|
-
likes_for(klass).each {|like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
|
618
|
-
dislikes_for(klass).each {|dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
|
619
|
-
end
|
620
|
-
end
|
621
|
-
|
622
|
-
# Used for teardown purposes. Destroys the redis sets containing self's
|
623
|
-
# likes and dislikes, as they are only used during the process of
|
624
|
-
# updating recommendations and similarity values.
|
625
|
-
# @private
|
626
|
-
def destroy_recommended_to_sets
|
627
|
-
Recommendable.recommendable_classes.each do |klass|
|
628
|
-
Recommendable.redis.del likes_set_for(klass)
|
629
|
-
Recommendable.redis.del dislikes_set_for(klass)
|
630
|
-
end
|
631
|
-
end
|
632
|
-
|
633
|
-
# Used internally during liking/disliking/stashing/ignoring objects. This
|
634
|
-
# will prep an object to be liked, disliked, etc. by making sure that self
|
635
|
-
# doesn't already have this item in their list of likes, dislikes, stashed
|
636
|
-
# items or ignored items.
|
637
|
-
#
|
638
|
-
# param [Object] object the object to destroy Recommendable models for
|
639
|
-
# @private
|
640
|
-
def completely_unrecommend(object)
|
641
|
-
unlike(object)
|
642
|
-
undislike(object)
|
643
|
-
unstash(object)
|
644
|
-
unignore(object)
|
645
|
-
unpredict(object)
|
646
|
-
end
|
647
|
-
|
648
|
-
protected :likes_set_for, :dislikes_set_for, :create_recommended_to_sets,
|
649
|
-
:destroy_recommended_to_sets
|
650
|
-
|
651
|
-
private :similarity_set, :unpredict, :predictions_set_for,
|
652
|
-
:update_recommendations_for, :update_recommendations,
|
653
|
-
:update_similarities, :similarity_with, :predict
|
654
638
|
end
|
655
639
|
end
|
656
640
|
end
|
657
|
-
|
658
|
-
def klassify(klass)
|
659
|
-
(klass.is_a?(String) || klass.is_a?(Symbol)) ? klass.to_s.camelize.constantize : klass
|
660
|
-
end
|
@@ -2,14 +2,5 @@ module Recommendable
|
|
2
2
|
class Railtie < Rails::Railtie
|
3
3
|
ActiveRecord::Base.send :include, Recommendable::ActsAsRecommendedTo
|
4
4
|
ActiveRecord::Base.send :include, Recommendable::ActsAsRecommendable
|
5
|
-
|
6
|
-
# Force load models if in a non-development environment and not caching classes
|
7
|
-
config.after_initialize do |app|
|
8
|
-
force_load_models if !Rails.env.development? && !app.config.cache_classes
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.force_load_models
|
12
|
-
Dir["#{ Rails.root }/app/models/**/*.rb"].each { |m| load m }
|
13
|
-
end
|
14
5
|
end
|
15
6
|
end
|
data/lib/recommendable.rb
CHANGED
@@ -7,11 +7,7 @@ require 'recommendable/version'
|
|
7
7
|
|
8
8
|
module Recommendable
|
9
9
|
mattr_accessor :redis, :user_class
|
10
|
-
mattr_writer :
|
11
|
-
|
12
|
-
def self.user_class
|
13
|
-
@@user_class.camelize.constantize
|
14
|
-
end
|
10
|
+
mattr_writer :recommendable_classes
|
15
11
|
|
16
12
|
def self.recommendable_classes
|
17
13
|
@@recommendable_classes ||= []
|
data/spec/models/user_spec.rb
CHANGED
@@ -64,14 +64,14 @@ class UserSpec < MiniTest::Spec
|
|
64
64
|
it "should not be ignoring an item after liking it" do
|
65
65
|
@user.ignore(@movie)
|
66
66
|
@user.like(@movie).must_equal true
|
67
|
-
@user.
|
67
|
+
@user.ignored?(@movie).must_equal false
|
68
68
|
@user.likes?(@movie).must_equal true
|
69
69
|
end
|
70
70
|
|
71
71
|
it "should not have an item stashed after liking it" do
|
72
72
|
@user.stash(@movie)
|
73
73
|
@user.like(@movie).must_equal true
|
74
|
-
@user.
|
74
|
+
@user.stashed?(@movie).must_equal false
|
75
75
|
@user.likes?(@movie).must_equal true
|
76
76
|
end
|
77
77
|
|
@@ -97,14 +97,14 @@ class UserSpec < MiniTest::Spec
|
|
97
97
|
it "should not be ignoring an item after disliking it" do
|
98
98
|
@user.ignore(@movie)
|
99
99
|
@user.dislike(@movie).must_equal true
|
100
|
-
@user.
|
100
|
+
@user.ignored?(@movie).must_equal false
|
101
101
|
@user.dislikes?(@movie).must_equal true
|
102
102
|
end
|
103
103
|
|
104
104
|
it "should not have an item stashed after disliking it" do
|
105
105
|
@user.stash(@movie)
|
106
106
|
@user.dislike(@movie).must_equal true
|
107
|
-
@user.
|
107
|
+
@user.stashed?(@movie).must_equal false
|
108
108
|
@user.dislikes?(@movie).must_equal true
|
109
109
|
end
|
110
110
|
|
@@ -116,21 +116,21 @@ class UserSpec < MiniTest::Spec
|
|
116
116
|
@user.like(@movie)
|
117
117
|
@user.ignore(@movie).must_equal true
|
118
118
|
@user.likes?(@movie).must_equal false
|
119
|
-
@user.
|
119
|
+
@user.ignored?(@movie).must_equal true
|
120
120
|
end
|
121
121
|
|
122
122
|
it "should not dislike an item after ignoring it" do
|
123
123
|
@user.dislike(@movie)
|
124
124
|
@user.ignore(@movie).must_equal true
|
125
125
|
@user.dislikes?(@movie).must_equal false
|
126
|
-
@user.
|
126
|
+
@user.ignored?(@movie).must_equal true
|
127
127
|
end
|
128
128
|
|
129
129
|
it "should not have an item stashed after ignoring it" do
|
130
130
|
@user.stash(@movie)
|
131
131
|
@user.ignore(@movie).must_equal true
|
132
|
-
@user.
|
133
|
-
@user.
|
132
|
+
@user.stashed?(@movie).must_equal false
|
133
|
+
@user.ignored?(@movie).must_equal true
|
134
134
|
end
|
135
135
|
|
136
136
|
it "should be able to stash a recommendable item" do
|
@@ -141,21 +141,21 @@ class UserSpec < MiniTest::Spec
|
|
141
141
|
@user.like(@movie)
|
142
142
|
@user.stash(@movie).must_be_nil
|
143
143
|
@user.likes?(@movie).must_equal true
|
144
|
-
@user.
|
144
|
+
@user.stashed?(@movie).must_equal false
|
145
145
|
end
|
146
146
|
|
147
147
|
it "should not stash a disliked item" do
|
148
148
|
@user.dislike(@movie)
|
149
149
|
@user.stash(@movie).must_be_nil
|
150
150
|
@user.dislikes?(@movie).must_equal true
|
151
|
-
@user.
|
151
|
+
@user.stashed?(@movie).must_equal false
|
152
152
|
end
|
153
153
|
|
154
154
|
it "should not have an item ignored after stashing it" do
|
155
155
|
@user.ignore(@movie)
|
156
156
|
@user.stash(@movie).must_equal true
|
157
|
-
@user.
|
158
|
-
@user.
|
157
|
+
@user.ignored?(@movie).must_equal false
|
158
|
+
@user.stashed?(@movie).must_equal true
|
159
159
|
end
|
160
160
|
|
161
161
|
it "should not be able to rate or ignore an item that is not recommendable. doing so should not be enough to create Redis keys" do
|
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.1.
|
4
|
+
version: 0.1.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-02-09 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sqlite3
|
16
|
-
requirement: &
|
16
|
+
requirement: &70238356296840 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70238356296840
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: minitest
|
27
|
-
requirement: &
|
27
|
+
requirement: &70238356312480 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70238356312480
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: shoulda
|
38
|
-
requirement: &
|
38
|
+
requirement: &70238356311720 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70238356311720
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: yard
|
49
|
-
requirement: &
|
49
|
+
requirement: &70238356310980 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 0.6.0
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70238356310980
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: bundler
|
60
|
-
requirement: &
|
60
|
+
requirement: &70238356310160 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 1.0.0
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70238356310160
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: jeweler
|
71
|
-
requirement: &
|
71
|
+
requirement: &70238356309340 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ~>
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: 1.6.4
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70238356309340
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: rcov
|
82
|
-
requirement: &
|
82
|
+
requirement: &70238356308780 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70238356308780
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: rails
|
93
|
-
requirement: &
|
93
|
+
requirement: &70238356307740 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ! '>='
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: 3.0.0
|
99
99
|
type: :runtime
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *70238356307740
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: redis
|
104
|
-
requirement: &
|
104
|
+
requirement: &70238356307200 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - ~>
|
@@ -109,10 +109,10 @@ dependencies:
|
|
109
109
|
version: 2.2.0
|
110
110
|
type: :runtime
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *70238356307200
|
113
113
|
- !ruby/object:Gem::Dependency
|
114
114
|
name: resque
|
115
|
-
requirement: &
|
115
|
+
requirement: &70238356306660 !ruby/object:Gem::Requirement
|
116
116
|
none: false
|
117
117
|
requirements:
|
118
118
|
- - ~>
|
@@ -120,10 +120,10 @@ dependencies:
|
|
120
120
|
version: 1.19.0
|
121
121
|
type: :runtime
|
122
122
|
prerelease: false
|
123
|
-
version_requirements: *
|
123
|
+
version_requirements: *70238356306660
|
124
124
|
- !ruby/object:Gem::Dependency
|
125
125
|
name: resque-loner
|
126
|
-
requirement: &
|
126
|
+
requirement: &70238356305800 !ruby/object:Gem::Requirement
|
127
127
|
none: false
|
128
128
|
requirements:
|
129
129
|
- - ~>
|
@@ -131,7 +131,7 @@ dependencies:
|
|
131
131
|
version: 1.2.0
|
132
132
|
type: :runtime
|
133
133
|
prerelease: false
|
134
|
-
version_requirements: *
|
134
|
+
version_requirements: *70238356305800
|
135
135
|
description: Allow a model (typically User) to Like and/or Dislike models in your
|
136
136
|
app. Generate recommendations quickly using redis.
|
137
137
|
email: david@davidcelis.com
|