Topic: HOWTO: Create User Friendships with Rails

This post is about how i got a "friendship" system happening in my Rails App. It's by no means the most efficient way of doing things. But having said that, it works, and for a newbie like myself it creates a good starting point on which i could REFACTOR and optimise later on. I tried Google searching around about how to do this and checked out some Rails-related books, and the term that kept popping up was "self-referential many-to-many" relationships. That made no sense to me initially, and even when it did, i could not find examples of actual code that i could use to implement this that worked, or that i understood! Thankfully, though with much struggle, i managed to get it working on my app, here's how...

Please note that in my app i have a User Model, and i am using the RESTful Authentication plugin. To start with, let's think about the other Model(s) and specific relationships that you might need for this to work...

What you want is for each User to have many Friends, and each Friend to have and belong to many Users. But, that doesn't quite make sense, because a Friend is actually just another User. Essentially what we are trying to establish is a many-to-many relationship between all Users. That is, a "self-referential" relationship.

We can create this relationship using some simple declarations that i will mention shortly. In doing so we can avoid creating a new Friend Model, which would be pointless given that a Friend is just another User. Instead of a Friend Model, we will create a Friendship Model. This will act as a join Model between Users and "Friends" (the other Users). The friendships table will basically store all the friendships that exist between the Users. It will also store the status of that friendship. In this example the friendship status can be "REQUESTED", "PENDING", or "ACCEPTED". While we're at it, we will add a datetime/timestamp column to the table as well.

Ok, so first up:

generate Model Friendship

Go to your 00x_create_friendships.rb migration file. Edit your migration file in one of the following ways, either will work, the latter is the new Rails 2.0 way...

class CreateFriendships
def self.up
create_table :friendships do |t|
t.column :user_id, :integer
t.column :friend_id, :integer
t.column :status, :string
t.column :created_at, :datetime
end
end

def self.down
drop_table :friendships
end
end


Or the new way...

class CreateFriendships
def self.up
create_table :friendships do |t|
t.integer :user_id, :friend_id
t.string :status
t.timestamps
end
end

def self.down
drop_table :friendships
end
end


Go ahead and run the migration:

rake db:migrate

Add the following lines to your Friendship Model as shown.

Class Friendship
belongs_to :user
belongs_to :friend, :class_name => 'User', :foreign_key =>'friend_id'
end

Here we establish that a Friendship belongs to both a User and a Friend, and we specify that a Friend also belongs to the User class. Note again that there is no Friend Model!

Add the following lines to your User Model as shown.

Class User
has_many :friends, :through => :friendships, :conditions => "status = 'accepted'"
has_many :requested_friends, :through => :friendships, :source => :friend, :conditions => "status = 'requested'", :order => :created_at
has_many :pending_friends, :through => :friendships, :source => :friend, :conditions => "status = 'pending'", :order => :created_at
has_many :friendships, :dependent => :destroy
end

Here we are using our imaginary Friend model, and we tell Rails that this Model exists :through the Friendship join Model, and we are specifiying the :conditions as being for ACCEPTED friendships only. We are also defining the REQUESTED and PENDING friendships, who are also members of the imaginary Friend model, as indicated by the :source.

Let's create the controller that will make the magic happen.

generate Friends Controller

Add a new nested resource, eg:

map.resources :users do |user|
user.resources :friends
end

This will give us access to the '/user/1/friends' url, which we will use later when we edit the Friend's index.rhtml view page.

Now, edit the Friends controller.

class FriendsController
before_filter :login_required, :except => [:index, :show]

def index
@user = User.find(params[:user_id])
end

def show
redirect_to user_path(params[:id])
end

def new
@friendship1 = Friendship.new
@friendship2 = Friendship.new
end

def create
@user = User.find(current_user)
@friend = User.find(params[:friend_id])
params[:friendship1] = {:user_id => @user.id, :friend_id => @friend.id, :status => 'requested'}
params[:friendship2] = {:user_id => @friend.id, :friend_id => @user.id, :status => 'pending'}
@friendship1 = Friendship.create(params[:friendship1])
@friendship2 = Friendship.create(params[:friendship2])
if @friendship1.save && @friendship2.save
redirect_to user_friends_path(current_user)
else
redirect_to user_path(current_user)
end
end

def update
@user = User.find(current_user)
@friend = User.find(params[:id])
params[:friendship1] = {:user_id => @user.id, :friend_id => @friend.id, :status => 'accepted'}
params[:friendship2] = {:user_id => @friend.id, :friend_id => @user.id, :status => 'accepted'}
@friendship1 = Friendship.find_by_user_id_and_friend_id(@user.id, @friend.id)
@friendship2 = Friendship.find_by_user_id_and_friend_id(@friend.id, @user.id)
if @friendship1.update_attributes(params[:friendship1]) && @friendship2.update_attributes(params[:friendship2])
flash[:notice] = 'Friend sucessfully accepted!'
redirect_to user_friends_path(current_user)
else
redirect_to user_path(current_user)
end
end

def destroy
@user = User.find(params[:user_id])
@friend = User.find(params[:id])
@friendship1 = @user.friendships.find_by_friend_id(params[:id]).destroy
@friendship2 = @friend.friendships.find_by_user_id(params[:id]).destroy
redirect_to user_friends_path(:user_id => current_user)
end
end


As you can see, when a friendship request is made, it will be stored 2 times in the database, ie for each pair of Users that the friendship involves. For example, for one User in a friendship pair, the record in the database will be that of a REQUESTED friendship, and for the other User it will be a PENDING friendship. Once the friendship is ACCEPTED, the friendship will have the ACCEPTED status in the record for both Users. Thus when we also update or destroy friendships, these actions must affect 2 records in the database.

In the Friends view directoty create an index.rhtml file and edit as follows.

My Friends:
<% unless @user == current_user %>
You do not have permission to access this page!
<% else %>
Current Friends:
<%= render :partial => 'friends/friends' %>
Requested Friends:
<%= render :partial => 'friends/requested_friends' %>
Pending Friends:
<%= render :partial => 'friends/pending_friends' %>
<% end %>

In the Friends view directory create the following partials.

_friends.rhtml:

<% unless @user.friends.empty? %>
<% @user.friends.each do |u| %>
<%= link_to u.login, user_path(u.id) %>
Since <%= u.created_at.to_s(:long) %>
<% if logged_in? && @user = current_user %>
<%= link_to '[Remove]', user_friend_path(:user_id => current_user, :id => u), :method => :delete, :confirm => 'Are you sure?' %>
<% end %>
<% end %>
<% else %>
None
<% end %>

_requested_friends.rhtml:

<% unless @user.requested_friends.empty? %>
<% @user.requested_friends.each do |u| %>
<%= link_to u.login, user_path(u.id) %>
Since <%= u.created_at.to_s(:long) %>
<% if logged_in? && @user = current_user %>
<%= link_to '[Cancel]', user_friend_path(:user_id => current_user, :id => u), :method => :delete, :confirm => 'Are you sure?' %>
<% end %>
<% end %>
<% else %>
None
<% end %>

_pending_friends.rhtml:

<% unless @user.pending_friends.empty? %>
<% @user.pending_friends.each do |u| %>
<%= link_to u.login, user_path(u.id) %>
Since <%= u.created_at.to_s(:long) %>
<% if logged_in? && @user = current_user %>
<%= link_to '[Accept]', user_friend_path(:user_id => current_user, :id => u), :method => :put, :confirm => 'Accept friend request! Are you sure?' %> |
<%= link_to '[Reject]', user_friend_path(:user_id => current_user, :id => u), :method => :delete, :confirm => 'Reject friend request! Are you sure?' %>
<% end %>
<% end %>
<% else %>
None
<% end %>

The Friends index.rhtml page for each User will list all their accepted, requested, and pending friendships. It will also allow 3 things:

   1. The User will be able to REMOVE (destroy) an already accepted friendship.
   2. The User will be able to CANCEL (destroy) a requested friendship.
   3. The User will be able to ACCEPT (update) or REJECT (destroy) a pending friendship.

Basically we are just using the DESTROY and UPDATE actions in the Friends controller to get all this done.

Your Users Controller, which you have probably already set up yourself, will have a show action with the @user defined as follows:

class UsersController
def show
@user = User.find(params[:id])
end
end

What i then do is add the following code to the Users directory show.rhtml file (which for me is the User's Profile page), and again i will presume that you have some sort of User Profile page setup already in you app:

<% if logged_in? %>
<% unless @user == current_user || current_user.requested_friends.include?(@user) || current_user.friends.include?(@user) || current_user.pending_friends.include?(@user) %>
<% form_for(:friendship, :url => user_friends_path(:user_id => current_user.id, :friend_id => @user.id)) do |f| %>
<%= submit_tag "Add to My Friends" %>
<% end %>
<% end %>
<% end %>

This code also does 3 things:

   1. It will only display the "Add to My Friends" button if you are a logged-in User.
   2. It will only display the "Add to My Friends" button for Users who you are not friends with, and for whom you do not already have a requested or pending friendship with.
   3. It will NOT display the "Add to My Friends" button on your own User profile, as you do not want to be friends with yourself!

That's it!

If you got any tips as to who i could REFACTOR any of the above code to make it look much more simpler and cleaner, please let me know! Right now i have skinny models and fat controllers, i think it probably should be the other way around...Also, if you are interested in helping me make this into a plugin, post a reply.

smile

PJ.

Re: HOWTO: Create User Friendships with Rails

Nice tutorial...

As far as re-factoring is concerned you really only need one one friendship object in your actions, since you already have current_user. I would do something like this

change the form to pass only the friend_id, you already have the current_user id, no need to pass that again


def create
@request_friendship = current_user.friendship.new(:friend_id => params[:id], :status => "requested")
@request_friendship.save
end

I honestly wouldn't create both at the sametime then in update, is where i'd change the status of the @requested_friendship I created, and then create the other friendship. But that would mean you would have to change pending to something where you check that the status is requested, and the current_user is in the friend_id.

But that's just one of the many aail ways you could refactor this. I'm only 8months into ruby and rails, so might not be the best way, but it def does cut the code down.


Thought about it.... I'd prob create it so that it only creates one row per friendships, for maintenance. You really don't need one for each person on a friendship

because basically a "requested" friendship is when the current person is the user_id and the sstatus is requested (or just pending since u'd only need one)

a "pending" friendship is when the current person is the friend_id and the status is "requester" (or pending, u only need one)

So the status is either pending or accepted.... one row, one initiator, one accepter.

Last edited by Omarvelous (2008-04-06 12:56:14)

Re: HOWTO: Create User Friendships with Rails

I was thinking about that as well - having two rows to maintain one friendship isn't an elegant solution.  Wouldn't having a single row create problems in retrieving friendships, though?  Since the user id of the user in question could be in one of two columns, you'd have to check both.  This doesn't seem elegant either.  I'm no expert on this (yet), but I feel that there must be a better solution.

Re: HOWTO: Create User Friendships with Rails

Thanks for the feedback guys, i will see if i can modify the code and make it still work smile.

PJ.

Re: HOWTO: Create User Friendships with Rails

this is not working for me hmm


Error code:

Mysql::Error: #23000Column 'created_at' in order clause is ambiguous: SELECT `users`.id FROM `users`  INNER JOIN friendships ON users.id = friendships.friend_id      WHERE (`users`.`id` = 2) AND ((`friendships`.user_id = 1) AND ((status = 'requested')))  ORDER BY created_at LIMIT 1

Re: HOWTO: Create User Friendships with Rails

Refer this book for the solution:

http://rapidshare.com/files/127777864/F … n.2007.pdf

Re: HOWTO: Create User Friendships with Rails

@Frozzare,

I just encountered this problem.  I'm not sure where it came from.  Best solution seems to be to change the lines in your user.rb model to:

app/models/user.rb

  has_many :friends, :through => :friendships, :conditions => "status = 'accepted'"
  has_many :requested_friends, :through => :friendships, :source => :friend, :conditions => "status = 'requested'", :order => "friendships.created_at"
  has_many :pending_friends, :through => :friendships, :source => :friend, :conditions => "status = 'pending'", :order => "friendships.created_at"
  has_many :friendships, :dependent => :destroy

There are two edits.  Both :order conditions need to be changed from "created_at" to "friendships.created_at"

This seems to work, though I haven't tested it too much yet.  Let me know if it works for you.

Last edited by senihele (2008-07-17 00:50:35)

Re: HOWTO: Create User Friendships with Rails

Hi

It is possible to make a method that allows only part of a view to be shown if they are not a friend? Example hide a users personal detail if they are not their friend.

I have been trying this but to no avail

Regards

Re: HOWTO: Create User Friendships with Rails

Great tutorial.

My only problem is that when i am user 1 trying to be friend with user 2, Where is the accept button on user 2 to click on?


I cant seem to destroy the request either.

Last edited by urdabum (2009-05-16 21:16:42)

-Michael

Re: HOWTO: Create User Friendships with Rails

normboy wrote:

Hi

It is possible to make a method that allows only part of a view to be shown if they are not a friend? Example hide a users personal detail if they are not their friend.

I have been trying this but to no avail

Regards

Rails 3 version:

In user model:

# user.rb
  def is_stranger(user)
    @friendship = Friendship.where(:user_id => user, :friend_id => self.id).first
    @friendship.blank?
  end

In view:

<% unless @friend.is_stranger(current_user) %>
  <p><%= @friend.email %></p>
<% end %>
High-Tech Creative Services http://new.techism.com/

Re: HOWTO: Create User Friendships with Rails

Whoa it is not so easy but it is not so hard.

Re: HOWTO: Create User Friendships with Rails

How can I add a form to enter an email-address and then add the user to the requested friends? I don't want profile-pages in my app.

Regards

Re: HOWTO: Create User Friendships with Rails

How can i find the finnish spitz in this website