views:

2069

answers:

4

My User model has the usual id primary key, but it also has a unique login which can be used as an identifier. Therefore, I would like to define routes so that users can be accessed either by id or by login. Ideally, the routes would be something like this:

/users/:id (GET) => show (:id)
/users/:id (PUT) => update (:id)
...

/users/login/:login (GET) => show (:login)
/users/login/:login (PUT) => update (:login)
...

What is the best way to do this (or something similar)?

A: 

Not exactly sure what you are doing here but this may be of some help.

You can define actions that are outside of the automatic RESTful routes that rails provides by adding a :member or :collection option.

map.resources :users, :member => { :login => [:post, :get] }

This will generate routes that look like this:

/users/:id (GET)
...
/users/:id/login (GET)
/users/:id/login (POST)

Another thing you could do just use the login as the attribute that you look up (assuming that it is unique). Check out Ryan Bates screencast on it. In your controller you would have:

def show
@user = User.find_by_login(params[:id])
...
end

He also has another screencast that may help you. The second one talks about custom named routes.

vrish88
Hi vrish88, thanks for your answer. I'm sorry my question was not clear enough. I just edited it to make it clearer (I hope). I found a possible solution in the mean time (see below). It's not great so I hope someone knows a better way.
MiniQuark
+1  A: 

So far, the best I could come up with is this:

map.resources :users

map.resources :users_by_login,
     :controller => "User",
     :only => [:show, :edit, :update, :destroy],
     :requirements => {:by_login => true}

The usual RESTful routes are created for users, and on top of that, the users_by_login resource adds the following routes (and only those):

GET    /users_by_login/:id/edit
GET    /users_by_login/:id/edit.:format
GET    /users_by_login/:id
GET    /users_by_login/:id.:format
PUT    /users_by_login/:id
PUT    /users_by_login/:id.:format
DELETE /users_by_login/:id
DELETE /users_by_login/:id.:format

These routes are actually mapped to the UserController as well (for the show/edit/update/destroy methods only). An extra by_login parameter is added (equal to true): this way, the UserController methods can tell whether the id parameter represents a login or an id.

It does the job, but I wish there was a better way.

MiniQuark
+1  A: 

Just check to see if the ID passed to the controller methods is an integer.

if params[:id].is_a?(Integer)
    @user = User.find params[:id]
else
    @user = User.find_by_login params[:id]

No need to add special routes.

Kyle Boon
I thought about that, but unfortunately we do have users whose login is an integer!
MiniQuark
+1  A: 

Actually Kyle Boon has the correct idea here. But it is slightly off. When the params variable comes in all the values are stored as strings so his example would return false every time. What you can do is this:

if params[:id].to_i.zero?
@user = User.find_by_login params[:id]
else
@user = User.find params[:id]
end

This way if the :id is an actual string Ruby just converts it to 0. You can test this out by looking at the params hash using the ruby-debug gem.

(I would have just commented but I don't have enough experience to do that yet ;)

vrish88
Well unfortunately we have some users whose login is an integer. Maybe I can prefix it in some way or another. I'll check.
MiniQuark
What about: @user = User.find(:first, :conditions => ['login = ? OR id = ?', params[:id], params[:id]])
vrish88