recommendable 0.1.3 → 0.1.4
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 +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 [](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
|