Social graph, twitter style follow system - SeanHolden/Wiki GitHub Wiki

#Create a Twitter style Follow System using Ruby on Rails in less than 5 minutes ##Step-by-step guide ( Based on the guide from railstutorial.org )

NOTE: Guide is written for Rails 3.

###Assuming you already have some kind of User model…

###Generate a new relationship model: $ rails generate model Relationship follower_id:integer followed_id:integer

Since we will be finding relationships by follower_id and by followed_id, we should add an index on each column for efficiency.

Open your newly generated migration file:

# db/migrate/[timestamp]_create_relationships.rb 

class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end

    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

Notes:

  • Notice how we added a composite index that enforces uniqueness of pairs of (follower_id, followed_id), so that a user can’t follow another user more than once.

  • We could also add a uniqueness validation to the Relationship model, but because it is always an error to create duplicate relationships, due to the composite index, this code is sufficient enough.

###Now… migrate your changes!

$ bundle exec rake db:migrate

###User/relationship associations

A user has_many relationships, and—since relationships involve two users—a relationship belongs_to both a follower and a followed user.

######Add the following 'has_many' associations to your User model:

# app/models/user.rb

has_many :relationships, foreign_key: "follower_id", dependent: :destroy
has_many :followed_users, through: :relationships, source: :followed

has_many :reverse_relationships, foreign_key: "followed_id", class_name: "Relationship", dependent: :destroy
has_many :followers, through: :reverse_relationships, source: :follower

######Add the following 'belongs_to' associations to the Relationship model, along with validations:

# app/models/relationship.rb

class Relationship < ActiveRecord::Base
  attr_accessible :followed_id

  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"

  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

###Add some useful methods to User model

  • We want to be able to write user.following?(other_user) to check whether the user is following a particular user or not.

  • We want to be able to write user.follow!(other_user) to create the relationship between those users. We use an exclamation point that an exception will be raised on failure.

  • We want to be able to write user.unfollow!(other_user) to destroy the relationship between those users.

######Add the following lines of code to the User model:

# app/models/user.rb

  def following?(other_user)
    relationships.find_by_followed_id(other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end
  
  def unfollow!(other_user)
    relationships.find_by_followed_id(other_user.id).destroy
  end

#That's it! You're done! (unless you didn't test along the way… in which case read below)…

So far, for speed and just to get to the point, I did not include tests in this guide. If you want to see a guide on how to test this with rspec, then see below: ##How I tested with RSpec:

Note: It is probably better practice to use a gem such as factory_girl to create test users. This guide, however, is written without factory_girl.

####Testing the Relationship model:

# spec/models/relationship_spec.rb

require 'spec_helper'

describe Relationship do
  before(:each) do
    @attr1 = { 
      :email => "[email protected]",
      :firstname => "somefirstname",
      :lastname => "somelastname",
      :password => "password123",
      :password_confirmation => "password123",
      :agree_to_terms => true
    }
    @attr2 = { 
      :email => "[email protected]",
      :firstname => "anotherfirstname",
      :lastname => "anotherlastname",
      :password => "password123",
      :password_confirmation => "password123",
      :agree_to_terms => true
    }
  end

  let(:follower) { User.create(@attr1) }
  let(:followed) { User.create(@attr2) }
  let(:relationship) { follower.relationships.build(followed_id: followed.id) }

  subject { relationship }

  it { should be_valid }

  context "accessible attributes" do
    it "should not allow access to follower_id" do
      expect do
        Relationship.new(follower_id: follower.id)
      end.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
    end    
  end

  context "follower methods" do    
    it { should respond_to(:follower) }
    it { should respond_to(:followed) }
    its(:follower) { should == follower }
    its(:followed) { should == followed }
  end

  context "when followed id is not present" do
    before { relationship.followed_id = nil }
    it { should_not be_valid }
  end

  context "when follower id is not present" do
    before { relationship.follower_id = nil }
    it { should_not be_valid }
  end
end

####Testing the User model:

# spec/models/relationship_spec.rb

require 'spec_helper'

describe User do
  before(:each) do
    @attr1 = { 
      :email => "[email protected]",
      :firstname => "somefirstname",
      :lastname => "somelastname",
      :password => "password123",
      :password_confirmation => "password123",
      :agree_to_terms => true
    }
    @attr2 = { 
      :email => "[email protected]",
      :firstname => "anotherfirstname",
      :lastname => "anotherlastname",
      :password => "password123",
      :password_confirmation => "password123",
      :agree_to_terms => true
    }
  end

  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }  
  it { should respond_to(:following?) }
  it { should respond_to(:follow!) }
  it { should respond_to(:unfollow!) }
  it { should respond_to(:reverse_relationships) }
  it { should respond_to(:followers) }
 

  context "following users" do
    let(:user)       { User.create(@attr1) }
    let(:other_user) { User.create(@attr2) }    
    
    before do
      user.follow!(other_user)
    end
    
    subject { user }

    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }

    context "followed user" do
      subject { other_user }
      its(:followers) { should include(user) }
    end

    context "and unfollowing users" do
      before { user.unfollow!(other_user) }

      it { should_not be_following(other_user) }
      its(:followed_users) { should_not include(other_user) }
    end
  end

  
end