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