I have been putting tests together to ensure that some CRUD actions are not routable in my application based upon this resource declaration
resources :posts, only: [:index, :show] do
resources :categories
end
My Rspec Controller test
it 'does not route to #new' do
expect(get: '/posts/new').to_not be_routable
end
When I run this test I get the following output
expected {:get=>"/posts/new"} not to be routable, but it routes to {:controller=>"posts", :action=>"show", :id=>"new"}
So after some research I came across this post on SO, having read it an implementing the solution using constraints.
resources :posts, only: [:index, :show], except: :new, constraints: { id: /\d+/ } do
resources :categories
end
Now my tests pass, but I don't understand why and can't leave it without understanding it. So I have two questions
Why was the test failing in the beginning?
How did adding the constraints argument fix it?
According the the post you linked, and the answer you implemented,
The difference between
'/posts/new' # which is the route to the new action of post controller
and
'/posts/:id' #which is the route to the show action of the same controller
is almost nothing, apart from the fact that one is /new and the other is /:id
Now, this difference is almost nothing, given that having a route like /posts/:id means that the only thing really needed here is the /posts/ part. any other thing in front of this will be seen as an id for /posts/:id.
To this effect, /posts/5, /posts/10, /posts/you, /posts/me, and even /posts/new all matches /posts/:id. with :id equaling => 5, 10, you, me, new respectively.
The above is the reason why your test failed because going to /posts/new is routable for the show action, which essentially is /posts/:id
On the other hand, when you added the constraint ( id: /\d+/ ), you are telling the route that id should be strictly digits. This will prevent any character that is not a digit from being interpreted as an id, so, from the example above, ( /posts/5, /posts/10, /posts/you, /posts/me, and even /posts/new ), only posts/5 and /posts/10 will match the show action, while the rest will not.
Hope this is well explanatory ...
1) Your test was failing initially because of the :show route for :posts. It would look something like this:
GET /posts/:id(.:format) posts/#show
This will match any GET request to /posts/xxxx and will assign whatever you put in the second part of the path (xxxx in this case) to the :id element in your params hash.
If you inspect the params[:id] value in your show action after browsing to /posts/new you'll see that params[:id] == 'new'
So why did your test fail?
In your test, you are checking that /posts/new doesn't route anywhere. But without any constraints on what is an acceptable value for :id, it will route to your show action.
2) The constraint you added specifies that the :id parameter must match the regular expression /\d+/ in order to be accepted. This is one or more digits (0-9).
So, GET /posts/123 is accepted, but GET /posts/new is not. Incidentally, this constraint will prohibit you from using friendly ids to make your URLs nicer. eg GET /posts/my-new-post-about-something will not be acceptable due to the constraint specifying that any argument be an integer.
Alright, this is what I think happened.
If you put back your original code:
resources :posts, only: [:index, :show] do
resources :categories
end
and run rake routes, you will see that get posts/new can be matched in term to one of the /posts/:id route with :id being matched to the string "new".
Adding the contraint that id must be numeric does not allow for this match to happen anymore.
Note that if you were using mongoid instead of activerecord for instance, your contraint would not work since ids are string.
Related
I'm working on a Rails project that is giving me some problems. I've got a controller characters_controller.rb that has two methods.
class CharactersController < ApplicationController
before_action :authenticate_player!
def view
#character = Character.find(params[:id])
unless #character.player_id == current_player.id
redirect_to :root
end
end
def new
end
end
I've got routes set up for each of those.
get 'characters/:id', to: 'characters#view'
get 'characters/new', to: 'characters#new'
The first route works fine. I can get go to /characters/1 and I'm shown the appropriate view and the requested information. If I visit /characters/new I'm shown an error that references characters#view.
raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
and
app/controllers/characters_controller.rb:6:in `view'
So /characters/new is trying to get a Character from the database with an id of "new" but that doesn't work well. Any idea what I may be doing wrong?
Order matters in routes.rb, the router will find the first route that matches.
In your case, it would never go to characters#new, because the line above it will always match.
A simple solution would be to swap the two lines.
A better solution might be to use resource routing as documented in the Rails routing guide.
Rails parses routes sequentially and therefore it is considering 'new' as the :id for characters/:id route (which encountered first).
Just swap the order of routes as follow:
get 'characters/new', to: 'characters#new'
get 'characters/:id', to: 'characters#view'
If using this order in your routes.rb, for /character/new request, rails will understand that request is handled by view action with paramas[:id] = 'new'
Let characters/new before the other will resolve your problem:
get 'characters/new', to: 'characters#new'
get 'characters/:id', to: 'characters#view'
Try to use resourceful routes, much cleaner:
resources :characters, only: [:new, :show]
Also I suggest rename def view to def show to follow rails convention
Just use
resources :characters, :path => "characters"
in your routes.rb
I have following route in my Rails project
resources :custom_urls, :path => '', :only => [:show, :new, :create]
My route is working fine for show, new and create action. But when ever I go to other paths e.g
localhost:3000/index
it always goes to show page. Due to this I am getting error because my instance variable in show action is not set. How can I avoid this problem? I want to get 404 when I try to go to other routes. I only want my application to route to show, new and create action.
Thanks in advance.
Upate
Below are my routes
Prefix Verb URI Pattern Controller#Action
root GET / custom_urls#new
custom_urls POST / custom_urls#create
new_custom_url GET /new(.:format) custom_urls#new
custom_url GET /:id(.:format) custom_urls#show
There is nothing wrong with your solution, you are just getting a little bit tricked by Rails behaviour. What is happening is, since you changed the default route path for :custom_urls to "", when you access the following url:
localhost:3000/index
The server thinks of it as
localhost:3000/custom_urls/index
So it first look for a action called "index" on your controller. Since you didn't declare it and specified that this route doesn't really exists in your route.rb, The next logical step, for rails, is to think "index" as an ID for an object of the class "Custom_url", and because of that it triggers the show action causing the error.
Basically this logic will happen for whatever you type like this:
localhost:3000/XXXX
or this, if you change the routes.rb
localhost:3000/custom_urls/XXXX
I dont recommend the following step, but if you really want to redirect the /index to 404 you would need to create the action in routes
resources :custom_urls, :path => '', :only => [:show, :new, :create, :index]
Then inside the CustumUrlsController, create the action "index" to redirect to a 404 error
def index
raise ActionController::RoutingError.new('Not Found')
end
Are you getting ActiveRecord::RecordNotFound exception? If so, it is working as expected. In dev, it shows the webconsole and in production it will display the public/404.html.
Btw, how does the user go to localhost:3000/index page? Does your app has a menu item with /index?
If you observe rake routes carefully, localhost:3000/blah (when blah is not new) is same as telling your app find me custom_url with id blah. In this case the blah is index. It could be any thing and if it is a valid id it will fetch the record. localhost:3000/new takes precedence over localhost:3000/notnew.
This is kind of difficult to communicate but I'll try without pasting all my code. I have Members who have one Mailbox which has many Receipts. In the header layout I have a nav that calls
<%= link_to "Message Center", member_mailbox_path(current_user.member_id) %>
It works on most pages like trails/# , the resource pages for various models
But on other pages, seems like custom route pages, I get this error
No route matches {:action=>"show", :controller=>"mailbox", :member_id=>16}
Running rake routes shows this:
member_mailbox GET /members/:member_id/mailbox/:id(.:format) mailbox#show
Routes are confusing to me, here are my routes for this problem (show message isn't tested yet) ...
resources :members do
resources :mailbox do
resources :receipts do
member do
get :show_message
end
end
end
end
The routes for the pages that are showing the error are similar to this one
match '/my_plays', :to => "trails#my_plays"
match '/my_creations', :to => "trails#my_creations"
So not sure if my routes are right. I wonder if resources :mailbox is correct since I don't have a bunch of resources for that, it's a has_one .... THX
----EDIT--- after changing route per advice:
member_mailbox POST /members/:member_id/mailbox(.:format) mailboxes#create
new_member_mailbox GET /members/:member_id/mailbox/new(.:format) mailboxes#new
edit_member_mailbox GET /members/:member_id/mailbox/edit(.:format) mailboxes#edit
GET /members/:member_id/mailbox(.:format) mailboxes#show
PUT /members/:member_id/mailbox(.:format) mailboxes#update
DELETE /members/:member_id/mailbox(.:format) mailboxes#destroy
You may want to define a mailbox as a singular resource in your routes. Otherwise, Rails will expect you to pass in both the user id and the mailbox id for member_mailbox_path to route to mailbox#show. I believe this is why you're getting a routing error. Since each user has one mailbox, there's no need to make this extra lookup part of the route. So instead of resources :mailbox, you can do resource :mailbox:
resources :members do
resource :mailbox do
resources :receipts do
member do
get :show_message
end
end
end
end
I believe this would generate the following routes:
member_mailbox POST /members/:member_id/mailbox(.:format) mailboxes#create
new_member_mailbox GET /members/:member_id/mailbox/new(.:format) mailboxes#new
edit_member_mailbox GET /members/:member_id/mailbox/edit(.:format) mailboxes#edit
GET /members/:member_id/mailbox(.:format) mailboxes#show
PUT /members/:member_id/mailbox(.:format) mailboxes#update
DELETE /members/:member_id/mailbox(.:format) mailboxes#destroy
Notice that the lack of path names next to GET, PUT, and DELETE doesn't mean they don't exist; they're just repeats of the POST path, but each responds to different HTTP methods.
To render mailboxes#show, you'll need to add a MailboxesController with a show route, which might do a look up for the member:
class MailboxesController < ApplicationController
def show
#member = Member.find(params[:member_id])
# other mailbox code...
end
end
And you'll also create a template at app/views/mailboxes/show.html.erb to render the mailbox show page.
Also, I would recommend against deeply nesting your routes, as in third level :receipts.
I am trying to build a one page application and having trouble with Rails routing. Basically I want everything within admin route to admin controller index but json rails to specific resource. I've tried
namespace :admin do
constraints :format => 'html' do
match '*path' => 'admin#index'
end
constraints :format => 'json' do
resources :user, :items
end
end
In this case path will match greedily and match /admin/users.json
If I move the :format => 'json' block up. It matches /admin/users
Looks like the constraints blocks I specify does not works at all.
Rails version 3.2.6
rake routes
/admin/*path(.:format) admin/admin#index {:format=>"html"}
admin_users GET /admin/users(.:format) admin/users#index {:format=>"json"}
/* other normal resources routes for admin users and admin items */
I have checked and there is no route /admin/users(.format) admin/users#index {:format=>"html"}
so looks like it is exactly what I think it would be. but somehow still does not works
Update:
I have managed to get it workings if it move the json block up
However if the html block is on top. It is still causing me problem. But I think it is good enough for me now. Thanks guys
The original problem is I used request.xhr? in the controller where I should have used respond_to
Update 2
Uhm not acutally working now when I go to /admin/users I got a Not acceptable error. Where I would think that the first rule wont be match and match the second rule.
Does it work if you move the json block up and make the format segment mandatory for each json resource? In Rails 3.2 this happens by setting the format option to true:
namespace :admin do
constraints(format: "json") do
resources :items, format: true
resources :users, format: true
end
constraints(format: "html") do
match "*path" => "admin#index"
end
end
I have a controller SubscriptionsController with only actions new and create. How would I redirect to new if say someone tries to visit GET /subscriptions which would normally trigger the index action?
config/routes.rb
resource :subscriptions, :only => [:new, :create]
Using rails3 you can do it from a route, something like:
match "/subscriptions", :to => redirect("/subscriptions/new")
Edit:
From the comments it was made clear you want to capture more than that, using a wild card you can make it more generic. You may need to combine this form with the previous to deal with the non-slash form (or try the below form without a slash, I havent tried that). Also make sure to put these "catch all" routes below your other ones since routes are matched from top to bottom.
match "/subscriptions/*other", :to => redirect("/subscriptions/new")