EDIT (2007-03-07) :: I’ve updated this as a plugin and released it at my new blog (http://blog.dnite.org). I’ll keep this information up here, but everything is a lot cleaner and works a lot better with the new plugin. So go check out my new blog!
EDIT (2007-01-23) :: I’m depreciating this entry/tutorial!! If you have problems getting this working, just be patient, I’m going to be releasing a plugin in the very near future that will work a lot better and be a lot easier to impliment than this! Just give me a few days…I’ve been working on a project of mine and while I learn Rails. One thing that surprised me was that as many tutorials there are on how to create a blog, there are very few about creating a friends system. So I’ll try and go through the system I put together today after many headaches (that I’ll explain later).
We’re going to keep it RESTful here (or as RESTful as we can). I’ve found that the easiest way to approach anything RESTfully is to understand the path first. So let’s start there. These are the path’s we’ll need to accomplish our friends methods.
# List a user's friends (index action, GET method)
/users/1/friends
# Confirm that a user wants said friend. (confirm action, GET method)
/users/1/friends/2;confirm
# Add a friend (add action, POST method)
/users/1/friends/2;add
# Remove a friend (destroy action, DELETE method)
/users/1/friends/2
# Show a friend
/users/1/friends/2 (show action, GET method)
We’ll need a couple of models, a couple of controllers, and a couple of routes. I’m assuming you know how to make a new Rails project, so let’s get started by creating the models. We’ll need a User model and a Friendship model. I’ll keep them simple here.
First, here’s the tables I created.
create_table :users do |t|
t.column :name, :string
t.column :created_at, :datetime
end
create_table :friendships, :id => false do |t|
t.column :user_id, :integer, :null => false
t.column :friend_id, :integer, :null => false
t.column :status, :integer, :null => false, :default => 0
t.column :created_at, :datetime
end
The models look like this.
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :through => :friendships
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => 'User', :foreign_key => 'friend_id'
end
Now, here’s where I was pulling my hair out. Because, in theory, that association looks ok to me. But if I try and do something like…
User.find(1).friends
I get a nice and very strange SQL error.
Mysql::Error: Column 'friend_id' cannot be null: INSERT INTO friendships (`status`, `user_id`, `created_at`, `friend_id`) VALUES(0, 2, '2006-11-25 01:06:46', NULL)
The strange part isn’t the error. It’s that, one, there is no friend_id (and I still have no idea why) and two, it’s putting what SHOULD be the friend_id into the user_id column in the table. I still have yet to figure out why this happened, so I just went ahead and made my own little work around which works just fine and in all technicality, will probably work better down the road when I have more attributes in the friendships model. But if anyone has any ideas on what could be wrong, I’d love to hear about it.
Anyways.. Back on topic. Now we have the relationships, we need to interact with them. Being RESTful says that we need to do something like /users/1/friends/2 with the correct method. So I created a friends controller that looks like this.
class FriendsController < ApplicationController
def confirm
@friend = User.find_by_id(params[:id])
@current_user = current_user
end
def add
if !friends_already?(params[:user_id], params[:id])
add_friend(params[:user_id], params[:id])
end
redirect_to users_path
end
def destroy
if friends_already?(params[:user_id], params[:id])
remove_friend(params[:user_id], params[:id])
end
redirect_to users_path
end
protected
def friends_already?(user_id, friend_id)
user = User.find(user_id)
friend = User.find(friend_id)
return true if user.friends.include?(friend) && friend.friends.include?(user)
false
end
def add_friend(user_id, friend_id)
Friendship.create({:user_id => user_id, :friend_id => friend_id})
Friendship.create({:user_id => friend_id, :friend_id => user_id})
end
def remove_friend(user_id, friend_id)
Friendship.delete_all "user_id = #{user_id} and friend_id = #{friend_id}"
Friendship.delete_all "user_id = #{friend_id} and friend_id = #{user_id}"
end
end
The confirm action (/users/1/friends/2;confirm) shows a dialog w/ a button that lets the user confirm that he wants to be friends with this person. The button posts to /users/1/friends/2;add. From here it’s pretty self explanitory. It looks really nice when you can call /users/1/friends to list all of someone’s friends. Any extra attributes needed can be added in the add_friend action. Now all you need to do is add the new cool restful routes to config/routes.rb
map.resources :users do |user|
user.resources :friends, :member => { :confirm => :get, :add => :post }
end
That should do ya. Sorry this isn’t a full out tutorial, but you should definately have a good idea on how to impliment a friends system pretty quickly now. Make changes. Do what you want. Let me know if I screwed anything up. I’m still learning this stuff, so what I have here could be completely stupid. Let me know what you think.