acts_as_follower 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +44 -0
- data/README.rdoc +208 -0
- data/Rakefile +39 -0
- data/acts_as_follower.gemspec +25 -0
- data/init.rb +1 -0
- data/lib/acts_as_follower.rb +11 -0
- data/lib/acts_as_follower/followable.rb +102 -0
- data/lib/acts_as_follower/follower.rb +100 -0
- data/lib/acts_as_follower/follower_lib.rb +15 -0
- data/lib/acts_as_follower/railtie.rb +15 -0
- data/lib/acts_as_follower/version.rb +3 -0
- data/lib/generators/USAGE +5 -0
- data/lib/generators/acts_as_follower_generator.rb +30 -0
- data/lib/generators/templates/migration.rb +17 -0
- data/lib/generators/templates/model.rb +21 -0
- data/test/README +24 -0
- data/test/acts_as_followable_test.rb +241 -0
- data/test/acts_as_follower_test.rb +209 -0
- data/test/dummy30/Gemfile +1 -0
- data/test/dummy30/Rakefile +7 -0
- data/test/dummy30/app/models/band.rb +4 -0
- data/test/dummy30/app/models/user.rb +5 -0
- data/test/dummy30/config.ru +4 -0
- data/test/dummy30/config/application.rb +42 -0
- data/test/dummy30/config/boot.rb +10 -0
- data/test/dummy30/config/database.yml +7 -0
- data/test/dummy30/config/environment.rb +5 -0
- data/test/dummy30/config/environments/development.rb +19 -0
- data/test/dummy30/config/environments/test.rb +23 -0
- data/test/dummy30/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy30/config/initializers/inflections.rb +10 -0
- data/test/dummy30/config/initializers/secret_token.rb +7 -0
- data/test/dummy30/config/initializers/session_store.rb +8 -0
- data/test/dummy30/config/locales/en.yml +5 -0
- data/test/dummy30/config/routes.rb +2 -0
- data/test/factories/bands.rb +7 -0
- data/test/factories/users.rb +11 -0
- data/test/follow_test.rb +10 -0
- data/test/schema.rb +21 -0
- data/test/test_helper.rb +17 -0
- metadata +190 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
module ActsAsFollower #:nodoc:
|
2
|
+
module Follower
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def acts_as_follower
|
10
|
+
has_many :follows, :as => :follower, :dependent => :destroy
|
11
|
+
include ActsAsFollower::Follower::InstanceMethods
|
12
|
+
include ActsAsFollower::FollowerLib
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
|
18
|
+
# Returns true if this instance is following the object passed as an argument.
|
19
|
+
def following?(followable)
|
20
|
+
0 < Follow.unblocked.for_follower(self).for_followable(followable).count
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the number of objects this instance is following.
|
24
|
+
def follow_count
|
25
|
+
Follow.unblocked.for_follower(self).count
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a new follow record for this instance to follow the passed object.
|
29
|
+
# Does not allow duplicate records to be created.
|
30
|
+
def follow(followable)
|
31
|
+
follow = get_follow(followable)
|
32
|
+
if follow.blank? && self != followable
|
33
|
+
Follow.create(:followable => followable, :follower => self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Deletes the follow record if it exists.
|
38
|
+
def stop_following(followable)
|
39
|
+
if follow = get_follow(followable)
|
40
|
+
follow.destroy
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the follow records related to this instance by type.
|
45
|
+
def follows_by_type(followable_type, options={})
|
46
|
+
Follow.unblocked.includes(:followable).for_follower(self).for_followable_type(followable_type).find(:all, options)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the follow records related to this instance with the followable included.
|
50
|
+
def all_follows(options={})
|
51
|
+
self.follows.unblocked.includes(:followable).all(options)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the actual records which this instance is following.
|
55
|
+
def all_following(options={})
|
56
|
+
all_follows(options).collect{ |f| f.followable }
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the actual records of a particular type which this record is following.
|
60
|
+
def following_by_type(followable_type, options={})
|
61
|
+
follows = followable_type.constantize.
|
62
|
+
includes(:followings).
|
63
|
+
where('blocked = ?', false).
|
64
|
+
where(
|
65
|
+
"follows.follower_id = ? AND follows.follower_type = ? AND follows.followable_type = ?",
|
66
|
+
self.id, parent_class_name(self), followable_type
|
67
|
+
)
|
68
|
+
if options.has_key?(:limit)
|
69
|
+
follows = follows.limit(options[:limit])
|
70
|
+
end
|
71
|
+
follows
|
72
|
+
end
|
73
|
+
|
74
|
+
def following_by_type_count(followable_type)
|
75
|
+
Follow.unblocked.for_follower(self).for_followable_type(followable_type).count
|
76
|
+
end
|
77
|
+
|
78
|
+
# Allows magic names on following_by_type
|
79
|
+
# e.g. following_users == following_by_type('User')
|
80
|
+
# Allows magic names on following_by_type_count
|
81
|
+
# e.g. following_users_count == following_by_type_count('User')
|
82
|
+
def method_missing(m, *args)
|
83
|
+
if m.to_s[/following_(.+)_count/]
|
84
|
+
following_by_type_count($1.singularize.classify)
|
85
|
+
elsif m.to_s[/following_(.+)/]
|
86
|
+
following_by_type($1.singularize.classify)
|
87
|
+
else
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a follow record for the current instance and followable object.
|
93
|
+
def get_follow(followable)
|
94
|
+
Follow.unblocked.for_follower(self).for_followable(followable).first
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActsAsFollower
|
2
|
+
module FollowerLib
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
# Retrieves the parent class name if using STI.
|
7
|
+
def parent_class_name(obj)
|
8
|
+
if obj.class.superclass != ActiveRecord::Base
|
9
|
+
return obj.class.superclass.name
|
10
|
+
end
|
11
|
+
return obj.class.name
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'acts_as_follower'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module ActsAsFollower
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
|
7
|
+
initializer "acts_as_follower.active_record" do |app|
|
8
|
+
ActiveSupport.on_load :active_record do
|
9
|
+
include ActsAsFollower::Follower
|
10
|
+
include ActsAsFollower::Followable
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
class ActsAsFollowerGenerator < Rails::Generators::Base
|
5
|
+
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
def self.source_root
|
9
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
10
|
+
end
|
11
|
+
|
12
|
+
# Implement the required interface for Rails::Generators::Migration.
|
13
|
+
# taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
|
14
|
+
def self.next_migration_number(dirname)
|
15
|
+
if ActiveRecord::Base.timestamped_migrations
|
16
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
17
|
+
else
|
18
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_migration_file
|
23
|
+
migration_template 'migration.rb', 'db/migrate/acts_as_follower_migration.rb'
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_model
|
27
|
+
template "model.rb", File.join('app/models', "follow.rb")
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class ActsAsFollowerMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :follows, :force => true do |t|
|
4
|
+
t.references :followable, :polymorphic => true, :null => false
|
5
|
+
t.references :follower, :polymorphic => true, :null => false
|
6
|
+
t.boolean :blocked, :default => false, :null => false
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :follows, ["follower_id", "follower_type"], :name => "fk_follows"
|
11
|
+
add_index :follows, ["followable_id", "followable_type"], :name => "fk_followables"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :follows
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Follow < ActiveRecord::Base
|
2
|
+
extend ActsAsFollower::FollowerLib
|
3
|
+
|
4
|
+
scope :for_follower, lambda { |follower| where(["follower_id = ? AND follower_type = ?", follower.id, parent_class_name(follower)]) }
|
5
|
+
scope :for_followable, lambda { |followable| where(["followable_id = ? AND followable_type = ?", followable.id, parent_class_name(followable)]) }
|
6
|
+
scope :for_follower_type, lambda { |follower_type| where("follower_type = ?", follower_type) }
|
7
|
+
scope :for_followable_type, lambda { |followable_type| where("followable_type = ?", followable_type) }
|
8
|
+
scope :recent, lambda { |from| where(["created_at > ?", (from || 2.weeks.ago).to_s(:db)]) }
|
9
|
+
scope :descending, order("follows.created_at DESC")
|
10
|
+
scope :unblocked, where(:blocked => false)
|
11
|
+
scope :blocked, where(:blocked => true)
|
12
|
+
|
13
|
+
# NOTE: Follows belong to the "followable" interface, and also to followers
|
14
|
+
belongs_to :followable, :polymorphic => true
|
15
|
+
belongs_to :follower, :polymorphic => true
|
16
|
+
|
17
|
+
def block!
|
18
|
+
self.update_attribute(:blocked, true)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/test/README
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Testing
|
2
|
+
=======
|
3
|
+
|
4
|
+
Tests are written with Shoulda on top of Test::Unit and Factory Girl is used instead of fixtures. Tests are run using rake.
|
5
|
+
|
6
|
+
1. Clone your fork git locally.
|
7
|
+
2. Install the dependencies
|
8
|
+
$ bundle install
|
9
|
+
3. Run the tests:
|
10
|
+
rake test
|
11
|
+
|
12
|
+
|
13
|
+
Coverage
|
14
|
+
=======
|
15
|
+
|
16
|
+
Test coverage can be calculated using Rcov. Make sure you have the rcov gem installed.
|
17
|
+
|
18
|
+
Again in the acts_as_follower directory:
|
19
|
+
|
20
|
+
rake rcov:gen DB=sqlite3 # For sqlite
|
21
|
+
|
22
|
+
The coverage will now be available in the test/coverage directory.
|
23
|
+
|
24
|
+
rake rcov:clobber will delete the coverage directory.
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class ActsAsFollowableTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
context "instance methods" do
|
6
|
+
setup do
|
7
|
+
@sam = Factory(:sam)
|
8
|
+
end
|
9
|
+
|
10
|
+
should "be defined" do
|
11
|
+
assert @sam.respond_to?(:followers_count)
|
12
|
+
assert @sam.respond_to?(:followers)
|
13
|
+
assert @sam.respond_to?(:followed_by?)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "acts_as_followable" do
|
18
|
+
setup do
|
19
|
+
@sam = Factory(:sam)
|
20
|
+
@jon = Factory(:jon)
|
21
|
+
@oasis = Factory(:oasis)
|
22
|
+
@metallica = Factory(:metallica)
|
23
|
+
@sam.follow(@jon)
|
24
|
+
end
|
25
|
+
|
26
|
+
context "followers_count" do
|
27
|
+
should "return the number of followers" do
|
28
|
+
assert_equal 0, @sam.followers_count
|
29
|
+
assert_equal 1, @jon.followers_count
|
30
|
+
end
|
31
|
+
|
32
|
+
should "return the proper number of multiple followers" do
|
33
|
+
@bob = Factory(:bob)
|
34
|
+
@sam.follow(@bob)
|
35
|
+
assert_equal 0, @sam.followers_count
|
36
|
+
assert_equal 1, @jon.followers_count
|
37
|
+
assert_equal 1, @bob.followers_count
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "followers" do
|
42
|
+
should "return users" do
|
43
|
+
assert_equal [], @sam.followers
|
44
|
+
assert_equal [@sam], @jon.followers
|
45
|
+
end
|
46
|
+
|
47
|
+
should "return users (multiple followers)" do
|
48
|
+
@bob = Factory(:bob)
|
49
|
+
@sam.follow(@bob)
|
50
|
+
assert_equal [], @sam.followers
|
51
|
+
assert_equal [@sam], @jon.followers
|
52
|
+
assert_equal [@sam], @bob.followers
|
53
|
+
end
|
54
|
+
|
55
|
+
should "return users (multiple followers, complex)" do
|
56
|
+
@bob = Factory(:bob)
|
57
|
+
@sam.follow(@bob)
|
58
|
+
@jon.follow(@bob)
|
59
|
+
assert_equal [], @sam.followers
|
60
|
+
assert_equal [@sam], @jon.followers
|
61
|
+
assert_equal [@sam, @jon], @bob.followers
|
62
|
+
end
|
63
|
+
|
64
|
+
should "accept AR options" do
|
65
|
+
@bob = Factory(:bob)
|
66
|
+
@bob.follow(@jon)
|
67
|
+
assert_equal 1, @jon.followers(:limit => 1).count
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "followed_by" do
|
72
|
+
should "return_follower_status" do
|
73
|
+
assert_equal true, @jon.followed_by?(@sam)
|
74
|
+
assert_equal false, @sam.followed_by?(@jon)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "destroying a followable" do
|
79
|
+
setup do
|
80
|
+
@jon.destroy
|
81
|
+
end
|
82
|
+
|
83
|
+
should_change("follow count", :by => -1) { Follow.count }
|
84
|
+
should_change("@sam.all_following.size", :by => -1) { @sam.all_following.size }
|
85
|
+
end
|
86
|
+
|
87
|
+
context "blocks" do
|
88
|
+
setup do
|
89
|
+
@bob = Factory(:bob)
|
90
|
+
@jon.block(@sam)
|
91
|
+
@jon.block(@bob)
|
92
|
+
end
|
93
|
+
|
94
|
+
should "accept AR options" do
|
95
|
+
assert_equal 1, @jon.blocks(:limit => 1).count
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "blocking a follower" do
|
100
|
+
context "in my following list" do
|
101
|
+
setup do
|
102
|
+
@jon.block(@sam)
|
103
|
+
end
|
104
|
+
|
105
|
+
should "remove him from followers" do
|
106
|
+
assert_equal 0, @jon.followers_count
|
107
|
+
end
|
108
|
+
|
109
|
+
should "add him to the blocked followers" do
|
110
|
+
assert_equal 1, @jon.blocked_followers_count
|
111
|
+
end
|
112
|
+
|
113
|
+
should "not be able to follow again" do
|
114
|
+
@jon.follow(@sam)
|
115
|
+
assert_equal 0, @jon.followers_count
|
116
|
+
end
|
117
|
+
|
118
|
+
should "not be present when listing followers" do
|
119
|
+
assert_equal [], @jon.followers
|
120
|
+
end
|
121
|
+
|
122
|
+
should "be in the list of blocks" do
|
123
|
+
assert_equal [@sam], @jon.blocks
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context "not in my following list" do
|
128
|
+
setup do
|
129
|
+
@sam.block(@jon)
|
130
|
+
end
|
131
|
+
|
132
|
+
should "add him to the blocked followers" do
|
133
|
+
assert_equal 1, @sam.blocked_followers_count
|
134
|
+
end
|
135
|
+
|
136
|
+
should "not be able to follow again" do
|
137
|
+
@sam.follow(@jon)
|
138
|
+
assert_equal 0, @sam.followers_count
|
139
|
+
end
|
140
|
+
|
141
|
+
should "not be present when listing followers" do
|
142
|
+
assert_equal [], @sam.followers
|
143
|
+
end
|
144
|
+
|
145
|
+
should "be in the list of blocks" do
|
146
|
+
assert_equal [@jon], @sam.blocks
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context "unblocking a blocked follow" do
|
152
|
+
setup do
|
153
|
+
@jon.block(@sam)
|
154
|
+
@jon.unblock(@sam)
|
155
|
+
end
|
156
|
+
|
157
|
+
should "not include the unblocked user in the list of followers" do
|
158
|
+
assert_equal [], @jon.followers
|
159
|
+
end
|
160
|
+
|
161
|
+
should "remove him from the blocked followers" do
|
162
|
+
assert_equal 0, @jon.blocked_followers_count
|
163
|
+
assert_equal [], @jon.blocks
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "unblock a non-existent follow" do
|
168
|
+
setup do
|
169
|
+
@sam.stop_following(@jon)
|
170
|
+
@jon.unblock(@sam)
|
171
|
+
end
|
172
|
+
|
173
|
+
should "not be in the list of followers" do
|
174
|
+
assert_equal [], @jon.followers
|
175
|
+
end
|
176
|
+
|
177
|
+
should "not be in the blockked followers count" do
|
178
|
+
assert_equal 0, @jon.blocked_followers_count
|
179
|
+
end
|
180
|
+
|
181
|
+
should "not be in the blocks list" do
|
182
|
+
assert_equal [], @jon.blocks
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context "followers_by_type" do
|
187
|
+
setup do
|
188
|
+
@sam.follow(@oasis)
|
189
|
+
@jon.follow(@oasis)
|
190
|
+
end
|
191
|
+
|
192
|
+
should "return the followers for given type" do
|
193
|
+
assert_equal [@sam], @jon.followers_by_type('User')
|
194
|
+
assert_equal [@sam,@jon], @oasis.followers_by_type('User')
|
195
|
+
end
|
196
|
+
|
197
|
+
should "not return block followers in the followers for a given type" do
|
198
|
+
@oasis.block(@jon)
|
199
|
+
assert_equal [@sam], @oasis.followers_by_type('User')
|
200
|
+
end
|
201
|
+
|
202
|
+
should "return the count for followers_by_type_count for a given type" do
|
203
|
+
assert_equal 1, @jon.followers_by_type_count('User')
|
204
|
+
assert_equal 2, @oasis.followers_by_type_count('User')
|
205
|
+
end
|
206
|
+
|
207
|
+
should "not count blocked follows in the count" do
|
208
|
+
@oasis.block(@sam)
|
209
|
+
assert_equal 1, @oasis.followers_by_type_count('User')
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
context "method_missing" do
|
214
|
+
setup do
|
215
|
+
@sam.follow(@oasis)
|
216
|
+
@jon.follow(@oasis)
|
217
|
+
end
|
218
|
+
|
219
|
+
should "return the followers for given type" do
|
220
|
+
assert_equal [@sam], @jon.user_followers
|
221
|
+
assert_equal [@sam,@jon], @oasis.user_followers
|
222
|
+
end
|
223
|
+
|
224
|
+
should "not return block followers in the followers for a given type" do
|
225
|
+
@oasis.block(@jon)
|
226
|
+
assert_equal [@sam], @oasis.user_followers
|
227
|
+
end
|
228
|
+
|
229
|
+
should "return the count for followers_by_type_count for a given type" do
|
230
|
+
assert_equal 1, @jon.count_user_followers
|
231
|
+
assert_equal 2, @oasis.count_user_followers
|
232
|
+
end
|
233
|
+
|
234
|
+
should "not count blocked follows in the count" do
|
235
|
+
@oasis.block(@sam)
|
236
|
+
assert_equal 1, @oasis.count_user_followers
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|