diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..222078e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: ruby +rvm: + - 1.9.3 + - 2.0.0 diff --git a/README.rdoc b/README.rdoc index 95c2109..5dce0c4 100644 --- a/README.rdoc +++ b/README.rdoc @@ -6,10 +6,14 @@ Main uses would be for Users to follow other Users or for Users to follow Books, (Basically, to develop the type of follow system that GitHub has) +{}[https://travis-ci.org/tcocca/acts_as_follower] + == Installation -=== The master branch supports rails 3 +=== The master branch supports Rails 4 + +The first release that support Rails 4 is 0.2.0 Add the gem to the gemfile: gem "acts_as_follower" @@ -19,6 +23,23 @@ Run the generator: This will generate a migration file as well as a model called Follow. +=== Rails 3.x support + +Rails 3 is supports in the rails_3 branch https://github.com/tcocca/acts_as_follower/tree/rails_3 +The last gem release for Rails 3 support was 0.1.1, so install the gem using ~> 0.1.1 + +Add the gem to the gemfile: + gem "acts_as_follower", '~> 0.1.1' + +or install from the branch + + gem "acts_as_follower", :git => 'git://github.com/tcocca/acts_as_follower.git', :branch => 'rails_3' + +Run the generator: + rails generate acts_as_follower + +This will generate a migration file as well as a model called Follow. + === Rails 2.3.x support Rails 2.3.x is supported in the rails_2.3.x branch http://github.com/tcocca/acts_as_follower/tree/rails_2.3.x but must be installed as a plugin. @@ -71,7 +92,7 @@ You can check to see if an object that acts_as_follower is following another obj user.following?(book) # Returns true or false To get the total number (count) of follows for a user use the following on a model that acts_as_follower - user.follow_count # Returns and integer + user.follow_count # Returns an integer To get follow records that have not been blocked use the following user.all_follows # returns an array of Follow records @@ -95,6 +116,10 @@ To get the count of all Follow records by a certain type use the following There is also a method_missing to get the count by type user.following_books_count # Calls the user.following_by_type_count('Book') method +There is now a method that will just return the Arel scope for follows so that you can chain anything else you want onto it: + book.follows_scoped +This does not return the actual follows, just the scope of followings including the followables, essentially: book.follows.unblocked.includes(:followable) + The following methods take an optional hash parameter of ActiveRecord options (:limit, :order, etc...) follows_by_type, all_follows, all_following, following_by_type --- @@ -104,6 +129,10 @@ The following methods take an optional hash parameter of ActiveRecord options (: To get all the followers of a model that acts_as_followable book.followers # Returns an array of all the followers for that book, a collection of different object types (eg. type User or type Book) +There is also a method that will just return the Arel scope for followers so that you can chain anything else you want onto it: + book.followers_scoped +This does not return the actual followers, just the scope of followings including the followers, essentially: book.followings.includes(:follower) + To get just the number of follows use book.followers_count diff --git a/Rakefile b/Rakefile index e33a737..ab56276 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ Bundler::GemHelper.install_tasks require 'rake' require 'rake/testtask' -require 'rake/rdoctask' +require 'rdoc/task' desc 'Default: run unit tests.' task :default => :test diff --git a/acts_as_follower.gemspec b/acts_as_follower.gemspec index 7975494..844e6ef 100644 --- a/acts_as_follower.gemspec +++ b/acts_as_follower.gemspec @@ -10,6 +10,7 @@ Gem::Specification.new do |s| s.homepage = "https://github.com/tcocca/acts_as_follower" s.summary = %q{A Rubygem to add Follow functionality for ActiveRecord models} s.description = %q{acts_as_follower is a Rubygem to allow any model to follow any other model. This is accomplished through a double polymorphic relationship on the Follow model. There is also built in support for blocking/un-blocking follow records. Main uses would be for Users to follow other Users or for Users to follow Books, etc… (Basically, to develop the type of follow system that GitHub has)} + s.license = 'MIT' s.rubyforge_project = "acts_as_follower" @@ -19,7 +20,8 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] s.add_development_dependency "sqlite3" - s.add_development_dependency "shoulda" - s.add_development_dependency "factory_girl" - s.add_development_dependency "rails", "~>3.0.10" + s.add_development_dependency "shoulda_create" + s.add_development_dependency "shoulda", ">= 3.5.0" + s.add_development_dependency "factory_girl", ">= 4.2.0" + s.add_development_dependency "rails", "~> 4.0.0" end diff --git a/lib/acts_as_follower/follow_scopes.rb b/lib/acts_as_follower/follow_scopes.rb index 6917132..17a01eb 100644 --- a/lib/acts_as_follower/follow_scopes.rb +++ b/lib/acts_as_follower/follow_scopes.rb @@ -2,7 +2,8 @@ module ActsAsFollower #:nodoc: module FollowScopes def for_follower(follower) - where(:follower_id => follower.id, :follower_type => parent_class_name(follower)) + where(:follower_id => follower.id, + :follower_type => parent_class_name(follower)) end def for_followable(followable) diff --git a/lib/acts_as_follower/followable.rb b/lib/acts_as_follower/followable.rb index 1ef6d1f..7cd5401 100644 --- a/lib/acts_as_follower/followable.rb +++ b/lib/acts_as_follower/followable.rb @@ -25,8 +25,8 @@ def followers_by_type(follower_type, options={}) follows = follower_type.constantize. joins(:follows). where('follows.blocked' => false, - 'follows.followable_id' => self.id, - 'follows.followable_type' => parent_class_name(self), + 'follows.followable_id' => self.id, + 'follows.followable_type' => parent_class_name(self), 'follows.follower_type' => follower_type) if options.has_key?(:limit) follows = follows.limit(options[:limit]) @@ -55,24 +55,36 @@ def method_missing(m, *args) end end + def respond_to?(m, include_private = false) + super || m.to_s[/count_(.+)_followers/] || m.to_s[/(.+)_followers/] + end + def blocked_followers_count self.followings.blocked.count end - # Returns the following records. + # Returns the followings records scoped + def followers_scoped + self.followings.includes(:follower) + end + def followers(options={}) - self.followings.unblocked.includes(:follower).all(options).collect{|f| f.follower} + followers_scope = followers_scoped.unblocked + followers_scope = apply_options_to_scope(followers_scope, options) + followers_scope.to_a.collect{|f| f.follower} end # We don't want your blocks! We want our blocks! # def blocks(options={}) - # self.followings.blocked.includes(:follower).all(options).collect{|f| f.follower} + # blocked_followers_scope = followers_scoped.blocked + # blocked_followers_scope = apply_options_to_scope(blocked_followers_scope, options) + # blocked_followers_scope.to_a.collect{|f| f.follower} # end # Returns true if the current instance is followed by the passed record # Returns false if the current instance is blocked by the passed record or no follow is found def followed_by?(follower) - self.followings.unblocked.for_follower(follower).exists? + self.followings.unblocked.for_follower(follower).first.present? end def block(follower) @@ -90,7 +102,7 @@ def get_follow_for(follower) private def block_future_follow(follower) - follows.create(:followable => self, :follower => follower, :blocked => true) + Follow.create(:followable => self, :follower => follower, :blocked => true) end def block_existing_follow(follower) diff --git a/lib/acts_as_follower/follower.rb b/lib/acts_as_follower/follower.rb index 2bef3bb..a086ebe 100644 --- a/lib/acts_as_follower/follower.rb +++ b/lib/acts_as_follower/follower.rb @@ -29,7 +29,7 @@ def follow_count # Does not allow duplicate records to be created. def follow(followable) if self != followable - self.follows.find_or_create_by_followable_id_and_followable_type(followable.id, parent_class_name(followable)) + self.follows.find_or_create_by(followable_id: followable.id, followable_type: parent_class_name(followable)) end end @@ -40,14 +40,21 @@ def stop_following(followable) end end + # returns the follows records to the current instance + def follows_scoped + self.follows.unblocked.includes(:followable) + end + # Returns the follow records related to this instance by type. def follows_by_type(followable_type, options={}) - self.follows.unblocked.includes(:followable).for_followable_type(followable_type).all(options) + follows_scope = follows_scoped.for_followable_type(followable_type) + follows_scope = apply_options_to_scope(follows_scope, options) end # Returns the follow records related to this instance with the followable included. def all_follows(options={}) - self.follows.unblocked.includes(:followable).all(options) + follows_scope = follows_scoped + follows_scope = apply_options_to_scope(follows_scope, options) end # Returns the actual records which this instance is following. @@ -60,8 +67,8 @@ def following_by_type(followable_type, options={}) followables = followable_type.constantize. joins(:followings). where('follows.blocked' => false, - 'follows.follower_id' => self.id, - 'follows.follower_type' => parent_class_name(self), + 'follows.follower_id' => self.id, + 'follows.follower_type' => parent_class_name(self), 'follows.followable_type' => followable_type) if options.has_key?(:limit) followables = followables.limit(options[:limit]) @@ -90,6 +97,10 @@ def method_missing(m, *args) end end + def respond_to?(m, include_private = false) + super || m.to_s[/following_(.+)_count/] || m.to_s[/following_(.+)/] + end + # Returns a follow record for the current instance and followable object. def get_follow(followable) self.follows.unblocked.for_followable(followable).first diff --git a/lib/acts_as_follower/follower_lib.rb b/lib/acts_as_follower/follower_lib.rb index 5a57806..2bd01b4 100644 --- a/lib/acts_as_follower/follower_lib.rb +++ b/lib/acts_as_follower/follower_lib.rb @@ -11,5 +11,23 @@ def parent_class_name(obj) return obj.class.name end + def apply_options_to_scope(scope, options = {}) + if options.has_key?(:limit) + scope = scope.limit(options[:limit]) + end + if options.has_key?(:includes) + scope = scope.includes(options[:includes]) + end + if options.has_key?(:joins) + scope = scope.joins(options[:joins]) + end + if options.has_key?(:where) + scope = scope.where(options[:where]) + end + if options.has_key?(:order) + scope = scope.order(options[:order]) + end + scope + end end end diff --git a/lib/acts_as_follower/version.rb b/lib/acts_as_follower/version.rb index 5ac1999..5a19d16 100644 --- a/lib/acts_as_follower/version.rb +++ b/lib/acts_as_follower/version.rb @@ -1,3 +1,3 @@ module ActsAsFollower - VERSION = "0.1.1" + VERSION = "0.2.1" end diff --git a/test/acts_as_followable_test.rb b/test/acts_as_followable_test.rb index 8b9364d..b7416a7 100644 --- a/test/acts_as_followable_test.rb +++ b/test/acts_as_followable_test.rb @@ -4,7 +4,7 @@ class ActsAsFollowableTest < ActiveSupport::TestCase context "instance methods" do setup do - @sam = Factory(:sam) + @sam = FactoryGirl.create(:sam) end should "be defined" do @@ -16,10 +16,10 @@ class ActsAsFollowableTest < ActiveSupport::TestCase context "acts_as_followable" do setup do - @sam = Factory(:sam) - @jon = Factory(:jon) - @oasis = Factory(:oasis) - @metallica = Factory(:metallica) + @sam = FactoryGirl.create(:sam) + @jon = FactoryGirl.create(:jon) + @oasis = FactoryGirl.create(:oasis) + @metallica = FactoryGirl.create(:metallica) @sam.follow(@jon) end @@ -30,7 +30,7 @@ class ActsAsFollowableTest < ActiveSupport::TestCase end should "return the proper number of multiple followers" do - @bob = Factory(:bob) + @bob = FactoryGirl.create(:bob) @sam.follow(@bob) assert_equal 0, @sam.followers_count assert_equal 1, @jon.followers_count @@ -45,7 +45,7 @@ class ActsAsFollowableTest < ActiveSupport::TestCase end should "return users (multiple followers)" do - @bob = Factory(:bob) + @bob = FactoryGirl.create(:bob) @sam.follow(@bob) assert_equal [], @sam.followers assert_equal [@sam], @jon.followers @@ -53,7 +53,7 @@ class ActsAsFollowableTest < ActiveSupport::TestCase end should "return users (multiple followers, complex)" do - @bob = Factory(:bob) + @bob = FactoryGirl.create(:bob) @sam.follow(@bob) @jon.follow(@bob) assert_equal [], @sam.followers @@ -62,7 +62,7 @@ class ActsAsFollowableTest < ActiveSupport::TestCase end should "accept AR options" do - @bob = Factory(:bob) + @bob = FactoryGirl.create(:bob) @bob.follow(@jon) assert_equal 1, @jon.followers(:limit => 1).count end @@ -86,7 +86,7 @@ class ActsAsFollowableTest < ActiveSupport::TestCase context "get follow record" do setup do - @bob = Factory(:bob) + @bob = FactoryGirl.create(:bob) @follow = @bob.follow(@sam) end @@ -101,7 +101,7 @@ class ActsAsFollowableTest < ActiveSupport::TestCase context "blocks" do setup do - @bob = Factory(:bob) + @bob = FactoryGirl.create(:bob) @jon.block(@sam) @jon.block(@bob) end @@ -251,6 +251,18 @@ class ActsAsFollowableTest < ActiveSupport::TestCase assert_equal 1, @oasis.count_user_followers end end + + context "respond_to?" do + should "advertise that it responds to following methods" do + assert @oasis.respond_to?(:user_followers) + assert @oasis.respond_to?(:user_followers_count) + end + + should "return false when called with a nonexistent method" do + assert (not @oasis.respond_to?(:foobar)) + end + end + end end diff --git a/test/acts_as_follower_test.rb b/test/acts_as_follower_test.rb index 01eed6d..59b8f8c 100644 --- a/test/acts_as_follower_test.rb +++ b/test/acts_as_follower_test.rb @@ -4,7 +4,7 @@ class ActsAsFollowerTest < ActiveSupport::TestCase context "instance methods" do setup do - @sam = Factory(:sam) + @sam = FactoryGirl.create(:sam) end should "be defined" do @@ -19,9 +19,9 @@ class ActsAsFollowerTest < ActiveSupport::TestCase context "acts_as_follower" do setup do - @sam = Factory(:sam) - @jon = Factory(:jon) - @oasis = Factory(:oasis) + @sam = FactoryGirl.create(:sam) + @jon = FactoryGirl.create(:jon) + @oasis = FactoryGirl.create(:oasis) @sam.follow(@jon) @sam.follow(@oasis) end @@ -83,8 +83,8 @@ class ActsAsFollowerTest < ActiveSupport::TestCase context "follows" do setup do - @band_follow = Follow.find(:first, :conditions => ["follower_id = ? and follower_type = 'User' and followable_id = ? and followable_type = 'Band'", @sam.id, @oasis.id]) - @user_follow = Follow.find(:first, :conditions => ["follower_id = ? and follower_type = 'User' and followable_id = ? and followable_type = 'User'", @sam.id, @jon.id]) + @band_follow = Follow.where("follower_id = ? and follower_type = 'User' and followable_id = ? and followable_type = 'Band'", @sam.id, @oasis.id).first + @user_follow = Follow.where("follower_id = ? and follower_type = 'User' and followable_id = ? and followable_type = 'User'", @sam.id, @jon.id).first end context "follows_by_type" do @@ -94,7 +94,7 @@ class ActsAsFollowerTest < ActiveSupport::TestCase end should "accept AR options" do - @metallica = Factory(:metallica) + @metallica = FactoryGirl.create(:metallica) @sam.follow(@metallica) assert_equal 1, @sam.follows_by_type('Band', :limit => 1).count end @@ -102,7 +102,7 @@ class ActsAsFollowerTest < ActiveSupport::TestCase context "following_by_type_count" do should "return the count of the requested type" do - @metallica = Factory(:metallica) + @metallica = FactoryGirl.create(:metallica) @sam.follow(@metallica) assert_equal 2, @sam.following_by_type_count('Band') assert_equal 1, @sam.following_by_type_count('User') @@ -134,9 +134,13 @@ class ActsAsFollowerTest < ActiveSupport::TestCase assert_equal [], @jon.all_following end - should "accept AR options" do + should "accept AR limit option" do assert_equal 1, @sam.all_following(:limit => 1).count end + + should "accept AR where option" do + assert_equal 1, @sam.all_following(:where => { :id => @oasis.id }).count + end end context "following_by_type" do @@ -146,7 +150,7 @@ class ActsAsFollowerTest < ActiveSupport::TestCase end should "accept AR options" do - @metallica = Factory(:metallica) + @metallica = FactoryGirl.create(:metallica) @sam.follow(@metallica) assert_equal 1, @sam.following_by_type('Band', :limit => 1).to_a.size end @@ -159,7 +163,7 @@ class ActsAsFollowerTest < ActiveSupport::TestCase end should "call following_by_type_count" do - @metallica = Factory(:metallica) + @metallica = FactoryGirl.create(:metallica) @sam.follow(@metallica) assert_equal 2, @sam.following_bands_count assert_equal 1, @sam.following_users_count @@ -173,6 +177,17 @@ class ActsAsFollowerTest < ActiveSupport::TestCase end end + context "respond_to?" do + should "advertise that it responds to following methods" do + assert @sam.respond_to?(:following_users) + assert @sam.respond_to?(:following_users_count) + end + + should "return false when called with a nonexistent method" do + assert (not @sam.respond_to?(:foobar)) + end + end + context "destroying follower" do setup do @jon.destroy diff --git a/test/dummy30/config/environments/test.rb b/test/dummy30/config/environments/test.rb index c89e813..770a6ea 100644 --- a/test/dummy30/config/environments/test.rb +++ b/test/dummy30/config/environments/test.rb @@ -7,9 +7,6 @@ # and recreated between test runs. Don't rely on the data there! config.cache_classes = true - # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true - # Show full error reports and disable caching config.consider_all_requests_local = true diff --git a/test/factories/bands.rb b/test/factories/bands.rb index 4e7e5f9..3e6eb9b 100644 --- a/test/factories/bands.rb +++ b/test/factories/bands.rb @@ -1,7 +1,9 @@ -Factory.define :oasis, :class => Band do |b| - b.name 'Oasis' -end +FactoryGirl.define do + factory :oasis, :class => Band do |b| + b.name 'Oasis' + end -Factory.define :metallica, :class => Band do |b| - b.name 'Metallica' + factory :metallica, :class => Band do |b| + b.name 'Metallica' + end end diff --git a/test/factories/users.rb b/test/factories/users.rb index 3aef0c0..b819f61 100644 --- a/test/factories/users.rb +++ b/test/factories/users.rb @@ -1,11 +1,13 @@ -Factory.define :jon, :class => User do |u| - u.name 'Jon' -end +FactoryGirl.define do + factory :jon, class: User do |u| + u.name 'Jon' + end -Factory.define :sam, :class => User do |u| - u.name 'Sam' -end + factory :sam, :class => User do |u| + u.name 'Sam' + end -Factory.define :bob, :class => User do |u| - u.name 'Bob' + factory :bob, :class => User do |u| + u.name 'Bob' + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 3875d13..c26c441 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,6 +12,7 @@ require File.dirname(__FILE__) + '/../lib/generators/templates/model.rb' require 'shoulda' +require 'shoulda_create' require 'factory_girl' +ActiveSupport::TestCase.extend(ShouldaCreate) FactoryGirl.find_definitions -