Implementing multiple identifiers for routing resources in Rails - ruby-on-rails

Here's the scenario. Suppose each user account in my rails application have an unique user handles on top of their assigned ids, how might I modify the routes (or otherwise) such that
/users/1
/users/johnsmith
Would both show the user who has id 1 and user handle 'johnsmith'?
I'm currently implementing it as follows by having a get_user method in my users controller which I call prior to the controller actions:
def get_user
identifier = params[:id]
if identifier.to_i.to_s == identifier # if numeric id
#user = User.find(identifier)
else # else hande
#user = User.find_by(handle: identifier)
end
end
Is there a more elegant solution to go about doing this in Rails?

Just use friendly_id, it's exactly what you need.
#Gemfile
gem 'friendly_id', '~> 5.1'
$ rails generate friendly_id
$ rails generate scaffold user name:string slug:string:uniq
$ rake db:migrate
#app/models/user.rb
class User < ActiveRecord::Base
extend FriendlyId
friendly_id :handle, use: [:finders, :slugged]
end
$ rails c
$ User.find_each(&:save)
This will replace all the references to :id in both your routes and model lookups, so you'll be able to use:
<%= link_to #user.handle, #user %>
... and it will show the user's handle.

Here is an idea that i might work for your situation. in your routes you could create a route like this: get '/user/:id_or_handle', controller: 'users', action: 'action', as: 'user_page' as: creates a path user_page_pathwhere you can pass in user object or current_user if you store user to session, like this user_page_path(user.id or user.username) then you can manipulate :id_or_handle

Related

Find resource by slug in nested resource in rails

I have following routes
resources :shops do
resources :categories
end
And when I visit this url:
http://localhost:3000/shops/rabin-shop/categories
I first want to find the shop by using slug 'rabin-shop', then I can filter the categories of products that belong to that shop. In my controller I have tried to implement
def find_shop
#shop = Shop.find(params[:slug])
end
But this is not working. I know this is not how to find in nested resource. I am using friendly_id gem . I cannot do something like current_user.shop because I want the page to be accessed even when the user is not logged in.
If you are using the freindly_id gem, this is how you find a record by it's slug :
def find_shop
# params[:shop_id] if nested
#shop = Shop.friendly.find(params[:id])
end

How to have different routes ids on different routes for the same resources?

I have never found a good solution for this problem. I have the following routes structure:
resources :contents
namespace :admin do
resources :contents
end
When I call content_path(content) I want the id to be the slug of the content, while when I call admin_content_path(content) I want the id to be the id of the content. I just want the id not to be related to the model (actually the id is the returning value of the to_param method of the model), but to the route.
I would like to avoid defining helper methods for every route, it's a weak solution in my opinion.
I know I can write admin_content_path(id: content.id) or content_path(id: content.slug), but this is just an hack actually. Also, this is especially annoying in form_for, since I can't write
form_for #content
but I'm forced to use
form_for #content, url: #content.new_record? ? admin_contents_path : admin_contents_path(id: #content.id)
Usually, you would change the route to:
resources :contents, param: :slug
and then you override to_param method to become:
class Content < ApplicationRecord
def to_param
slug
end
end
And finally in your controller, you replace Content.find(params[:id] with Content.find_by(slug: params[:slug]).
That will give you URLs like /contents/foo-bar when you call content_path(content).
In your case, you can additionally create a subclass that overrides the to_param method:
module Admin
class Content < ::Content
def to_param
id && id.to_s # This is the default for ActiveRecord
end
end
end
Since your admin/contents_controller.rb is namespaced under Admin (e.g Admin::ContentsController), it will by default use the Admin::Content class instead of the normal Content class, and thus the object itself and all routes should be as you like them to be, including forms.
I would say that's two different problems : URL generation for your resources on the user front-end side (using slugs) and URL generation for your admin forms.
Obviously in your admin, you will never be able to just write form_for #resource because your admin is namespaced, so the minimum would at least be form_for [:admin, #resource].
Let's say you have to_param on some of your models to return a slug, you may create your own customised helpers on your admin back-office to always return a path namespaced to /admin/ and using the id of the record.
One generic way to do that is adding this kind of code in your Admin root controller.
class Admin::AdminController < ApplicationController
helper_method :admin_resource_path, :edit_admin_resource_path
def admin_resource_path(resource)
if resource.new_record?
polymorphic_path([:admin, ActiveModel::Naming.route_key(resource)])
else
polymorphic_path([:admin, ActiveModel::Naming.singular_route_key(resource)], id: resource.id)
end
end
def edit_admin_resource_path(resource)
polymorphic_path([:edit, :admin, ActiveModel::Naming.singular_route_key(resource)], id: resource.id)
end
end
Then in your form you can use form_for(#user, url: admin_resource_path(#user). It will work on both user creation and user edition.
You will be able to use those helpers also in your controllers to redirect...
Well, I found a nice solution, but only on Rails >= 5.1 (which is in rc1 at the moment), using the brand new direct method:
namespace :admin do
resources :contents
end
# Maps admin content paths in order to use model.id instead of model.to_param
{ admin_content: :show, edit_admin_content: :edit }.each do |direct_name, action|
direct direct_name do |model, options|
options.merge(controller: 'admin/contents', action: action, id: model.id)
end
end

Accessing current_user in Rails route

Please forgive me... I know there are other posts with a similar title but I have not seen my question so...
I am trying to create a url mysite.com/myusername/profile and I was wondering how to create the route for that. At the moment, the url for user#profile is just that, mysite.com/user/profile, but I want to make it something more specific like say each user has a username like JohnnySmith the URL would be mysite.com/JohnnySmith/profile. I was thinking something like
get "/#{current_user.username}", to: "user#profile", as: user_profile
but I know this isn't correct.
I should mention that, too, that it is not possible for just anyone to access mysite.com/JohnnySmith/profile.... the current user would have to be JohnnySmith.
Can someone help? Thanks.
If you want to pass a parameter in a route, it should be
get "/:username/profile", to: "user#profile", as: user_profile
Please take a look at http://guides.rubyonrails.org/routing.html#naming-routes
Then you can use params[:username] in your controller to validate the user like
if current_user.username != params[:username]
# redirect to error page
Or you can use cancancan gem to do this.
You need to use friendly_id with CanCanCan for authorization.
Essentially, what you're trying to do is allow Rails to process usernames through the params. This can be done without friendly_id, but is somewhat hacky.
Using the friendly_id gem will allow you to use the following:
#Gemfile
gem "friendly_id"
$ rails generate friendly_id
$ rails generate scaffold user name:string slug:string:uniq
$ rake db:migrate
#app/models/user.rb
class User < ActiveRecord::Base
extend FriendlyID
friendly_id :username, use: [:finders, :slugged]
end
You'd then be able to use:
#config/routes.rb
resources :users, path: "", only: [] do
get :profile, action: :show, on: :member #-> url.com/:id/profile
end
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
#user = User.find params[:id]
end
end
This will automatically translate params[:id] into the slug attribute for the User model:
<%= link_to "Profile", user_profile_path(current_user) %>
# -> url.com/:current_user_name/profile
--
The next stage to this is authorization.
Using CanCanCan should make it so that only the current_user can view their profile:
#Gemfile
gem "cancancan"
#app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :read, User, id: user.id
end
end
You can then use load_and_authorize_resource in your users controller:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
load_and_authorize_resource
def show
end
end

Rails routes with :name instead of :id url parameters

I have a controller named 'companies' and rather than the urls for each company being denoted with an :id I'd like to have the url use their :name such as: url/company/microsoft instead of url/company/3.
In my controller I assumed I would have
def show
#company = Company.find(params[:name])
end
Since there won't be any other parameter in the url I was hoping rails would understand that :name referenced the :name column in my Company model. I assume the magic here would be in the route but am stuck at this point.
Good answer with Rails 4.0+ :
resources :companies, param: :name
optionally you can use only: or except: list to specify routes
and if you want to construct a URL, you can override ActiveRecord::Base#to_param of a related model:
class Video < ApplicationRecord
def to_param
identifier
end
# or
alias_method :to_param, :identifier
end
video = Video.find_by(identifier: "Roman-Holiday")
edit_videos_path(video) # => "/videos/Roman-Holiday"
params
The bottom line is you're looking at the wrong solution - the params hash keys are rather irrelevant, you need to be able to use the data contained inside them more effectively.
Your routes will be constructed as:
#config/routes.rb
resources :controller #-> domain.com/controller/:id
This means if you request this route: domain.com/controller/your_resource, the params[:id] hash value will be your_resource (doesn't matter if it's called params[:name] or params[:id])
--
friendly_id
The reason you have several answers recommending friendly_id is because this overrides the find method of ActiveRecord, allowing you to use a slug in your query:
#app/models/model.rb
Class Model < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:slugged, :finders]
end
This allows you to do this:
#app/controllers/your_controller.rb
def show
#model = Model.find params[:id] #-> this can be the "name" of your record, or "id"
end
Honestly, I would just overwrite the to_param in the Model. This will allow company_path helpers to work correctly.
Note: I would create a separate slug column for complex name, but that's just me. This is the simple case.
class Company < ActiveRecord::Base
def to_param
name
end
end
Then change my routes param for readability.
# The param option may only be in Rails 4+,
# if so just use params[:id] in the controller
resources :companies, param: :name
Finally in my Controller I need to look it up the right way.
class CompaniesController < ApplicationController
def show
# Rails 4.0+
#company = Company.find_by(name: params[:name])
# Rails < 4.0
#company = Company.find_by_name(params[:name])
end
end
I recommend using the friendly_id for this purpose.
Please be noted that there are differences between friendly_id 4 and 5. In friendly_id 4, you can use like this
#company = Company.find(params[:id])
However, you won't be able to do that in friendly_id 5, you have to use:
#company = Company.friendly.find(params[:id])
In case that you don't want to use the params[:id] but params[:name], you have to override the route in routes.rb. For example
get '/companies/:name', to: "companies#show"
Hope these info would be helpful to you
There's actually no magic to implement this, you have to either build it yourself by correctly implementing to_param at your model (not recommended) or using one of the gems available for this like:
friendly_id
has_permalink
I use friendly_id and it does the job nicely.
Model.find(primary_key)
The default parameter here is primary_key id.
If you want to use other columns, you should use Model.find_by_xxx
so here it could be
def show
#company = Company.find_by_name(params[:name])
end
The :id parameter is whatever comes after the slash when the URL is requested, so a name attribute needs to be extracted from this by checking the :id parameter for non-numerical values with regular expressions and the match? method in the controller. If a non-numerical value is present, the instance can be assigned by the name attribute using the find_by_name() method that rails generated for the model (assuming that the model has an attribute called name)
That's how I figured out how to do it in my app with my Users resource. My users have a username attribute, and all I had to do was modify the UsersController to define my #user variable differently depending on the :id parameter:
private
# allow routing by name in addition to id
def get_user
if params[:id].match?(/\A\d+\Z/)
# if passed a number, use :id
#user = User.find(params[:id])
else
# if passed a name, use :username
#user = User.find_by_username(params[:id])
end
end
This gives me the option to use either id or username when I create a link to a particular user, or type it into the browser's address bar.
Then the only other (optional) thing to do is to change all the links in the views so that they point to the URL with the name instead of the URL with the id.
For example, within the link_to() method call in my navigation bar I changed
... user_path(current_user) ...
to
... user_path(current_user.username) ...
In your example, you might have a company view with the following link:
<%= link_to #company.name, company_path(#company.name) %>
Which, if the current company is Microsoft, would display "Microsoft" and link to "companies/Microsoft", even though the URL "companies/1" would still be valid and display the same thing as "companies/Microsoft"

Rails: how to create a default route for specific object

I have a User class and map.resources :users in my routes.
If I create a link
link_to #user.name, #user
It will somehow automatically create a link to /users/3 where 3 is an ID of the user.
What if I want to create more userfriendly links and identify users not by IDs but by their usernames. So path would look like /users/some_user_name. How do I reassign the default link for #user so I wouldn't need to change all templates?
You can use FriendlyId gem. This is exactly what you want. For example, if you want links look like /users/username:
class User < ActiveRecord::Base
has_friendly_id :username
end
Found it.
In User.rb:
def to_param
username
end

Resources