acts_as_follower 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +44 -0
  4. data/README.rdoc +208 -0
  5. data/Rakefile +39 -0
  6. data/acts_as_follower.gemspec +25 -0
  7. data/init.rb +1 -0
  8. data/lib/acts_as_follower.rb +11 -0
  9. data/lib/acts_as_follower/followable.rb +102 -0
  10. data/lib/acts_as_follower/follower.rb +100 -0
  11. data/lib/acts_as_follower/follower_lib.rb +15 -0
  12. data/lib/acts_as_follower/railtie.rb +15 -0
  13. data/lib/acts_as_follower/version.rb +3 -0
  14. data/lib/generators/USAGE +5 -0
  15. data/lib/generators/acts_as_follower_generator.rb +30 -0
  16. data/lib/generators/templates/migration.rb +17 -0
  17. data/lib/generators/templates/model.rb +21 -0
  18. data/test/README +24 -0
  19. data/test/acts_as_followable_test.rb +241 -0
  20. data/test/acts_as_follower_test.rb +209 -0
  21. data/test/dummy30/Gemfile +1 -0
  22. data/test/dummy30/Rakefile +7 -0
  23. data/test/dummy30/app/models/band.rb +4 -0
  24. data/test/dummy30/app/models/user.rb +5 -0
  25. data/test/dummy30/config.ru +4 -0
  26. data/test/dummy30/config/application.rb +42 -0
  27. data/test/dummy30/config/boot.rb +10 -0
  28. data/test/dummy30/config/database.yml +7 -0
  29. data/test/dummy30/config/environment.rb +5 -0
  30. data/test/dummy30/config/environments/development.rb +19 -0
  31. data/test/dummy30/config/environments/test.rb +23 -0
  32. data/test/dummy30/config/initializers/backtrace_silencers.rb +7 -0
  33. data/test/dummy30/config/initializers/inflections.rb +10 -0
  34. data/test/dummy30/config/initializers/secret_token.rb +7 -0
  35. data/test/dummy30/config/initializers/session_store.rb +8 -0
  36. data/test/dummy30/config/locales/en.yml +5 -0
  37. data/test/dummy30/config/routes.rb +2 -0
  38. data/test/factories/bands.rb +7 -0
  39. data/test/factories/users.rb +11 -0
  40. data/test/follow_test.rb +10 -0
  41. data/test/schema.rb +21 -0
  42. data/test/test_helper.rb +17 -0
  43. 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,3 @@
1
+ module ActsAsFollower
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ Description:
2
+ rails generate acts_as_follower
3
+
4
+ no need to specify a name after acts_as_follower as you can not change the model name from Follow
5
+ the acts_as_follower_migration file will be created in db/migrate
@@ -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