In building my first Ruby on Rails app, I needed to create a facebook / social-networking style friend relationship between users. The simple requirements were that it the friendship should require approval (e.g. a friend request followed by an accept or ignore) and it should be lightweight (not using two records for a single relationship).
This method and code is inspired by two blog posts, which got me started but neither of which really fulfilled my complete requirement. The first used two records per friendship and the second was a twitter-style friend/follower without the approval process.
So, here I break down my Friendship model which hopefully you will find useful and/or insightful as a complete solution or a starting point for your own implementation.
First step is to generate a new model that we’ll call Friendship. A Friendship will connect two users, tracking who made the request, who the friend is, and whether the request has been approved. So we generate it like so:
$ script/generate model Friendship user_id:integer friend_id:integer approved:boolean
This will create the db migration script and the app/models/friendship.rb file which we’re going to edit to include relationships to the User model. When first created, the Friendship model will be mostly empty so we need to add two lines.
class Friendship < ActiveRecord::Base belongs_to :user belongs_to :friend, :class_name => "User", :foreign_key => "friend_id" end
The first line is obviously creating a relationship to the User model, but the second line is a little more complex. It also needs to create a relationship to the User model (there isn’t a Friend model) but we want to refer to this as the friend. So we have to specify both the :class_name and the :foreign_key so that Rails knows how to hook up this :friend field to actual models.
Now that we have a Friendship, we need to configure the User model to allow for these self-referential relationship through the Friendship model. Before we hit the code, let’s walk through the concepts here that define the state of friendships and friends.
A friendship is a two-way relationship which is initiated by one user and approved by the other user. So, we have two states of a friendship (not-approved-yet or approved) and we have two directions of friendship relative to the current user: friendships requested by the current user (we’ll call these “direct” friendships), and friendships requested to the current user (we’ll call these “inverse” friendships).
So using this terminology, we have 4 possible states of Friendship:
1) direct approved
2) indirect approved
3) direct not-approved
4) indirect not-approved
States #1 and #2 are simply what we will be calling “friends” – they are approved relationships, no matter the direction. Because we don’t care who requested the friendship once it’s approved, we will group these together.
State #3 is what we call “pending friends” – other users whom the current user has requested to be friends with and which are awaiting the other user’s approval. These are out of the control of the current user, and just waiting to be approved or rejected.
State #4 is what we’ll call “requested friends” – other users who have requested that the current user be their friend, and are awaiting the current user’s approval. These are the actionable items for the current user to approve. Ignoring a friend request simply deletes the non-approved Friendship, which is the Facebook method (doesn’t tell the other person they were rejected, but allows them to send another request if they want.)
Now that we have that groundwork, here is the User model:
class User < ActiveRecord::Base
[...]
has_many :friendships
has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id"
has_many :direct_friends, :through => :friendships, :conditions => "approved = true", :source => :friend
has_many :inverse_friends, :through => :inverse_friendships, :conditions => "approved = true", :source => :user
has_many :pending_friends, :through => :friendships, :conditions => "approved = false", :foreign_key => "user_id", :source => :user
has_many :requested_friendships, :class_name => "Friendship", :foreign_key => "friend_id", :conditions => "approved = false"
def friends
direct_friends | inverse_friends
end
[...]
end
The first two lines are pretty self-explanatory as the first references our “direct” Friendships and the second our “inverse” Friendships which we’ll be using as the basis of all of our friend lists. Lines 5 and 6 simply create our direct and inverse friend lists, only looking at friendships which have been approved. These are states #1 and #2 from our discussion above and are complete, approved bi-directional friends lists.
Line 8 here creates the list of pending friends (state #3 above) by looking for direct friendships which have not been approved. Then finally in line 9 we handle state #4 which are the “requested friendship” waiting for us to approve them, by taking all the non-approved Friendships where the current user is the target of the request (which is why we specify the foreign_key of “friend_id”).
And finally, because it would be a hassle to deal with two lists of approved friends, we create a method to simply combine them under the name of “friends”, in lines 11-13.
That’s it! You should be able to create and manage facebook-style friend requests now.
I’m not going to cover creation of the Friendship controller here in this post, but I may in the future if people would find it helpful. In short, the controller needs to handle just five actions: index, create, approve, ignore, delete. In my implementation, all actions except for index are used only for ajax calls through link_to_remote, so they’re pretty lightweight.
Updated: the direct_friends line in the User model above was incorrectly using “:source => :user” when it should be “:source => :friend” so that the returned models are the Friends you requested, not yourself the requestor.
- BROWSE / IN TIMELINE
- « Rails Deployment: Engine Yard or Heroku?
- » Running Rails 3 using RVM
- BROWSE / IN ruby on rails
- « Rails Deployment: Engine Yard or Heroku?
- » Running Rails 3 using RVM
COMMENTS / 12 COMMENTS
Call of Duty Black Ops Beta Code Generator [Working as of August 14th] |Crossbow Scopes said on Aug 23 11 at 1:59 pm[...] A User Friend Relationship Model in Rails | Code Iteratively [...]
Chris Kelly said on Aug 31 10 at 9:13 pmI’m getting rolling with Rails at my new job, so it’s fun to see these from your perspective :)
One thought, why not have a single “Friendships” relationship table that also maintains the state? You could then create named scopes to get at the ones you want, i.e:
named_scope :direct_friends, :conditions => {:status => “direct”}
so you could call:
Friend.first.direct_friends.each { |friend| print friend.name }
This makes your User model a lot cleaner and may simplify whatever the heck ActiveRecord is up to behind the scenes.
ed said on Oct 01 10 at 8:34 amHi, Thanks for the tutorial.. learning rails here too and this was the very thing i was trying to do.
Three things:
1. In my migration i default the approved field to false.
2. I had to change :pending_friends to:
has_many :pending_friends, :through => :friendships, :conditions => “approved = false”, :foreign_key => :user_id, :source => :friend
3. I was getting errors in my unit tests with sqlite3 as the db, once I switched to mysql all was well.Cheers,
Ed
David Czarnecki said on Oct 25 10 at 3:17 pmYou might also have a look at Amistad, http://github.com/raw1z/amistad, as a friendships management solution for Rails 3. I’ve found it to be quite useful.
Vlad said on Feb 27 11 at 8:06 amJust for a sake of less db requests:
scope :friends_of, proc {|user|
joins(“inner join (select f.user_id, f.friend_id from friendships f
where ((f.user_id = #{user.id}) or (f.friend_id = #{user.id})) AND f.approved = ‘t’) f
on users.id = f.friend_id or users.id = f.user_id
where users.id != #{user.id}”) }so you can use it like: User.friends_of(User.first)
Zach said on Apr 22 11 at 5:11 pmHello,
I’m currently building a Rails app for language learning and I’d love to see how the controller and views are built. though I could probably figure it out myself I think it would also help others too. There isn’t a lot of good posts like this one out there.
David Stephens said on Aug 22 11 at 9:43 amThis is an interesting approach and something I’ve been looking into recently.
From the reading I’ve done, there are two ways to do this.
One method, is you store one database row per friendship, then work out the “inverse relationships” (as you have done here). This uses one row per friendship, but requires two database queries to work out a friends list.
The other method, is once a friendship is “agreed”, you also write a database row in the other direction. This means you have two database rows per friendship, but only require one query to retrieve a friend list.
Personally, I think given that friend lists will be read more than modified, it makes sense to minimize the database queries and write a “friendship” in each direction between the users, halving your database load.
I’d be interested to hear others’ thoughts to this problem.
Sudhir said on Oct 19 11 at 6:01 amhii i am a newbie , can somebody send me the views and controllers please ..i am stuck in the app ..pls pls help
Michael said on Dec 08 11 at 5:55 amthe friends method with the binary OR blew my mind. Great stuff!
Jack said on Jan 03 12 at 10:50 amGreat post. I’m looking to re-factor an app that has a “Follow” mechanism to require approval, much like this.
I too would be interested to see the controllers! Would you happen to have them in a github repo?
Many thanks. Very helpful!
SPEAK / ADD YOUR COMMENT
Comments are moderated.
