Is it a bad practice to leave the user's database id in the url like this:
localhost:3000/users/16/edit
If it is bad, how can I hide the id in the url? What do I have to watch out when calling the path in my view, routes.rb, etc?
If this is relevant to the discussion, the user resource looks like this in my routes.rb:
resources :users, only: [:new, :edit, :create, :update]
Simply override to_param in ActiveRecord::Base subclass
class User < ActiveRecord::Base
validates_uniqueness_of :name
def to_param #overriden
name
end
end
Then query it like this
user = User.find_by_name('Phusion')
user_path(user) # => "/users/Phusion"
Alternatively you can use gem friendly_id
While you can use friendly ids as described by hawk and RailsCast #314: Pretty URLs with FriendlyId, using the primary key in your routes is standard practice, maybe even best practice. Your primary key ensures the right record is being fetched whenever '/posts/1/edit' is being called. If you use a slug, you have to ensure uniqueness of this very slug yourself!
In your specific case it seems that you are building some kind of "Edit Profile" functionality. If each user is to edit only his or her own profile, you can route a Singular Resource.
Possible routes:
/profile # show profile
/profile/edit # edit profile
Then your user's primary key would not be visible from the URL. In all other models, I'd prefer to go with the id in the URL.
Based on your route, it looks like your users will not have a publicly visible profile. If this is the case then you can simply use example.com/settings for Users#edit and example.com/sign_up for Users#new.
get 'settings', to: 'users#edit'
get 'sign_up', to: 'users#new'
resource :users, path: '', only: [:create, :update]
If your users will indeed have a publicly visible profile in the future, then you can either use the friendly_id gem as suggested by hawk or perhaps a randomized 7 digit ID by overwriting it before creating the record. After a bit of research, this is how I ended up doing it:
before_create :randomize_id
private
def randomize_id
self.id = loop do
random_id = SecureRandom.random_number(10_000_000)
break random_id unless random_id < 1_000_000 or User.where(id: random_id).exists?
end
end
Related
Issue: For when a user isn't signed in, they have no access to their Order Show page. So I have created an order confirmations method in my OrdersController like so:
def order_confirmation
#order = Order.find_by(order_token: params[:order_token])
end
Now, as you see I am currently using a order_token find_by which uses a to_param override.
I want to avoid using the override since it applies controller wide and I haven't figured out a way to not have it used only on the one method only. This messes up my associated models as you can see here: Why is my :order_token being passed as my :order_id when submitting a form file?
How can I make it so my route:
resources :orders do
get 'order_confirmation', :on => :member
end
without the use of the to_param override uses a URL such as :
example.com/orders/:order_token/order_confirmation
?
Update and possible answer:
I will make this the answer rafter some further testing.
When using:
resources :orders, param: :order_token do
get 'order_confirmation', :on => :member
end
In my routes, I am able to go to the URL i want. Although, after an order is created, it still directs me to a route using the :id.
I then change my redirect to:
redirect_to order_confirmation_order_path(#order.order_token)
And it works.
I also removed my to_param override.
In short, you'll want to resources :orders, param: :order_token
and then in your Order model
class Order < ApplicationRecord
def to_param
order_token
end
end
this will have side effects throughout your app.
Rails guides has more info on Overriding Named Route Parameters
When using:
resources :orders, param: :order_token do
get 'order_confirmation', :on => :member
end
In my routes, I am able to go to the URL i want. Although, after an order is created, it still directs me to a route using the :id.
I then change my redirect to:
redirect_to order_confirmation_order_path(#order.order_token)
And it works.
I also removed my to_param override.
get 'users/:id/edit/settings' => 'users#account'
What is the dry way to reference this path in link_to?
As a side note, I use 'users/:id/edit' to edit name/location/age etc and I am using the route above to edit password and email, because I wish to force the user to authenticate their :current_password before editing these more sensitive attributes. I mention this just to make sure my routing logic is correct.
Just run rake routes and you will see all the routes that you have in you app. It should be to the far right
You can use the as: option to setup a named route.
However I would set it up with conventional rails routes:
Rails.application.routes.draw do
resources :users do
resource :settings, only: [:edit, :update], module: :users
end
end
This would create an idiomatically correct RESTful route.
Using the singular resource creates routes without an id parameter. Also you should only use the name :id for the rightmost dynamic segment in a route to avoid violating the principle of least surprise.
rake routes will show you the following routes:
Prefix Verb URI Pattern Controller#Action
edit_user_settings GET /users/:user_id/settings/edit(.:format) users/settings#edit
user_settings PATCH /users/:user_id/settings(.:format) users/settings#update
PUT /users/:user_id/settings(.:format) users/settings#update
...
As a side note, I use 'users/:id/edit' to edit name/location/age etc
and I am using the route above to edit password and email, because I
wish to force the user to authenticate their :current_password before
editing these more sensitive attributes. I mention this just to make
sure my routing logic is correct.
Your route will in no way enforce this authorization concern.
Instead you should do a check in your controller:
# app/models/users/settings_controller.rb
class Users::SettingsController
before_action :set_user
before_action :check_password, except: [:edit]
def edit
# ...
end
def update
# ...
end
private
def set_user
#user = User.find(params[:user_id])
end
def check_password
# this is an example using ActiveModel::SecurePassword
unless #user.authorize(params[:current_password])
#user.errors.add(:current_password, 'must be correct.')
end
end
end
change it to:
get 'users/:id/edit/settings' => 'users#account', as: :edit_user_settings
and then you can just reference it as:
link_to edit_user_settings_path(#user)
rake routes will probably give you a path something like users_path which you can link to using something like
<%= link_to 'Users', users_path(#id) %>
I have the Comment model, which is polymorphic associated to commentable models like Project, User, Update etc. And I have a page where a user can see every User's comment. I want a link near each comment with an address of an object this comment is associated with.
I could write something like that:
link_to 'show on page', Object.const_get(c.commentable_type).find(c.commentable_id)
But this will work only for not nested routes (like User). Here's how my routes look like:
resources :users do
resources :projects, only: [:show, :edit, :update, :destroy]
end
So when I need a link to a Project page, I will get an error, because I need a link like user_project_path.
How can I make Rails to generate a proper link? Somehow I have to find out if this object's route is nested or not and find a parent route for nested ones
You could use a bit of polymophic routing magic.
module CommentsHelper
def path_to_commentable(commentable)
resources = [commentable]
resources.unshift(commentable.parent) if commentable.respond_to?(:parent)
polymorphic_path(resources)
end
def link_to_commentable(commentable)
link_to(
"Show # {commentable.class.model_name.human}",
path_to_commentable(commentable)
)
end
end
class Project < ActiveRecord::Base
# ...
def parent
user
end
end
link_to_commentable(c.commentable)
But it feels dirty. Your model should not be aware of routing concerns.
But a better way to solve this may be to de-nest the routes.
Unless a resource is purely nested and does not make sense outside its parent context it is often better to employ a minimum of nesting and consider that resources may have different representations.
/users/:id/projects may show the projects belonging to a user. While /projects would display all the projects in the app.
Since each project has a unique identifier on its own we can route the individual routes without nesting:
GET /projects/:id - projects#show
PATCH /projects/:id - projects#update
DELETE /projects/:id - projects#destroy
This lets us use polymorphic routing without any knowledge of the "parent" resource and ofter leads to better API design.
Consider this example:
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :projects
resources :users do
# will route to User::ProjectsController#index
resources :projects, module: 'user', only: [:index]
end
end
class ProjectsController < ApplicationController
def index
#projects = Project.all
end
# show, edit, etc
end
class User::ProjectsController < ApplicationController
def index
#user = User.joins(:projects).find(params[:user_id])
#projects = #user.comments
end
end
This would let us link to any project from a comment by:
link_to 'show on page', c.commentable
And any users projects by:
link_to "#{#user.name}'s projects", polymorphic_path(#user, :projects)
To view a user page on my app you have to enter their id /user/2.
How can I make it so that it uses their username in the params instead of id value for user show page? I would like it to be /user/username or /username.
Any help would be appreciated. Thanks!
Routes:
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
get 'logout' => 'sessions#destroy'
get 'edit' => 'users#edit'
get "/profile/:id" => "users#show"
get "profile/:id/settings" => 'users#edit'
get 'settings/:id' => 'users#settings'
resources :users do
resources :messages do
post :new
collection do
get :askout
end
end
collection do
get :trashbin
post :empty_trash
end
end
Users controller:
def show
#user = User.find(params[:id])
end
In my experience the easiest way I've found to do this is to use the friendly_id gem. There is a to_param method in ActiveRecord that you can set to define what a model's route id is going to be, but if the attribute you want to use is not already URL friendly it will become very complicated. Assuming your usernames contain no spaces or other URL "unfriendly" characters, you could do something like this:
class User < ActiveRecord::Base
...
def to_param
username
end
...
end
But make sure in your controller you then find users by their username.
class UsersController < ApplicationController
...
def set_user
#user = User.find_by(username: params[:id])
end
end
Note that if the value in to_param is not EXACTLY what the value in the database is, finding your object again is going to be more difficult. For example, if you wanted to use name.parameterize to set have URLs like /users/john-doe when your actual name attribute is John Doe, you'll have to find a way to consistently "deparameterize" your parameter. Or, you can create a new database column that contains a unique parameterized string of the attribute you want in the url (called a slug). That's why I use friendly_id. It handles your slugs semi-automatically.
As for routing your users to /username, you have several options:
get ':id', to: 'users#show', as: 'show'
resources 'users', path: '/'
Just make sure you put these routes at the end of your routes file. That way if you try to get to your index action on SomeOtherModelsController by going to /some_other_model it's not going to try to find you a user with username "some_other_model".
There are two options:
i. You can use the gem friendly_id
ii. Add to_param method to your user model and return username
class User < ActiveRecord::Base
def to_param
username
end
end
With this option you will have to replace
User.find(params[:id])
with
User.find_by_username(params[:id])
Slugged Routes
What you're asking about is something called "slugged" routes.
These are when you use a slug in your application to determine which objects to load, rather than using a primary_key (usually an id)
To handle this in Rails, you'll need to be able to support the slug in the backend, and the best way to do this is to use the friendly_id gem:
friendly_id
Id highly recommend using the friendly_id gem for this:
#app/models/user.rb
Class User < ActiveRecord::Base
friendly_id :username, use: [:slugged, :finders]
end
The friendly_id gem does 2 things extremely well:
It "upgrades" the ActiveRecord find method to use the slug column, as well as the primary key
It allows you to reference the slugged object directly in your link helpers
It basically means you can do this:
<%= link_to user.name, user %>
If using the friendly_id gem, this will automatically populate with the slug attribute of your table
Further, it allows you to do something else - it gives you the ability to treat the params[:id] option in the backend in exactly the same way as before - providing the functionality you require.
Routes
You should clear up your routes as follows:
#config/routes.rb
get "/profile/:id" => "users#show"
get "profile/:id/settings" => 'users#edit'
get 'settings/:id' => 'users#settings'
resources :sessions, only: [:new, :destroy], path_names: { new: "login", destroy: "logout" }
resources :users, path_names: { new: "signup" } do
resources :messages do
post :new
collection do
get :askout
end
end
collection do
get :trashbin
post :empty_trash
end
end
Make a new attribute on your User model. You can call it what you want, but usually it's called "slug".
rails g migration AddSlugToUser slug:string
rake db:migrate
Store in it a "url friendly" version of the username. the parameterize method is good for that.
class User < ActiveRecord::Base
before_save :create_slug
def create_slug
self.slug = self.name.parameterize
end
In the same model create a to_param method which will automatically include slug in links (instead of the user id)
def to_param
slug
end
Finally, where you do User.find replace it with find_by_slug
#user = User.find_by_slug(params[:id])
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)?
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.
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.
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 ;)
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.