Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit bf5870bd6c523715528025ef17916eb866f51330 0 parents
@fnando fnando authored
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Nando Vieira
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
103 README
@@ -0,0 +1,103 @@
+has_friends
+===========
+
+Instalation
+-----------
+
+1) Install the plugin with `script/plugin install git://github.com/fnando/has_friends.git`
+2) Generate a migration with `script/generate migration create_friendships` and add the following code:
+
+class CreateFriendships < ActiveRecord::Migration
+ def self.up
+ create_table :friendships do |t|
+ t.references :user, :friend
+ t.datetime :requested_at, :accepted_at, :denied_at, :null => true, :default => nil
+ t.string :status
+ end
+
+ add_index :friendships, [:user_id, :friend_id]
+ add_index :friendships, :status
+ end
+
+ def self.down
+ drop_table :friendships
+ end
+end
+
+3) Run the migrations with `rake db:migrate`
+
+Usage
+-----
+
+1) Add the method call `has_friends` to your model.
+
+class User < ActiveRecord::Base
+ has_friends
+end
+
+john = User.find_by_login 'john'
+mary = User.find_by_login 'mary'
+paul = User.find_by_login 'paul'
+
+# john wants to be friend with mary
+# always return a friendship object
+john.be_friend_with(mary)
+
+# are they friends?
+john.friend_with?(mary)
+
+# get the friendship object
+john.friendship_for(mary)
+
+# get the friendship status
+# can be :pending, :accepted, :denied or nil
+john.friendship_status_for(mary)
+
+# mary accepts john's request if it exists;
+# makes a friendship request otherwise.
+mary.be_friend_with(john)
+
+# check if paul is mary's friend
+mary.friend_with?(paul)
+
+# now it's time for paul to make a john's request;
+# and john accepts it with #accept_friendship_with method,
+# that returns friendship object or false
+paul.be_friend_with(john)
+john.accept_friendship_with(paul)
+
+# paul also wants to be friend with mary;
+# unfortunately, mary denies his request.
+paul.be_friend_with(mary)
+mary.deny_friendship_with(paul)
+
+# you can retrieve mutual friends
+# the secord hash argument will be passed to
+# the User model, when doing the find
+mary.mutual_friends(john, :limit => 10)
+
+# you can retrieve possible friends;
+# is this case, will return only paul
+mary.possible_friends(john, :order => 'name asc')
+
+# check if a user is the current user, so it can
+# be differently presented
+mary.friends.each {|friend| friend.is?(current_user) }
+
+# paginate mary's friends
+friends = mary.friends.paginate(:page => 3, :per_page => 10)
+friends.page_count
+friends.per_page
+friends.total_entries
+
+# if you're dealing with a friendship object,
+# the following methods are available
+friendship.accept!
+friendship.deny!
+friendship.pending!
+friendship.requested_by?(john)
+
+NOTE: You should have a User model. You should also have a friends_count column
+on your model. Otherwise, this won't work!
+
+Copyright (c) 2008 Nando Vieira, released under the MIT license
4 init.rb
@@ -0,0 +1,4 @@
+require "has_friends"
+ActiveRecord::Base.send(:include, SimplesIdeias::Acts::Friendships)
+
+require File.dirname(__FILE__) + "/lib/friendship"
79 lib/friendship.rb
@@ -0,0 +1,79 @@
+class Friendship < ActiveRecord::Base
+ # constants
+ MESSAGES = {
+ :user_is_required => "is required",
+ :friend_is_required => "is required"
+ }
+
+ STATUSES = %w(pending accepted denied)
+
+ # associations
+ belongs_to :user
+ belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
+
+ # validations
+ validates_presence_of :user_id, :user,
+ :message => MESSAGES[:user_is_required]
+
+ validates_presence_of :friend_id, :friend,
+ :message => MESSAGES[:friend_is_required]
+
+ validates_inclusion_of :status,
+ :in => STATUSES
+
+ validate :should_not_add_yourself_as_friend
+
+ # callbacks
+ before_save :ensure_date_is_set_according_to_status
+ before_destroy :decrement_friends_counter
+
+ def accept!
+ write_attribute(:status, 'accepted')
+ write_attribute(:accepted_at, Time.now)
+
+ increment_friends_counter
+
+ save(false)
+ end
+
+ def deny!
+ write_attribute(:status, 'denied')
+ write_attribute(:denied_at, Time.now)
+ save(false)
+ end
+
+ def pending!
+ write_attribute(:status, 'pending')
+ write_attribute(:pending_at, nil)
+ save(false)
+ end
+
+ def requested_by?(u)
+ user == u
+ end
+
+ private
+ def should_not_add_yourself_as_friend
+ errors.add(:friend, 'should not be you') if user && friend && friend_id == user_id
+ end
+
+ def ensure_date_is_set_according_to_status
+ write_attribute(:requested_at, Time.now) if status == 'pending' && requested_at.blank?
+ write_attribute(:denied_at, Time.now) if status == 'denied' && denied_at.blank?
+ write_attribute(:accepted_at, Time.now) if status == 'accepted' && accepted_at.blank?
+ end
+
+ def increment_friends_counter
+ [friend, user].each do |u|
+ u.friends_count += 1
+ u.save(false)
+ end
+ end
+
+ def decrement_friends_counter
+ [friend, user].each do |u|
+ u.friends_count -= 1
+ u.save(false)
+ end
+ end
+end
168 lib/has_friends.rb
@@ -0,0 +1,168 @@
+module SimplesIdeias
+ module Acts
+ module Friendships
+ def self.included(base)
+ base.extend SimplesIdeias::Acts::Friendships::ClassMethods
+ end
+
+ module ClassMethods
+ FRIENDS_QUERY = 'SELECT DISTINCT users.* ' +
+ 'FROM users, friendships ' +
+ 'WHERE friendships.status = "accepted" AND ' +
+ '(friendships.user_id = #{id} OR friendships.friend_id = #{id}) AND ' +
+ 'users.id IN(friendships.user_id, friendships.friend_id) AND ' +
+ 'users.id <> #{id} ' +
+ 'ORDER BY users.name'
+
+ def has_friends
+ include SimplesIdeias::Acts::Friendships::InstanceMethods
+
+ # all your friendships
+ has_many :friendships
+
+ # all your friends
+ has_many :friends,
+ :finder_sql => FRIENDS_QUERY,
+ :class_name => 'User' do
+ def paginate(options={})
+ options[:per_page] ||= 10
+ options[:page] ||= 1
+
+ page = [1, options.delete(:page).to_i].max
+ per_page = options.delete(:per_page).to_i
+ @total_entries = proxy_owner.friends.count
+ @total_pages = (@total_entries.to_f / per_page).ceil
+
+ offset = (page - 1) * per_page
+
+ query = FRIENDS_QUERY.dup + " LIMIT #{offset},#{per_page}"
+ query.gsub! /#\{id\}/sm, proxy_owner.id.to_s
+ users = User.find_by_sql [query, offset, per_page]
+
+ users.instance_variable_set('@total_entries', @total_entries)
+ users.instance_variable_set('@total_pages', @total_pages)
+ users.instance_variable_set('@per_page', per_page)
+
+ def users.total_entries
+ @total_entries
+ end
+
+ def users.per_page
+ @per_page
+ end
+
+ def users.page_count
+ @total_pages
+ end
+
+ users
+ end
+ end
+
+ # return all friendships users requested
+ # and you haven't accepted yet
+ has_many :requested_friendships,
+ :class_name => 'Friendship',
+ :foreign_key => 'friend_id',
+ :conditions => {:status => 'pending'}
+
+ # return all friendships you requested
+ # that wasn't accepted yet
+ has_many :pending_friendships,
+ :class_name => 'Friendship',
+ :conditions => {:status => 'pending'}
+
+ # return all accepted friendships you requested
+ has_many :accepted_friendships,
+ :class_name => 'Friendship',
+ :conditions => {:status => 'accepted'}
+
+ # return all denied friendships you requested
+ has_many :denied_friendships,
+ :class_name => 'Friendship',
+ :conditions => {:status => 'denied'}
+ end
+ end
+
+ module InstanceMethods
+ def friendship_for(friend)
+ for_conditions = [friend, self].flatten
+ Friendship.find(:first, :conditions => ["friend_id IN(?) AND user_id IN(?)", for_conditions, for_conditions])
+ end
+
+ def friendship_status_for(friend)
+ friendship = friendship_for(friend)
+ return friendship.status.to_sym if friendship
+ return nil
+ end
+
+ def friendship_status?(friend, status)
+ friendship_status_for(friend) == status.to_sym
+ end
+
+ def be_friend_with(friend)
+ # Users are already friends
+ return friendship_for(friend) if friend_with?(friend)
+
+ # Check if I have a request from this user; if so, just create the friendship
+ friendship = requested_friendships.find(:first, :conditions => {:user_id => id_for(friend)})
+ return friendship if friendship && friendship.accept!
+
+ # Check if a friendship has been already requested
+ return friendship if friendship_status_for(friend) == :pending
+
+ # Has a friendship request, so set it to pending
+ friendship = friendship_for(friend)
+ return friendship if friendship && friendship.pending!
+
+ # Yay! Just request a friendship
+ friendship = friendships.create(:friend_id => id_for(friend), :requested_at => Time.now, :status => 'pending')
+ return friendship
+ end
+
+ def friend_with?(friend)
+ friendship_status?(friend, :accepted)
+ end
+
+ def accept_friendship_with(friend)
+ friendship = friendship_for(friend)
+ return friendship if friendship && friendship.accept!
+ false
+ end
+
+ def deny_friendship_with(friend)
+ friendship = friendship_for(friend)
+ return friendship if friendship && friendship.deny!
+ false
+ end
+
+ def remove_friendship_with(friend)
+ friendship = friendship_for(friend)
+ return friendship if friendship && friendship.destroy
+ false
+ end
+
+ def mutual_friends(friend, options={})
+ common_friends_ids = (friend_ids & friend.friend_ids).uniq
+ User.find(common_friends_ids, options)
+ end
+
+ def possible_friends(friend, options={})
+ not_friends_ids = (friend_ids - friend.friend_ids).uniq
+ User.find(not_friends_ids, options)
+ end
+
+ def is?(friend)
+ self == friend
+ end
+
+ private
+ def id_for(object)
+ return nil unless object
+ return object.id unless object.is_a?(Integer)
+ return object
+ end
+ end
+ end
+ end
+end
11 test/fixtures/users.yml
@@ -0,0 +1,11 @@
+homer:
+ name: Homer Simpson
+
+bart:
+ name: Bart Simpson
+
+moe:
+ name: Moe Szyslak
+
+burns:
+ name: Charles Montgomery Burns
373 test/has_friends_spec.rb
@@ -0,0 +1,373 @@
+require "spec_helper"
+
+# unset models used for testing purposes
+Object.unset_class('User')
+
+class User < ActiveRecord::Base
+ has_many :friendships, :dependent => :destroy
+ has_friends
+end
+
+describe "has_friends" do
+ fixtures :users
+
+ before(:each) do
+ @now = Time.now
+ @friendships = []
+ @user = User.create(:name => 'John')
+ end
+
+ it "should have friends association" do
+ lambda { @homer.friends }.should_not raise_error
+ end
+
+ it "should have friendships association" do
+ lambda { @homer.friendships }.should_not raise_error
+ end
+
+ it "should have requested_friendships association" do
+ lambda { @homer.requested_friendships }.should_not raise_error
+ end
+
+ it "should have pending_friendships association" do
+ lambda { @homer.pending_friendships }.should_not raise_error
+ end
+
+ it "should have accepted_friendships association" do
+ lambda { @homer.accepted_friendships }.should_not raise_error
+ end
+
+ it "should have denied_friendships association" do
+ lambda { @homer.denied_friendships }.should_not raise_error
+ end
+
+ it "homer should request friendship with bart" do
+ lambda do
+ create_friendship_between(:homer, :bart)
+ @homer.friendship_status_for(@bart).should == :pending
+ end.should change(Friendship, :count).by(1)
+ end
+
+ it "bart should accept homer friendship requesting" do
+ create_friendship_between(:homer, :bart)
+ @homer.accept_friendship_with(@bart)
+ @homer.should be_friend_with(@bart)
+ end
+
+ it "bart should deny homer friendship requesting" do
+ create_friendship_between(:homer, :bart)
+ @bart.deny_friendship_with(@homer)
+ @bart.friendship_status_for(@homer).should == :denied
+ end
+
+ it "bart should automatically be friend with homer if homer has added him first" do
+ create_friendship_between(:homer, :bart)
+ create_friendship_between(:bart, :homer)
+
+ @homer.should be_friend_with(@bart)
+ end
+
+ it "should remove friendship between homer and bart" do
+ create_friendship_between(:homer, :bart)
+
+ @homer.remove_friendship_with(@bart)
+ @homer.friendship_for(@bart).should be_nil
+ end
+
+ it "should not remove an inexistent friendship between homer and bart" do
+ @homer.remove_friendship_with(@bart).should be_false
+ end
+
+ it "should set requested_at when adding an user as friend" do
+ Time.should_receive(:now).at_least(:once).and_return(@now)
+ friendship = create_friendship_between(:homer, :bart)
+ friendship.requested_at.to_s(:db).should == @now.to_s(:db)
+ end
+
+ it "should set accepted_at when accepting a friendship" do
+ Time.should_receive(:now).at_least(:once).and_return(@now)
+ friendship = create_friendship_between(:homer, :bart)
+ @homer.accept_friendship_with(@bart)
+ friendship.reload
+ friendship.accepted_at.to_s(:db).should == @now.to_s(:db)
+ end
+
+ it "should set denied_at when denying a friendship" do
+ Time.should_receive(:now).at_least(:once).and_return(@now)
+ friendship = create_friendship_between(:homer, :bart)
+ @homer.deny_friendship_with(@bart)
+ friendship.reload
+ friendship.denied_at.to_s(:db).should == @now.to_s(:db)
+ end
+
+ it "homer should get requested friendships from moe and bart" do
+ @friendships << create_friendship_between(:moe, :homer)
+ @friendships << create_friendship_between(:bart, :homer)
+
+ @homer.requested_friendships.should == @friendships
+ end
+
+ it "homer should get pending friendships" do
+ @friendships << create_friendship_between(:moe, :homer)
+ @friendships << create_friendship_between(:bart, :homer)
+
+ @homer.requested_friendships.should == @friendships
+ end
+
+ it "homer should get denied friendships" do
+ @friendships << create_friendship_between(:homer, :moe)
+ @friendships << create_friendship_between(:homer, :bart)
+
+ @friendships.each(&:deny!).each(&:reload)
+
+ @homer.denied_friendships.should == @friendships
+ end
+
+ it "homer should get accepted friendships" do
+ @friendships << create_friendship_between(:homer, :moe)
+ @friendships << create_friendship_between(:homer, :bart)
+
+ @friendships.each(&:accept!).each(&:reload)
+
+ @homer.accepted_friendships.should == @friendships
+ end
+
+ it "homer should get all his friends" do
+ @friendships << create_friendship_between(:homer, :moe)
+ @friendships << create_friendship_between(:homer, :bart)
+
+ @friendships.each(&:accept!)
+ @homer.friends.should == [@bart, @moe]
+ end
+
+ it "should not add yourself as friend" do
+ create_friendship_between(:homer, :homer).should_not be_valid
+ end
+
+ it "should require friend" do
+ lambda do
+ friendship = @homer.be_friend_with(nil)
+ friendship.errors.on(:friend).should_not be_nil
+ end.should_not change(Friendship, :count)
+ end
+
+ it "should get common friends between homer and moe" do
+ @friendships << create_friendship_between(:homer, :moe)
+ @friendships << create_friendship_between(:homer, :burns)
+ @friendships << create_friendship_between(:bart, :moe)
+
+ @friendships.each(&:accept!)
+
+ @homer.mutual_friends(@bart).should == [@moe]
+ end
+
+ it "should get people homer may know by his connection with moe" do
+ @friendships << create_friendship_between(:homer, :moe)
+ @friendships << create_friendship_between(:homer, :burns)
+ @friendships << create_friendship_between(:bart, :moe)
+
+ @friendships.each(&:accept!)
+ @homer.possible_friends(@bart).should == [@burns]
+ end
+
+ describe "friends_count" do
+ it "should be incremented using accept_friendship_with" do
+ @user.be_friend_with(@homer)
+ @homer.accept_friendship_with(@user)
+
+ @user.reload
+ @homer.reload
+
+ @user.friends_count.should == 1
+ @homer.friends_count.should == 1
+ end
+
+ it "should be incremented using be_friend_with" do
+ @user.be_friend_with(@homer)
+ @homer.be_friend_with(@user)
+
+ @user.reload
+ @homer.reload
+
+ @user.friends_count.should == 1
+ @homer.friends_count.should == 1
+ end
+
+ it "should be decremented when removing a friendship" do
+ @user.be_friend_with(@homer)
+ @homer.be_friend_with(@user)
+
+ @user.remove_friendship_with(@homer)
+
+ @user.reload
+ @homer.reload
+
+ @user.friends_count.should == 0
+ @homer.friends_count.should == 0
+ end
+
+ it "should be decremented when removing a friendship using destroy method" do
+ @user.be_friend_with(@homer)
+ friendship = @homer.be_friend_with(@user)
+
+ friendship.destroy
+
+ @user.reload
+ @homer.reload
+
+ @user.friends_count.should == 0
+ @homer.friends_count.should == 0
+ end
+ end
+
+ describe "friends#paginate" do
+ before(:each) do
+ User.destroy_all
+ @user = User.create(:name => 'John')
+
+ (1..35).each do |i|
+ @another_user = User.create(:name => "User #{i}")
+ @user.be_friend_with(@another_user)
+ friendship = @another_user.accept_friendship_with(@user)
+
+ @user.reload
+ @another_user.reload
+ end
+ end
+
+ it "should count" do
+ @user.friends.count.should == 35
+ end
+
+ it "should use defaults" do
+ @friends = @user.friends.paginate
+ @friends.size.should == 10
+ @friends.should == do_query
+ end
+
+ it "should use custom page" do
+ @friends = @user.friends.paginate(:page => 3)
+ @friends.should == do_query(:offset => 20)
+ end
+
+ it "should use custom size" do
+ @friends = @user.friends.paginate(:per_page => 3)
+ @friends.should == do_query(:limit => 3)
+ end
+
+ it "should use custom page and size" do
+ @friends = @user.friends.paginate(:page => 2, :per_page => 3)
+ @friends.should == do_query(:offset => 3, :limit => 3)
+ end
+
+ it "should return the number of pages" do
+ @friends = @user.friends.paginate(:per_page => 5)
+ @friends.page_count.should == 7
+
+ @friends = @user.friends.paginate(:per_page => 1)
+ @friends.page_count.should == 35
+
+ @friends = @user.friends.paginate(:per_page => 34)
+ @friends.page_count.should == 2
+ end
+
+ it "should return the number of entries" do
+ @friends = @user.friends.paginate
+ @friends.total_entries.should == 35
+ end
+
+ it "should return the number of items per page" do
+ @friends = @user.friends.paginate(:per_page => 3)
+ @friends.per_page.should == 3
+ end
+
+ private
+ def do_query(options={})
+ User.all({
+ :limit => 10,
+ :offset => 0,
+ :order => "name asc",
+ :conditions => ["id <> ?", @user]
+ }.merge(options))
+ end
+ end
+
+ describe Friendship do
+ it "should create friendship" do
+ lambda do
+ friendship = create_friendship
+ friendship.should be_valid
+ end.should change(Friendship, :count).by(1)
+ end
+
+ it "should require status" do
+ lambda do
+ friendship = create_friendship(:status => nil)
+ friendship.errors.on(:status).should_not be_nil
+ end.should_not change(Friendship, :count)
+ end
+
+ it "should require valid status" do
+ lambda do
+ friendship = create_friendship(:status => "invalid")
+ friendship.errors.on(:status).should_not be_nil
+ end.should_not change(Friendship, :count)
+ end
+
+ it "should set requested_at when creating friendship with status equals to pending" do
+ Time.should_receive(:now).at_least(:once).and_return(@now)
+ friendship = create_friendship(:status => "pending")
+ friendship.requested_at.to_s(:db).should == @now.to_s(:db)
+ end
+
+ it "should set accepted_at when creating friendship with status equals to accepted" do
+ Time.should_receive(:now).at_least(:once).and_return(@now)
+ friendship = create_friendship(:status => "accepted")
+ friendship.accepted_at.to_s(:db).should == @now.to_s(:db)
+ end
+
+ it "should set denied_at when creating friendship with status equals to denied" do
+ Time.should_receive(:now).at_least(:once).and_return(@now)
+ friendship = create_friendship(:status => "denied")
+ friendship.denied_at.to_s(:db).should == @now.to_s(:db)
+ end
+
+ it "homer should be detected as the guy who started the friendship" do
+ friendship = create_friendship_between(:homer, :moe)
+ friendship.should be_requested_by(@homer)
+ end
+
+ it "should mark friendship as accepted" do
+ friendship = create_friendship(:status => 'pending')
+ friendship.accept!.should be_true
+ friendship.status.should == 'accepted'
+ end
+
+ it "should mark friendship as denied" do
+ friendship = create_friendship(:status => 'pending')
+ friendship.deny!.should be_true
+ friendship.status.should == 'denied'
+ end
+
+ it "should request friendship only once when users are friends" do
+ lambda {
+ friendship = create_friendship_between(:homer, :bart)
+ friendship.accept!
+ create_friendship_between(:homer, :bart)
+ }.should change(Friendship, :count).by(1)
+ end
+ end
+
+ private
+ def create_friendship_between(from, to)
+ users(from).be_friend_with(users(to))
+ end
+
+ def create_friendship(options={})
+ Friendship.create({
+ :user => users(:homer),
+ :friend => users(:bart),
+ :status => 'pending'
+ }.merge(options))
+ end
+end
12 test/schema.rb
@@ -0,0 +1,12 @@
+ActiveRecord::Schema.define(:version => 0) do
+ create_table :users do |t|
+ t.string :name
+ t.integer :friends_count, :null => false, :default => 0
+ end
+
+ create_table :friendships do |t|
+ t.references :user, :friend
+ t.datetime :requested_at, :accepted_at, :denied_at, :null => true, :default => nil
+ t.string :status
+ end
+end
29 test/spec_helper.rb
@@ -0,0 +1,29 @@
+ENV["RAILS_ENV"] = "test"
+
+require File.expand_path("../../../../config/environment")
+require "spec"
+require "spec/rails"
+require "ruby-debug"
+
+ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ":memory:"}}
+ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["test"])
+ActiveRecord::Base.logger = Logger.new(RAILS_ROOT + '/log/plugin.log')
+
+load('schema.rb')
+
+Spec::Runner.configure do |config|
+ config.use_transactional_fixtures = true
+ config.use_instantiated_fixtures = true
+ config.fixture_path = File.dirname(__FILE__) + '/fixtures/'
+end
+
+class Object
+ def self.unset_class(*args)
+ class_eval do
+ args.each do |klass|
+ eval(klass) rescue nil
+ remove_const(klass) if const_defined?(klass)
+ end
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.