Rails 3 - How to change the parameter :id in resources - ruby-on-rails

In routes.rb,
resources :projects
gives the following routes
/projects/
/projects/:id
While using nested resources like
resources :projects do
resources :photos
end
it gives the following routes
/projects/
/projects/:id
/projects/:project_id/photos
/projects/:project_id/photos/:id
This gives me the problem because I have to write controller specific before_filter choosing between params[:id] and params[:project_id] for doing Project.find(params[:project_id] || param[:id])
Is there any way to change the routes to have :project_id itself for all routes?
/projects/
**/projects/:project_id**
/projects/:project_id/photos
/projects/:project_id/photos/:id

Another way is define method, that find current project by :project_id, in ApplicationController
def current_project
#current_project ||= Project.find params[:project_id]
end
And redefine this method in ProjectController
def current_project
#current_project ||= Project.find params[:id]
end
Then, you can use current_project in filters for all your controllers

Name of resource's id param can't be changed
But you can redefine it as non-restful path before resourses
get "projects/:project_id" => "projects#show"
# etc. for all other 3 methods

I think what you're looking for is Shallow Nesting: http://edgeguides.rubyonrails.org/routing.html#nested-resources
Look down to 2.7.2 Shallow Nesting

Related

Rails Routing Precedence favors the first

So Ive been working on a rails project that defines two different create actions in the same controller. Here's my controller:
class SmsSendsController < ApplicationController
def new
#at = SmsSend.new
#contact = Contact.find_by(id: params[:id])
end
def create
#at = SmsSend.create(sms_params)
if #at.save!
#con = current_user.contacts.find_by(id: #at.contact_id)
AfricasTalkingGateway.new("trial-error").sendMessage(#con.phonenumber, #at.message)
end
end
def new_all
#at = SmsSend.new
#contact = Contact.find_by(id: params[:id])
end
def create_all
#at = SmsSend.create(sms_params)
if #at.save!
current_user.contacts.each do |c|
AfricasTalkingGateway.new("trial-error").sendMessage(c.phonenumber, #at.message)
end
end
end
private
def sms_params
params.require(:sms_send).permit(:mobile, :message, :contact_id)
end
end
In my
routes.rb
file, Ive used both custom and resourceful routes to define routes for the first and the second new/create actions:
Rails.application.routes.draw do
devise_for :users
get 'sms_sends/new_all', to: 'sms_sends#new_all'
post 'sms_sends', to: 'sms_sends#create_all'
resources :contacts
resources :sms_sends
root 'contacts#index'
end
So both post actions will work if and only if its routes are placed before the other. Is there a way I can get rid of the precedence? Or where am I going wrong?
Thankie.
So both post actions will work if and only if its routes are placed
before the other.
That is how you should define for the routes to work. Because the routes that defined in the routes.rb will be compiled from top-to-bottom. So if your custom routes gets preceded by resourceful routes, then the custom routes will conflict with your resourceful routes.
Is there a way I can get rid of the precedence?
Define them as collection routes like so,
resources :sms_sends do
get 'sms_sends/new_all', to: 'sms_sends#new_all', on: :collection
post 'sms_sends', to: 'sms_sends#create_all', on: :collection
end
The above will generate routes with path helpers like below
sms_sends_new_all_sms_sends GET /sms_sends/sms_sends/new_all(.:format) sms_sends#new_all
sms_sends_sms_sends POST /sms_sends/sms_sends(.:format) sms_sends#create_all
For a better readability, you can change your custom routes like so
resources :sms_sends do
get 'new_all', to: 'sms_sends#new_all', on: :collection
post 'create_all', to: 'sms_sends#create_all', on: :collection
end
This will generate the path helpers like below
new_all_sms_sends GET /sms_sends/new_all(.:format) sms_sends#new_all
create_all_sms_sends POST /sms_sends/create_all(.:format) sms_sends#create_all

What is the path for this route?

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) %>

Rails link_to polymorphic parent, which can have a nested route

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)

Ignore part of the path in Rails 3

I would like to be able to ignore part of the paths in my application.
For example:
example.com/products/toys/big-toy, should be routed by ignoring the 'toys' part (just products/big-toy). I am aware of the wildcard symbol available in the routes, but it ignores everything after the products path. I am not sure how to do this and keep my nested resources working.
Routes:
resources :products do
member do
match :details
end
resources :photos
end
product.rb:
def to_param
"#{category.slug}/#{slug}"
end
One way to solve this would be to use a route constraint.
Try this:
resources :products, constraints: { id: /[^\/]+\/[^\/]+/ } do
member do
match :details, via: :get
end
resources :photos
end
This will capture the product :id as anything with a slash in the middle, so /products/abc/xyz/details will route to products#details with params[:id] equal to abc/xyz.
Then, you could add a before filter in your ProductsController, like this:
class ProductsController < ApplicationController
before_filter :parse_id
// ...
def parse_id
slugs = params[:id].split("/")
params[:category_id] = slugs[0]
params[:id] = slugs[1]
end
end

Rails 3 add GET action to RESTful controller

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.

Resources