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])
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.
I have a resource, say Product, which can be accessed by two different user classes: say Customer and Admin. There is no inheritance between these two.
I am using Devise for authentication:
# config/routes.rb
devises_for :customers
deviser_for :admins
I have these two controllers:
# app/controllers/customers/products_controller.rb
class Customers::ProductsController < ApplicationController
and
# app/controllers/admins/products_controller.rb
class Admins::ProductsController < ApplicationController
Now depending on who logs in (Customer or Admin), I want products_path to point to the corresponding controller. And I want to avoid having customers_products_path and admins_products_path, that's messy.
So I have setup my routes as such
# config/routes.rb
devise_scope :admin do
resources :products, module: 'admins'
end
devise_scope :customer do
resources :products, module: 'customers'
end
This doesn't work. When I login as a Customer, products_path still points to Admins::ProductsController#index as it is the first defined.
Any clue? What I want to do might simply be impossible without hacking.
UPDATE
According to the code, it is not doable.
It turns out the best way to achieve that is to use routing constraints as such:
# config/routes.rb
resources :products, module: 'customers', constraints: CustomersConstraint.new
resources :products, module: 'admins', constraints: AdminsConstraint.new
# app/helpers/customers_constraint.rb
class CustomersConstraint
def matches? request
!!request.env["warden"].user(:customer)
end
end
# app/helpers/admins_constraint.rb
class AdminsConstraint
def matches? request
!!request.env["warden"].user(:admin)
end
end
I stored the constraint objects in the helper folder because I don't really know the best place to put them.
Thanks to #crackofdusk for the tip.
For this you will need to override the existing after_sign_in_path_for method of devise. Put this in your app/controllers/application_controller.rb
def after_sign_in_path_for(resource)
if resource.class == Admin
'/your/path/for/admins'
elsif resource.class == Customer
'/your/path/for/customers'
end
end
Note: If you want to implement previous_url that was requested by user then you can use it like this:
session[:previous_url] || 'your/path/for/admins'
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
It seems simple, in my model I have:
class CustomerAccount < ActiveRecord::Base
acts_as_url :name
def to_param
url # or whatever you set :url_attribute to
end
end
And in my controller, I have:
class CustomerAccountsController < ApplicationController
def show # dashboard for account, set as current account
#account = CustomerAccount.find_by_url params[:id]
no_permission_redirect if !#account.has_valid_user?(current_user)
set_current_account(#account)
#latest_contacts = Contact.latest_contacts(current_account)
end
end
What's currently in the routes.rb is:
resources :customer_accounts, :path => :customer_accounts.url do
member do
get 'disabled'
post 'update_billing'
end
end
That gives me the following error when I try to generate data via rake db:seed, or at least I assume the entry in routes is what's doing it.
undefined method `url' for :customer_accounts:Symbol
So what do I need to do to get the route set up? What I'd like is http://0.0.0.0/customeraccountname to map to the view for the customer account page.
UPDATE:
Here is the code that ended up working in routes.rb, which I discovered after looking at the examples in the answer below:
resources :customer_accounts, :path => '/:id' do
root :action => "show"
member do
get 'disabled'
post 'update_billing'
end
end
If you want to set it up so you have a route like you show, do this:
get '/:id', :to => "customer_accounts#show"
If you want the disabled and update_billing actions underneath this:
get '/:id/disabled', :to => "customer_accounts#disabled"
post '/:id/update_billing', :to => "customer_accounts#update_billing"
Alternatively (and much neater):
scope '/:id' do
controller "customer_accounts" do
root :action => "show"
get 'disabled'
get 'update_billing'
end
end
I have a controller with the 7 RESTful actions plus an additional 'current' action, which returns the first active foo record:
class FooController < ApplicationController
def current
#user = User.find(params[:user_id])
#foo = #user.foos.where(:active => true).first
#use the Show View
respond_to do |format|
format.html { render :template => '/foos/show' }
end
end
#RESTful actions
...
end
The Foo Model :belongs_to the User Model and the User Model :has_many Foos.
If I structure the routes as such:
resources :users do
resources :foos do
member do
get :current
end
end
end
The resulting route is '/users/:user_id/foos/:id'. I don't want to specify the foo :id, obviously.
I've also tried:
map.current_user_foo '/users/:user_id/current_foo', :controller => 'foos', :action => 'current'
resources :users do
resources :foos
end
The resulting route is more like I would expect: '/users/:user_id/current_foo'.
When I try to use this route, I get an error that reads:
ActiveRecord::RecordNotFound in FoosController#current
Couldn't find Foo without an ID
edit
When I move the current action to the application controller, everything works as expected. The named route must be conflicting with the resource routing.
/edit
What am I missing? Is there a better approach for the routing?
I think you want to define current on the collection, not the member (the member is what is adding the :id).
try this.
resources :users do
resources :foos do
collection do
get :current
end
end
end
Which should give you a route like this:
current_user_foos GET /users/:user_id/foos/current(.:format) {:controller=>"foos", :action=>"current"}
Also map isn't used anymore in the RC, it will give you a deprecation warning.