recommendable 1.0.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.markdown +8 -2
- data/Gemfile +1 -15
- data/Gemfile.lock +59 -66
- data/README.markdown +35 -16
- data/app/workers/recommendable/delayed_job_worker.rb +17 -0
- data/app/workers/recommendable/rails_worker.rb +17 -0
- data/app/workers/recommendable/resque_worker.rb +14 -0
- data/app/workers/recommendable/sidekiq_worker.rb +14 -0
- data/lib/generators/recommendable/templates/initializer.rb +0 -8
- data/lib/recommendable/acts_as_recommended_to.rb +79 -25
- data/lib/recommendable/exceptions.rb +1 -1
- data/lib/recommendable/version.rb +1 -1
- data/lib/recommendable.rb +13 -1
- data/recommendable.gemspec +15 -17
- data/spec/dummy/config/initializers/recommendable.rb +0 -2
- data/spec/models/dislike_spec.rb +2 -2
- data/spec/models/ignore_spec.rb +2 -2
- data/spec/models/like_spec.rb +2 -2
- data/spec/models/stash_spec.rb +1 -1
- data/spec/models/user_spec.rb +4 -4
- metadata +19 -48
- data/app/workers/recommendable/recommendation_refresher.rb +0 -12
data/CHANGELOG.markdown
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
Changelog
|
2
2
|
=========
|
3
3
|
|
4
|
-
1.
|
5
|
-
|
4
|
+
1.1.0 (current version)
|
5
|
+
-----------------------
|
6
|
+
* Support for Sidekiq, Resque, DelayedJob and Rails::Queueing (issue #28)
|
7
|
+
* You must manually bundle Sidekiq, Resque, or DelayedJob. Rails::Queueing is available as a fallback for Rails 4.x
|
8
|
+
* Use [apotonick/hooks](https://github.com/apotonick/hooks) to implement callbacks (issue #25). See the [detailed README](http://davidcelis.com/recommendable) for more info on usage.
|
9
|
+
|
10
|
+
1.0.0
|
11
|
+
-----
|
6
12
|
* Dynamic finders now return ActiveRecord::Relations! This means you can chain other ActiveRecord query methods like so:
|
7
13
|
|
8
14
|
```ruby
|
data/Gemfile
CHANGED
@@ -1,17 +1,3 @@
|
|
1
1
|
source 'http://rubygems.org'
|
2
2
|
# Add dependencies required to use your gem here.
|
3
|
-
|
4
|
-
gem 'redis', '>= 2.2.0'
|
5
|
-
gem 'resque', '~> 1.19.0'
|
6
|
-
gem 'resque-loner', '~> 1.2.0'
|
7
|
-
|
8
|
-
# Add dependencies to develop your gem here.
|
9
|
-
# Include everything needed to run rake, tests, features, etc.
|
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'
|
17
|
-
end
|
3
|
+
gemspec
|
data/Gemfile.lock
CHANGED
@@ -1,118 +1,111 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
recommendable (1.1.1)
|
5
|
+
hooks
|
6
|
+
rails (>= 3.0.0)
|
7
|
+
redis (~> 2.2.0)
|
8
|
+
|
1
9
|
GEM
|
2
10
|
remote: http://rubygems.org/
|
3
11
|
specs:
|
4
|
-
actionmailer (3.2.
|
5
|
-
actionpack (= 3.2.
|
6
|
-
mail (~> 2.4.
|
7
|
-
actionpack (3.2.
|
8
|
-
activemodel (= 3.2.
|
9
|
-
activesupport (= 3.2.
|
12
|
+
actionmailer (3.2.6)
|
13
|
+
actionpack (= 3.2.6)
|
14
|
+
mail (~> 2.4.4)
|
15
|
+
actionpack (3.2.6)
|
16
|
+
activemodel (= 3.2.6)
|
17
|
+
activesupport (= 3.2.6)
|
10
18
|
builder (~> 3.0.0)
|
11
19
|
erubis (~> 2.7.0)
|
12
|
-
journey (~> 1.0.
|
20
|
+
journey (~> 1.0.1)
|
13
21
|
rack (~> 1.4.0)
|
14
|
-
rack-cache (~> 1.
|
22
|
+
rack-cache (~> 1.2)
|
15
23
|
rack-test (~> 0.6.1)
|
16
|
-
sprockets (~> 2.1.
|
17
|
-
activemodel (3.2.
|
18
|
-
activesupport (= 3.2.
|
24
|
+
sprockets (~> 2.1.3)
|
25
|
+
activemodel (3.2.6)
|
26
|
+
activesupport (= 3.2.6)
|
19
27
|
builder (~> 3.0.0)
|
20
|
-
activerecord (3.2.
|
21
|
-
activemodel (= 3.2.
|
22
|
-
activesupport (= 3.2.
|
23
|
-
arel (~> 3.0.
|
28
|
+
activerecord (3.2.6)
|
29
|
+
activemodel (= 3.2.6)
|
30
|
+
activesupport (= 3.2.6)
|
31
|
+
arel (~> 3.0.2)
|
24
32
|
tzinfo (~> 0.3.29)
|
25
|
-
activeresource (3.2.
|
26
|
-
activemodel (= 3.2.
|
27
|
-
activesupport (= 3.2.
|
28
|
-
activesupport (3.2.
|
33
|
+
activeresource (3.2.6)
|
34
|
+
activemodel (= 3.2.6)
|
35
|
+
activesupport (= 3.2.6)
|
36
|
+
activesupport (3.2.6)
|
29
37
|
i18n (~> 0.6)
|
30
38
|
multi_json (~> 1.0)
|
31
|
-
arel (3.0.
|
39
|
+
arel (3.0.2)
|
32
40
|
builder (3.0.0)
|
33
41
|
erubis (2.7.0)
|
34
42
|
hike (1.2.1)
|
43
|
+
hooks (0.2.0)
|
35
44
|
i18n (0.6.0)
|
36
|
-
journey (1.0.
|
37
|
-
json (1.
|
38
|
-
mail (2.4.
|
45
|
+
journey (1.0.4)
|
46
|
+
json (1.7.3)
|
47
|
+
mail (2.4.4)
|
39
48
|
i18n (>= 0.4.0)
|
40
49
|
mime-types (~> 1.16)
|
41
50
|
treetop (~> 1.4.8)
|
42
|
-
mime-types (1.
|
51
|
+
mime-types (1.19)
|
43
52
|
miniskirt (1.1.1)
|
44
53
|
activesupport (>= 2.2)
|
45
|
-
minitest (2.
|
46
|
-
multi_json (1.
|
54
|
+
minitest (3.2.0)
|
55
|
+
multi_json (1.3.6)
|
47
56
|
polyglot (0.3.3)
|
48
|
-
rack (1.4.
|
49
|
-
rack-cache (1.
|
57
|
+
rack (1.4.1)
|
58
|
+
rack-cache (1.2)
|
50
59
|
rack (>= 0.4)
|
51
|
-
rack-protection (1.2.0)
|
52
|
-
rack
|
53
60
|
rack-ssl (1.3.2)
|
54
61
|
rack
|
55
62
|
rack-test (0.6.1)
|
56
63
|
rack (>= 1.0)
|
57
|
-
rails (3.2.
|
58
|
-
actionmailer (= 3.2.
|
59
|
-
actionpack (= 3.2.
|
60
|
-
activerecord (= 3.2.
|
61
|
-
activeresource (= 3.2.
|
62
|
-
activesupport (= 3.2.
|
64
|
+
rails (3.2.6)
|
65
|
+
actionmailer (= 3.2.6)
|
66
|
+
actionpack (= 3.2.6)
|
67
|
+
activerecord (= 3.2.6)
|
68
|
+
activeresource (= 3.2.6)
|
69
|
+
activesupport (= 3.2.6)
|
63
70
|
bundler (~> 1.0)
|
64
|
-
railties (= 3.2.
|
65
|
-
railties (3.2.
|
66
|
-
actionpack (= 3.2.
|
67
|
-
activesupport (= 3.2.
|
71
|
+
railties (= 3.2.6)
|
72
|
+
railties (3.2.6)
|
73
|
+
actionpack (= 3.2.6)
|
74
|
+
activesupport (= 3.2.6)
|
68
75
|
rack-ssl (~> 1.3.2)
|
69
76
|
rake (>= 0.8.7)
|
70
77
|
rdoc (~> 3.4)
|
71
|
-
thor (
|
78
|
+
thor (>= 0.14.6, < 2.0)
|
72
79
|
rake (0.9.2.2)
|
73
80
|
rdoc (3.12)
|
74
81
|
json (~> 1.4)
|
75
82
|
redis (2.2.2)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
vegas (~> 0.1.2)
|
83
|
-
resque-loner (1.2.0)
|
84
|
-
resque (~> 1.0)
|
85
|
-
shoulda (2.11.3)
|
86
|
-
sinatra (1.3.2)
|
87
|
-
rack (~> 1.3, >= 1.3.6)
|
88
|
-
rack-protection (~> 1.2)
|
89
|
-
tilt (~> 1.3, >= 1.3.3)
|
90
|
-
sprockets (2.1.2)
|
83
|
+
shoulda (3.0.1)
|
84
|
+
shoulda-context (~> 1.0.0)
|
85
|
+
shoulda-matchers (~> 1.0.0)
|
86
|
+
shoulda-context (1.0.0)
|
87
|
+
shoulda-matchers (1.0.0)
|
88
|
+
sprockets (2.1.3)
|
91
89
|
hike (~> 1.2)
|
92
90
|
rack (~> 1.0)
|
93
91
|
tilt (~> 1.1, != 1.3.0)
|
94
|
-
sqlite3 (1.3.
|
95
|
-
thor (0.
|
92
|
+
sqlite3 (1.3.6)
|
93
|
+
thor (0.15.4)
|
96
94
|
tilt (1.3.3)
|
97
95
|
treetop (1.4.10)
|
98
96
|
polyglot
|
99
97
|
polyglot (>= 0.3.1)
|
100
|
-
tzinfo (0.3.
|
101
|
-
vegas (0.1.11)
|
102
|
-
rack (>= 1.0.0)
|
98
|
+
tzinfo (0.3.33)
|
103
99
|
yard (0.6.8)
|
104
100
|
|
105
101
|
PLATFORMS
|
106
102
|
ruby
|
107
103
|
|
108
104
|
DEPENDENCIES
|
109
|
-
bundler
|
105
|
+
bundler
|
110
106
|
miniskirt
|
111
107
|
minitest
|
112
|
-
|
113
|
-
redis (>= 2.2.0)
|
114
|
-
resque (~> 1.19.0)
|
115
|
-
resque-loner (~> 1.2.0)
|
108
|
+
recommendable!
|
116
109
|
shoulda
|
117
110
|
sqlite3
|
118
111
|
yard (~> 0.6.0)
|
data/README.markdown
CHANGED
@@ -4,8 +4,11 @@ Recommendable is an engine for Rails 3 applications to quickly add the ability f
|
|
4
4
|
|
5
5
|
Requirements
|
6
6
|
------------
|
7
|
-
* Ruby 1.9.
|
8
|
-
* Rails 3.x
|
7
|
+
* Ruby 1.9.x
|
8
|
+
* Rails 3.x or 4.x
|
9
|
+
* Sidekiq or Resque (or DelayedJob)
|
10
|
+
|
11
|
+
Bundling one of the queueing systems above is highly recommended to avoid having to manually refresh users' recommendations. If running on Rails 4, the built-in queueing system is supported. If you bundle [Sidekiq][sidekiq], [Resque][resque], or [DelayedJob][delayed_job], Recommendable will use your bundled queueing system instead. If bundling Resque, you should also include ['resque-loner'][resque-loner] in your Gemfile to ensure your users only get queued once (Sidekiq does this by default, and there is no current way to avoid duplicate jobs in DelayedJob).
|
9
12
|
|
10
13
|
Installation
|
11
14
|
------------
|
@@ -13,7 +16,7 @@ Installation
|
|
13
16
|
Add the following to your Rails application's `Gemfile`:
|
14
17
|
|
15
18
|
``` ruby
|
16
|
-
gem
|
19
|
+
gem 'recommendable'
|
17
20
|
```
|
18
21
|
|
19
22
|
After bundling, run the installation generator:
|
@@ -22,15 +25,17 @@ After bundling, run the installation generator:
|
|
22
25
|
$ rails g recommendable:install
|
23
26
|
```
|
24
27
|
|
25
|
-
Double check `config/initializers/recommendable.rb` for options on configuring your Redis connection. After a user likes or dislikes something new, they are placed in a
|
28
|
+
Double check `config/initializers/recommendable.rb` for options on configuring your Redis connection. After a user likes or dislikes something new, they are placed in a queue to have their recommendations updated. If you're using the basic Rails 4.0 queue, you don't need to do anything explicit. If using Sidekiq, Resque, or DelayedJob, start your workers from the command line:
|
26
29
|
|
27
30
|
``` bash
|
31
|
+
# sidekiq
|
32
|
+
$ bundle exec sidekiq -q recommendable
|
33
|
+
# resque
|
28
34
|
$ QUEUE=recommendable rake environment resque:work
|
35
|
+
# delayed_job
|
36
|
+
$ rake jobs:work
|
29
37
|
```
|
30
38
|
|
31
|
-
You can run this command multiple times if you wish to start more than one
|
32
|
-
worker. For more options on this task, head over to [defunkt/resque][resque].
|
33
|
-
|
34
39
|
Usage
|
35
40
|
-----
|
36
41
|
|
@@ -53,28 +58,39 @@ Installing Redis
|
|
53
58
|
|
54
59
|
Recommendable requires Redis to deliver recommendations. The collaborative filtering logic is based almost entirely on set math, and Redis is blazing fast for this. _NOTE: Your redis database MUST be persistent._
|
55
60
|
|
56
|
-
###
|
61
|
+
### Mac OS X
|
57
62
|
|
58
|
-
For Mac OS X users, homebrew is by far the easiest way to install Redis.
|
63
|
+
For Mac OS X users, homebrew is by far the easiest way to install Redis. Make sure to read the caveats after installation!
|
59
64
|
|
60
65
|
``` bash
|
61
66
|
$ brew install redis
|
62
67
|
```
|
63
68
|
|
64
|
-
###
|
69
|
+
### Linux
|
65
70
|
|
66
|
-
|
67
|
-
will install and run Redis for you:
|
71
|
+
For Linux users, there is a package on apt-get.
|
68
72
|
|
69
73
|
``` bash
|
70
|
-
$
|
71
|
-
$
|
72
|
-
$ rake redis:install dtach:install
|
73
|
-
$ rake redis:start
|
74
|
+
$ sudo apt-get install redis-server
|
75
|
+
$ redis-server
|
74
76
|
```
|
75
77
|
|
76
78
|
Redis will now be running on localhost:6379. After a second, you can hit `ctrl-\` to detach and keep Redis running in the background.
|
77
79
|
|
80
|
+
### Redis problems?
|
81
|
+
|
82
|
+
Oops, did you kill your Redis database? Not to worry. Likes, Dislikes, Ignores,
|
83
|
+
and StashedItems are stored as models in your regular database. As long as these
|
84
|
+
still exist, you can regenerate the similarity values and recommendations on the
|
85
|
+
fly. But try not to have to do it!
|
86
|
+
|
87
|
+
``` ruby
|
88
|
+
Users.all.each do |user|
|
89
|
+
user.send :update_similarities
|
90
|
+
user.send :update_recommendations
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
78
94
|
Contributing to recommendable
|
79
95
|
-----------------------------
|
80
96
|
|
@@ -102,7 +118,10 @@ Copyright © 2012 David Celis. See LICENSE.txt for
|
|
102
118
|
further details.
|
103
119
|
|
104
120
|
[stars]: http://davidcelis.com/blog/2012/02/01/why-i-hate-five-star-ratings/
|
121
|
+
[sidekiq]: https://github.com/mperham/sidekiq
|
122
|
+
[delayed_job]: https://github.com/tobi/delayed_job
|
105
123
|
[resque]: https://github.com/defunkt/resque
|
124
|
+
[resque-loner]: https://github.com/jayniz/resque-loner
|
106
125
|
[forking]: http://help.github.com/forking/
|
107
126
|
[pull requests]: http://help.github.com/pull-requests/
|
108
127
|
[collaborative filtering]: http://davidcelis.com/blog/2012/02/07/collaborative-filtering-with-likes-and-dislikes/
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Recommendable
|
2
|
+
if defined?(Delayed::Job)
|
3
|
+
class DelayedJobWorker
|
4
|
+
attr_accessor :user_id
|
5
|
+
|
6
|
+
def initialize(user_id)
|
7
|
+
@user_id = user_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
user = Recommendable.user_class.find(self.user_id)
|
12
|
+
user.send :update_similarities
|
13
|
+
user.send :update_recommendations
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Recommendable
|
2
|
+
if defined?(Rails::Queueing)
|
3
|
+
class RailsWorker
|
4
|
+
attr_accessor :user_id
|
5
|
+
|
6
|
+
def initialize(user_id)
|
7
|
+
@user_id = user_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
user = Recommendable.user_class.find(self.user_id)
|
12
|
+
user.send :update_similarities
|
13
|
+
user.send :update_recommendations
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Recommendable
|
2
|
+
if defined?(Resque)
|
3
|
+
class ResqueWorker
|
4
|
+
include Resque::Plugins::UniqueJob if defined?(Resque::Plugins::UniqueJob)
|
5
|
+
@queue = :recommendable
|
6
|
+
|
7
|
+
def self.perform(user_id)
|
8
|
+
user = Recommendable.user_class.find(user_id)
|
9
|
+
user.send :update_similarities
|
10
|
+
user.send :update_recommendations
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Recommendable
|
2
|
+
if defined?(Sidekiq)
|
3
|
+
class SidekiqWorker
|
4
|
+
include ::Sidekiq::Worker
|
5
|
+
sidekiq_options :queue => :recommendable
|
6
|
+
|
7
|
+
def perform(user_id)
|
8
|
+
user = Recommendable.user_class.find(user_id)
|
9
|
+
user.send :update_similarities
|
10
|
+
user.send :update_recommendations
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,6 +1,4 @@
|
|
1
1
|
require "redis"
|
2
|
-
require "resque"
|
3
|
-
require "resque-loner"
|
4
2
|
|
5
3
|
# Recommendable requires a connection to a running redis-server. Either create
|
6
4
|
# a new instance based on a host/port or UNIX socket, or pass in an existing
|
@@ -10,12 +8,6 @@ require "resque-loner"
|
|
10
8
|
# Connect to Redis via a UNIX socket instead
|
11
9
|
<% unless options.redis_socket %># <% end %>Recommendable.redis = Redis.new(:sock => "<%= options.redis_socket %>")
|
12
10
|
|
13
|
-
# Resque also needs a connection to Redis. If you are currently initializing
|
14
|
-
# Resque somewhere else, leave this commented out. Otherwise, let it use the
|
15
|
-
# same Redis connection as Recommendable. If redis is running on localhost:6379,
|
16
|
-
# you can leave this commented out.
|
17
|
-
# Resque.redis = Recommendable.redis
|
18
|
-
|
19
11
|
# Tell Redis which database to use (usually between 0 and 15). The default of 0
|
20
12
|
# is most likely okay unless you have another application using that database.
|
21
13
|
# Recommendable.redis.select "0"
|
@@ -25,9 +25,38 @@ module Recommendable
|
|
25
25
|
include StashMethods
|
26
26
|
include IgnoreMethods
|
27
27
|
include RecommendationMethods
|
28
|
+
include Hooks
|
28
29
|
|
29
30
|
before_destroy :remove_from_similarities, :remove_recommendations
|
30
31
|
|
32
|
+
# This is just until apotonick merges my change into his published gem. I promise.
|
33
|
+
define_hook :before_like
|
34
|
+
define_hook :after_like
|
35
|
+
define_hook :before_unlike
|
36
|
+
define_hook :after_unlike
|
37
|
+
define_hook :before_dislike
|
38
|
+
define_hook :after_dislike
|
39
|
+
define_hook :before_undislike
|
40
|
+
define_hook :after_undislike
|
41
|
+
define_hook :before_stash
|
42
|
+
define_hook :after_stash
|
43
|
+
define_hook :before_unstash
|
44
|
+
define_hook :after_unstash
|
45
|
+
define_hook :before_ignore
|
46
|
+
define_hook :after_ignore
|
47
|
+
define_hook :before_unignore
|
48
|
+
define_hook :after_unignore
|
49
|
+
|
50
|
+
%w(like dislike ignore).each do |action|
|
51
|
+
send "before_#{action}", lambda { |obj| completely_unrecommend obj }
|
52
|
+
end
|
53
|
+
|
54
|
+
%w(like unlike dislike undislike).each do |action|
|
55
|
+
send "after_#{action}", lambda { |obj| obj.send(:update_score) and Recommendable.enqueue(self.id) }
|
56
|
+
end
|
57
|
+
|
58
|
+
before_stash { |obj| unignore(obj) and unpredict(obj) }
|
59
|
+
|
31
60
|
def method_missing method, *args, &block
|
32
61
|
if method.to_s =~ /^(liked|disliked)_(.+)_in_common_with$/
|
33
62
|
begin
|
@@ -75,14 +104,15 @@ module Recommendable
|
|
75
104
|
#
|
76
105
|
# @param [Object] object the object you want self to like.
|
77
106
|
# @return true if object has been liked
|
78
|
-
# @raise [
|
107
|
+
# @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
79
108
|
def like object
|
80
|
-
raise
|
109
|
+
raise UnrecommendableError unless object.recommendable?
|
81
110
|
return if likes? object
|
82
|
-
|
111
|
+
|
112
|
+
run_hook :before_like, object
|
83
113
|
likes.create! :likeable_id => object.id, :likeable_type => object.class
|
84
|
-
|
85
|
-
|
114
|
+
run_hook :after_like, object
|
115
|
+
|
86
116
|
true
|
87
117
|
end
|
88
118
|
|
@@ -99,9 +129,11 @@ module Recommendable
|
|
99
129
|
# @param [Object] object the object you want to remove from self's likes
|
100
130
|
# @return true if object is unliked, nil if nothing happened
|
101
131
|
def unlike object
|
102
|
-
|
103
|
-
|
104
|
-
|
132
|
+
like = likes.where(:likeable_id => object.id, :likeable_type => object.class.base_class.to_s)
|
133
|
+
if like.exists?
|
134
|
+
run_hook :before_unlike, object
|
135
|
+
like.first.destroy
|
136
|
+
run_hook :after_unlike, object
|
105
137
|
true
|
106
138
|
end
|
107
139
|
end
|
@@ -153,14 +185,15 @@ module Recommendable
|
|
153
185
|
#
|
154
186
|
# @param [Object] object the object you want self to dislike.
|
155
187
|
# @return true if object has been disliked
|
156
|
-
# @raise [
|
188
|
+
# @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
157
189
|
def dislike object
|
158
|
-
raise
|
190
|
+
raise UnrecommendableError unless object.recommendable?
|
159
191
|
return if dislikes? object
|
160
|
-
|
192
|
+
|
193
|
+
run_hook :before_dislike, object
|
161
194
|
dislikes.create! :dislikeable_id => object.id, :dislikeable_type => object.class
|
162
|
-
|
163
|
-
|
195
|
+
run_hook :after_dislike, object
|
196
|
+
|
164
197
|
true
|
165
198
|
end
|
166
199
|
|
@@ -177,9 +210,11 @@ module Recommendable
|
|
177
210
|
# @param [Object] object the object you want to remove from self's dislikes
|
178
211
|
# @return true if object is removed from self's dislikes, nil if nothing happened
|
179
212
|
def undislike object
|
180
|
-
|
181
|
-
|
182
|
-
|
213
|
+
dislike = dislikes.where(:dislikeable_id => object.id, :dislikeable_type => object.class.base_class.to_s)
|
214
|
+
if dislike.exists?
|
215
|
+
run_hook :before_undislike, object
|
216
|
+
dislike.first.destroy
|
217
|
+
run_hook :after_undislike, object
|
183
218
|
true
|
184
219
|
end
|
185
220
|
end
|
@@ -231,13 +266,15 @@ module Recommendable
|
|
231
266
|
#
|
232
267
|
# @param [Object] object the object you want self to stash.
|
233
268
|
# @return true if object has been stashed
|
234
|
-
# @raise [
|
269
|
+
# @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
235
270
|
def stash object
|
236
|
-
raise
|
271
|
+
raise UnrecommendableError unless object.recommendable?
|
237
272
|
return if rated?(object) || stashed?(object)
|
238
|
-
|
239
|
-
|
273
|
+
|
274
|
+
run_hook :before_stash, object
|
240
275
|
stashed_items.create! :stashable_id => object.id, :stashable_type => object.class
|
276
|
+
run_hook :after_stash, object
|
277
|
+
|
241
278
|
true
|
242
279
|
end
|
243
280
|
|
@@ -254,7 +291,13 @@ module Recommendable
|
|
254
291
|
# @param [Object] object the object you want to remove from self's stash
|
255
292
|
# @return true if object is stashed, nil if nothing happened
|
256
293
|
def unstash object
|
257
|
-
|
294
|
+
stash = stashed_items.where(:stashable_id => object.id, :stashable_type => object.class.base_class.to_s)
|
295
|
+
if stash.exists?
|
296
|
+
run_hook :before_unstash, object
|
297
|
+
stash.first.destroy
|
298
|
+
run_hook :after_unstash, object
|
299
|
+
true
|
300
|
+
end
|
258
301
|
end
|
259
302
|
|
260
303
|
# Get a list of records that self has currently stashed for later
|
@@ -290,12 +333,15 @@ module Recommendable
|
|
290
333
|
#
|
291
334
|
# @param [Object] object the object you want self to ignore.
|
292
335
|
# @return true if object has been ignored
|
293
|
-
# @raise [
|
336
|
+
# @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
294
337
|
def ignore object
|
295
|
-
raise
|
338
|
+
raise UnrecommendableError unless object.recommendable?
|
296
339
|
return if ignored? object
|
297
|
-
|
340
|
+
|
341
|
+
run_hook :before_ignore, object
|
298
342
|
ignores.create! :ignorable_id => object.id, :ignorable_type => object.class
|
343
|
+
run_hook :after_ignore, object
|
344
|
+
|
299
345
|
true
|
300
346
|
end
|
301
347
|
|
@@ -312,7 +358,13 @@ module Recommendable
|
|
312
358
|
# @param [Object] object the object you want to remove from self's ignores
|
313
359
|
# @return true if object is removed from self's ignores, nil if nothing happened
|
314
360
|
def unignore object
|
315
|
-
|
361
|
+
ignore = ignores.where(:ignorable_id => object.id, :ignorable_type => object.class.base_class.to_s)
|
362
|
+
if ignore.exists?
|
363
|
+
run_hook :before_unignore, object
|
364
|
+
ignore.first.destroy
|
365
|
+
run_hook :after_unignore, object
|
366
|
+
true
|
367
|
+
end
|
316
368
|
end
|
317
369
|
|
318
370
|
# Get a list of records that self is currently ignoring
|
@@ -694,6 +746,7 @@ module Recommendable
|
|
694
746
|
destroy_recommended_to_sets
|
695
747
|
end
|
696
748
|
|
749
|
+
# @private
|
697
750
|
def remove_from_similarities
|
698
751
|
Recommendable.redis.del similarity_set
|
699
752
|
|
@@ -704,6 +757,7 @@ module Recommendable
|
|
704
757
|
true
|
705
758
|
end
|
706
759
|
|
760
|
+
# @private
|
707
761
|
def remove_recommendations
|
708
762
|
Recommendable.recommendable_classes.each do |klass|
|
709
763
|
Recommendable.redis.del predictions_set_for(klass)
|
data/lib/recommendable.rb
CHANGED
@@ -5,6 +5,7 @@ require 'recommendable/acts_as_recommendable'
|
|
5
5
|
require 'recommendable/exceptions'
|
6
6
|
require 'recommendable/railtie' if defined?(Rails)
|
7
7
|
require 'recommendable/version'
|
8
|
+
require 'hooks'
|
8
9
|
|
9
10
|
module Recommendable
|
10
11
|
mattr_accessor :redis, :user_class
|
@@ -15,6 +16,17 @@ module Recommendable
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def self.enqueue(user_id)
|
18
|
-
|
19
|
+
if defined? Sidekiq
|
20
|
+
SidekiqWorker.perform_async user_id
|
21
|
+
elsif defined? Resque
|
22
|
+
Resque.enqueue ResqueWorker, user_id
|
23
|
+
elsif defined? Delayed::Job
|
24
|
+
Delayed::Job.enqueue DelayedJobWorker.new(user_id)
|
25
|
+
elsif defined? Rails::Queueing
|
26
|
+
unless Rails.queue.any? { |w| w.user_id == user_id }
|
27
|
+
Rails.queue.push RailsWorker.new(user_id)
|
28
|
+
Rails.application.queue_consumer.start
|
29
|
+
end
|
30
|
+
end
|
19
31
|
end
|
20
32
|
end
|
data/recommendable.gemspec
CHANGED
@@ -2,31 +2,29 @@
|
|
2
2
|
require File.expand_path('lib/recommendable/version')
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
|
-
s.name =
|
5
|
+
s.name = 'recommendable'
|
6
6
|
s.version = Recommendable::VERSION
|
7
7
|
s.date = Time.now.strftime('%Y-%m-%d')
|
8
8
|
|
9
|
-
s.authors = [
|
10
|
-
s.email =
|
11
|
-
s.homepage =
|
9
|
+
s.authors = ['David Celis']
|
10
|
+
s.email = 'david@davidcelis.com'
|
11
|
+
s.homepage = 'http://github.com/davidcelis/recommendable'
|
12
12
|
|
13
|
-
s.summary =
|
14
|
-
s.description =
|
13
|
+
s.summary = 'Add like-based and/or dislike-based recommendations to your app.'
|
14
|
+
s.description = 'Allow a model (typically User) to Like and/or Dislike models in your app. Generate recommendations quickly using redis.'
|
15
15
|
|
16
16
|
s.files = `git ls-files`.split("\n")
|
17
17
|
s.has_rdoc = 'yard'
|
18
18
|
|
19
|
-
s.add_development_dependency
|
20
|
-
s.add_development_dependency
|
21
|
-
s.add_development_dependency
|
22
|
-
s.add_development_dependency
|
23
|
-
s.add_development_dependency
|
24
|
-
s.add_development_dependency
|
25
|
-
s.add_development_dependency "rcov"
|
19
|
+
s.add_development_dependency 'sqlite3'
|
20
|
+
s.add_development_dependency 'minitest'
|
21
|
+
s.add_development_dependency 'miniskirt'
|
22
|
+
s.add_development_dependency 'shoulda'
|
23
|
+
s.add_development_dependency 'yard', '~> 0.6.0'
|
24
|
+
s.add_development_dependency 'bundler'
|
26
25
|
|
27
|
-
s.add_dependency
|
28
|
-
s.add_dependency
|
29
|
-
s.add_dependency
|
30
|
-
s.add_dependency "resque-loner", "~> 1.2.0"
|
26
|
+
s.add_dependency 'rails', '>= 3.0.0'
|
27
|
+
s.add_dependency 'redis', '~> 2.2.0'
|
28
|
+
s.add_dependency 'hooks'
|
31
29
|
end
|
32
30
|
|
data/spec/models/dislike_spec.rb
CHANGED
@@ -8,7 +8,7 @@ class DislikeSpec < MiniTest::Spec
|
|
8
8
|
|
9
9
|
it "should not be created for an object that does not act_as_recommendedable" do
|
10
10
|
django = PhpFramework.create(:name => "django")
|
11
|
-
proc { @user.dislike(django) }.must_raise Recommendable::
|
11
|
+
proc { @user.dislike(django) }.must_raise Recommendable::UnrecommendableError
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should be created for an object that does act_as_recommendable" do
|
@@ -24,4 +24,4 @@ class DislikeSpec < MiniTest::Spec
|
|
24
24
|
Recommendable::Dislike.count.must_equal 1
|
25
25
|
end
|
26
26
|
end
|
27
|
-
end
|
27
|
+
end
|
data/spec/models/ignore_spec.rb
CHANGED
@@ -8,7 +8,7 @@ class IgnoreSpec < MiniTest::Spec
|
|
8
8
|
|
9
9
|
it "should not be created for an object that does not act_as_recommendedable" do
|
10
10
|
web2py = PhpFramework.create(:name => "web2py")
|
11
|
-
proc { @user.ignore(web2py) }.must_raise Recommendable::
|
11
|
+
proc { @user.ignore(web2py) }.must_raise Recommendable::UnrecommendableError
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should be created for an object that does act_as_recommendable" do
|
@@ -24,4 +24,4 @@ class IgnoreSpec < MiniTest::Spec
|
|
24
24
|
Recommendable::Ignore.count.must_equal 1
|
25
25
|
end
|
26
26
|
end
|
27
|
-
end
|
27
|
+
end
|
data/spec/models/like_spec.rb
CHANGED
@@ -8,7 +8,7 @@ class LikeSpec < MiniTest::Spec
|
|
8
8
|
|
9
9
|
it "should not be created for an object that does not act_as_recommendedable" do
|
10
10
|
cake = PhpFramework.create(:name => "CakePHP")
|
11
|
-
proc { @user.like(cake) }.must_raise Recommendable::
|
11
|
+
proc { @user.like(cake) }.must_raise Recommendable::UnrecommendableError
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should be created for an object that does act_as_recommendable" do
|
@@ -25,4 +25,4 @@ class LikeSpec < MiniTest::Spec
|
|
25
25
|
Recommendable::Like.count.must_equal 1
|
26
26
|
end
|
27
27
|
end
|
28
|
-
end
|
28
|
+
end
|
data/spec/models/stash_spec.rb
CHANGED
@@ -8,7 +8,7 @@ class StashSpec < MiniTest::Spec
|
|
8
8
|
|
9
9
|
it "should not be created for an object that does not act_as_recommendedable" do
|
10
10
|
web2py = PhpFramework.create(:name => "web2py")
|
11
|
-
proc { @user.stash(web2py) }.must_raise Recommendable::
|
11
|
+
proc { @user.stash(web2py) }.must_raise Recommendable::UnrecommendableError
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should be created for an object that does act_as_recommendable" do
|
data/spec/models/user_spec.rb
CHANGED
@@ -189,10 +189,10 @@ class UserSpec < MiniTest::Spec
|
|
189
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
|
-
proc { @user.like(@cakephp) }.must_raise Recommendable::
|
193
|
-
proc { @user.dislike(@cakephp) }.must_raise Recommendable::
|
194
|
-
proc { @user.ignore(@cakephp) }.must_raise Recommendable::
|
195
|
-
proc { @user.stash(@cakephp) }.must_raise Recommendable::
|
192
|
+
proc { @user.like(@cakephp) }.must_raise Recommendable::UnrecommendableError
|
193
|
+
proc { @user.dislike(@cakephp) }.must_raise Recommendable::UnrecommendableError
|
194
|
+
proc { @user.ignore(@cakephp) }.must_raise Recommendable::UnrecommendableError
|
195
|
+
proc { @user.stash(@cakephp) }.must_raise Recommendable::UnrecommendableError
|
196
196
|
|
197
197
|
proc { @cakephp.liked_by }.must_raise NoMethodError
|
198
198
|
proc { @cakephp.disliked_by }.must_raise NoMethodError
|
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: 1.
|
4
|
+
version: 1.1.1
|
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-06
|
12
|
+
date: 2012-07-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sqlite3
|
@@ -44,7 +44,7 @@ dependencies:
|
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
46
|
- !ruby/object:Gem::Dependency
|
47
|
-
name:
|
47
|
+
name: miniskirt
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
49
49
|
none: false
|
50
50
|
requirements:
|
@@ -60,45 +60,29 @@ dependencies:
|
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
|
-
name:
|
64
|
-
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
|
-
requirements:
|
67
|
-
- - ~>
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
version: 0.6.0
|
70
|
-
type: :development
|
71
|
-
prerelease: false
|
72
|
-
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
|
-
requirements:
|
75
|
-
- - ~>
|
76
|
-
- !ruby/object:Gem::Version
|
77
|
-
version: 0.6.0
|
78
|
-
- !ruby/object:Gem::Dependency
|
79
|
-
name: bundler
|
63
|
+
name: shoulda
|
80
64
|
requirement: !ruby/object:Gem::Requirement
|
81
65
|
none: false
|
82
66
|
requirements:
|
83
|
-
- -
|
67
|
+
- - ! '>='
|
84
68
|
- !ruby/object:Gem::Version
|
85
|
-
version:
|
69
|
+
version: '0'
|
86
70
|
type: :development
|
87
71
|
prerelease: false
|
88
72
|
version_requirements: !ruby/object:Gem::Requirement
|
89
73
|
none: false
|
90
74
|
requirements:
|
91
|
-
- -
|
75
|
+
- - ! '>='
|
92
76
|
- !ruby/object:Gem::Version
|
93
|
-
version:
|
77
|
+
version: '0'
|
94
78
|
- !ruby/object:Gem::Dependency
|
95
|
-
name:
|
79
|
+
name: yard
|
96
80
|
requirement: !ruby/object:Gem::Requirement
|
97
81
|
none: false
|
98
82
|
requirements:
|
99
83
|
- - ~>
|
100
84
|
- !ruby/object:Gem::Version
|
101
|
-
version:
|
85
|
+
version: 0.6.0
|
102
86
|
type: :development
|
103
87
|
prerelease: false
|
104
88
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -106,9 +90,9 @@ dependencies:
|
|
106
90
|
requirements:
|
107
91
|
- - ~>
|
108
92
|
- !ruby/object:Gem::Version
|
109
|
-
version:
|
93
|
+
version: 0.6.0
|
110
94
|
- !ruby/object:Gem::Dependency
|
111
|
-
name:
|
95
|
+
name: bundler
|
112
96
|
requirement: !ruby/object:Gem::Requirement
|
113
97
|
none: false
|
114
98
|
requirements:
|
@@ -156,13 +140,13 @@ dependencies:
|
|
156
140
|
- !ruby/object:Gem::Version
|
157
141
|
version: 2.2.0
|
158
142
|
- !ruby/object:Gem::Dependency
|
159
|
-
name:
|
143
|
+
name: hooks
|
160
144
|
requirement: !ruby/object:Gem::Requirement
|
161
145
|
none: false
|
162
146
|
requirements:
|
163
147
|
- - ! '>='
|
164
148
|
- !ruby/object:Gem::Version
|
165
|
-
version:
|
149
|
+
version: '0'
|
166
150
|
type: :runtime
|
167
151
|
prerelease: false
|
168
152
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -170,23 +154,7 @@ dependencies:
|
|
170
154
|
requirements:
|
171
155
|
- - ! '>='
|
172
156
|
- !ruby/object:Gem::Version
|
173
|
-
version:
|
174
|
-
- !ruby/object:Gem::Dependency
|
175
|
-
name: resque-loner
|
176
|
-
requirement: !ruby/object:Gem::Requirement
|
177
|
-
none: false
|
178
|
-
requirements:
|
179
|
-
- - ~>
|
180
|
-
- !ruby/object:Gem::Version
|
181
|
-
version: 1.2.0
|
182
|
-
type: :runtime
|
183
|
-
prerelease: false
|
184
|
-
version_requirements: !ruby/object:Gem::Requirement
|
185
|
-
none: false
|
186
|
-
requirements:
|
187
|
-
- - ~>
|
188
|
-
- !ruby/object:Gem::Version
|
189
|
-
version: 1.2.0
|
157
|
+
version: '0'
|
190
158
|
description: Allow a model (typically User) to Like and/or Dislike models in your
|
191
159
|
app. Generate recommendations quickly using redis.
|
192
160
|
email: david@davidcelis.com
|
@@ -207,7 +175,10 @@ files:
|
|
207
175
|
- app/models/recommendable/ignore.rb
|
208
176
|
- app/models/recommendable/like.rb
|
209
177
|
- app/models/recommendable/stash.rb
|
210
|
-
- app/workers/recommendable/
|
178
|
+
- app/workers/recommendable/delayed_job_worker.rb
|
179
|
+
- app/workers/recommendable/rails_worker.rb
|
180
|
+
- app/workers/recommendable/resque_worker.rb
|
181
|
+
- app/workers/recommendable/sidekiq_worker.rb
|
211
182
|
- config/routes.rb
|
212
183
|
- db/migrate/20120124193723_create_likes.rb
|
213
184
|
- db/migrate/20120124193728_create_dislikes.rb
|
@@ -1,12 +0,0 @@
|
|
1
|
-
module Recommendable
|
2
|
-
class RecommendationRefresher
|
3
|
-
include Resque::Plugins::UniqueJob
|
4
|
-
@queue = :recommendable
|
5
|
-
|
6
|
-
def self.perform(user_id)
|
7
|
-
user = Recommendable.user_class.find(user_id)
|
8
|
-
user.send :update_similarities
|
9
|
-
user.send :update_recommendations
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|