restful routes rename :id - ruby-on-rails

I have this routes
resources :posts, :controller => 'frontend/posts' do
resources :photos, :controller => 'frontend/posts'
end
So frontend/posts_controller handles these requests:
/posts/:post_id/photos/:id
/posts/:id
Somtimes the :id means the photo id and in some cases the post id.
I want that post always uses :post_id . Is it possible to rename :id in :post_id without adding custom(match ...) routes?
thanks.

You should just write your finders to handle both cases. It's easier than messing around with parameter names:
#post = Post.find(params[:post_id] || params[:id])
That being said, I agree that it's annoying that the :id parameter changes names depending on the depth of the resource call.

The presence of :post_id is what will differentiate between the two routes. Personally, unless I have a compelling reason to depart from convention, I try to avoid it.

I'm not sure if this helps or just goes further down the rabbit hole but can't you force it to be like that with:
match "posts/:post_id" => "posts#show"

Related

How to shorten a single resourceful Rails route?

So, I have the following in routes.rb:
scope(path_names: { new: "register" }) do
resources :accounts
end
This works, as it generates a /accounts/register route, but would like to change it to simply say /register. I know I could use match "/register" => "accounts#new", but I am wondering if there is a better way to accomplish this, as this would still leave the /accounts/register route "open". I could probably rename it to something obscure by using {new: "pygmy_puff"}, but I am not confident if it's the right approach.
I'd really like to do this right.
Thanks
Try this:
match "/register" => "accounts#new"
# ...
scope(path_names: { new: "register" }) do
resources :accounts, :except => :new
end
In that order.
Personally, I wouldn't sweat the extra route, and use the extra match statement. Looking forward to rails 4, though I think it would be better written with the http method:
get "/register" => "accounts#new"

Rails 3 Finding the right :id in a controller using a specific route

I have my routes arranged so that when visiting the site the :id is displayed before the slug like so
match "/causes/:id/:slug" => "causes#show", :as => :cause, :via => 'get'
But I also have a nested attribute called "post" that belongs to causes like so
match "/causes/:id/:slug/posts" => "causes#posts", :via => 'get', :as => :posts
When I use this, everything works great for the causes, but not for the posts.
If I use
#post = Post.find(params[:id])
in causes or posts controller it always looks for the ID of the causes, and not the :id of the posts. So if the post :id is 9, and the cause :id is 1, and I use
#post = Post.find(params[:id])
it will always look for post[1] and not 9 or whatever the post id really is.
What am I doing wrong? Is there a way to make this work in the routes, or maybe a different way to find the id of a nested object in the controller?
I need the route to be the way I have it set up, :id/:slug...
rake routes information:
cause GET /causes/:id/:slug(.:format) causes#show
edit_cause GET /causes/:id/:slug/edit(.:format) causes#edit
PUT /causes/:id/:slug(.:format) causes#update
posts GET /causes/:id/:slug/posts(.:format) causes#posts
POST /causes/:id/:slug/posts(.:format)
PUT /causes/:id/:slug/posts(.:format) causes#update_post
DELETE /causes/:id/:slug/posts(.:format) causes#destroy_post
causes GET /causes(.:format) causes#index
POST /causes(.:format) causes#create
Any help would be great.
To solve your immediate problem, you'll want to add something like this to routes.rb
# config/routes.rb
match "/causes/:cause_id/:slug/post/:id" => "causes#update_post", :via => 'put', :as => :update_post
And then you can generate the URL in your views like this...
link_to 'Update this post', update_post_path(#cause, #post)
...and access the parameters in your controller as params[:id] (for the post) and params[:cause_id] (for the cause).
More generally, though, the way you are specifying your routes is pretty cumbersome, and I suspect you're making your life harder than it needs to be. If this were me, I would do something like
# config/routes.rb
resources :causes do
resources :posts
end
This would accomplish something pretty close to what you have now, the main difference being that it wouldn't contain slugs. I'm not sure why you need to have both slugs and IDs, maybe you could just identify your causes by their slugs? Stringex is a good gem for generating slugs, and you can set it so that slugs are guaranteed to be unique.
Here is the section of the Rails guide on nested resources
http://guides.rubyonrails.org/routing.html#nested-resources
And here is a Railscast about using slugs with nested resources
http://railscasts.com/episodes/314-pretty-urls-with-friendlyid?view=comments
Hope this helps.
This is because you're using the id of the cause, and if you're doing /causes/:id/posts shouldn't you be doing #posts = #cause.postsanyway?
I would look into the new router syntax for rails 3 if I were you, as there is a nicer way to nest resources http://guides.rubyonrails.org/routing.html
edit:
use the friendly_id gem and nest your resources, to avoid confusion follow REST best practises that resource in question is at the end so
/causes/:slug/posts/:slug

Rails Routing: how to separate a token from surrounding static segments

I'm having one of those bizarre "this used to work and then it stopped working" issues.
In my routes file I have
controller :questions do
match 'q/:topic-questions/:tag' => :search
end
So a URL of format q/java-questions/performance would route to the search action with params[:topic] = java and params[:tag] = performance
This used to work, but now I get a route not found error. If I switch to
match 'q/(:topic)-questions/:tag' => :search
it finds the route again, but I don't want topic to be an optional parameter. I think this implies that it's having trouble separating out :topic-questions into a token and then a static string. If there another way to neatly separate out the token, other then putting it in ()?
Note - the reason why topic cannot be an optional parameter, is that optional parameters are not included in the cache keys when doing action caching.
what about:
controller :questions do
match 'q/:topic-:modifier/:tag' => :search
end
then you would have three parameters
params[:topic]
params[:modifier]
params[:tag]
and you could then ignore the params[:modifier] one.
according to your caching issues, just make the "-questions" part optional:
controller :questions do
match 'q/:topic(-questions)/:tag' => :search
end
this will match q/java-questions/performance and q/java/performance the cache key is always distinct to the topic "java"
Edit:
This is a modification of #sorens post (he did 99% of the work):
controller :questions do
match 'q/:topic-:modifier/:tag' => :search, :defaults => {:modifier => 'questions'}, :as => :question_topic_tag
end
now your helper looks like:
question_topic_tag_path('java', 'performance') gives you q/java-questions/performance
I would agree with Dave Newton about trying to re-factor your URL structure, but you could possibly allow the "-questions" through the route and chop it off from params[:topic] in your controller and use constraints to validate the presence of something before "-questions" in the URL
controller :posts do
match 'q/:topic/:tag' => :index, :topic => /.+-questions/
end
Then in your controller you would need something like
topic = params[:topic].gsub!(/-questions/, "")
This smells a bit ;)
You could give it a regex condition that forces it to non-empty.
match 'q/(:topic)-questions/:tag' => :search, :topic => /[A-Za-z]*/ # Or whatever.
See the Segment Constraints section of the routing docs for details.
match 'q/:topic:fix_it/:tag' => :search, :fix_it => /-questions/

Validate no routing overlap when creating new resources in Ruby on Rails

I've got a RESTful setup for the routes in a Rails app using text permalinks as the ID for resources.
In addition, there are a few special named routes as well which overlap with the named resource e.g.:
# bunch of special URLs for one off views to be exposed, not RESTful
map.connect '/products/specials', :controller => 'products', :action => 'specials'
map.connect '/products/new-in-stock', :controller => 'products', :action => 'new_in_stock'
# the real resource where the products are exposed at
map.resources :products
The Product model is using permalink_fu to generate permalinks based on the name, and ProductsController does a lookup on the permalink field when accessing. That all works fine.
However when creating new Product records in the database, I want to validate that the generated permalink does not overlap with a special URL.
If a user tries to create a product named specials or new-in-stock or even a normal Rails RESTful resource method like new or edit, I want the controller to lookup the routing configuration, set errors on the model object, fail validation for the new record, and not save it.
I could hard code a list of known illegal permalink names, but it seems messy to do it that way. I'd prefer to hook into the routing to do it automatically.
(controller and model names changed to protect the innocent and make it easier to answer, the actual setup is more complicated than this example)
Well, this works, but I'm not sure how pretty it is. Main issue is mixing controller/routing logic into the model. Basically, you can add a custom validation on the model to check it. This is using undocumented routing methods, so I'm not sure how stable it'll be going forward. Anyone got better ideas?
class Product < ActiveRecord::Base
#... other logic and stuff here...
validate :generated_permalink_is_not_reserved
def generated_permalink_is_not_reserved
create_unique_permalink # permalink_fu method to set up permalink
#TODO feels really ugly having controller/routing logic in the model. Maybe extract this out and inject it somehow so the model doesn't depend on routing
unless ActionController::Routing::Routes.recognize_path("/products/#{permalink}", :method => :get) == {:controller => 'products', :id => permalink, :action => 'show'}
errors.add(:name, "is reserved")
end
end
end
You can use a route that would not otherwise exist. This way it won't make any difference if someone chooses a reserved word for a title or not.
map.product_view '/product_view/:permalink', :controller => 'products', :action => 'view'
And in your views:
product_view_path(:permalink => #product.permalink)
It's a better practice to manage URIs explicitly yourself for reasons like this, and to avoid accidentally exposing routes you don't want to.

Rails RESTful resources using to_param for a field that contains separator characters

I want my Rails 2.3.2 app to respond to and generate URLs like so:
/websites/asd.com
/websites/asd.com/dns_records/new
In my config/routes.rb, I have:
map.resources :websites, :has_many => :dns_records
map.resources :dns_records, :belongs_to => :website
I can then access resources such as:
/websites/1
/websites/1/dns_records
By modifying my Website model, I can generate better URLs like so:
class Website < ActiveRecord::Base
def to_param
domain_name
end
...
end
# app/views/websites/index.erb
<% #websites.each do |w| %>
<%= link_to "Show #{w}", website_path(w) %>
<% end %>
# Produces a link to:
/websites/example_without_periods_in_name
However, for domain names that contain '.' characters, Rails gets unhappy. I believe this is because the '.' character is defined in ActionController::Routing::SEPARATORS, which lists special characters to split the URL on. This allows you to do stuff like /websites/1.xml.
SO, is there a clean way to allow '.' characters in RESTful URLs?
I've tried redefining ActionController::Routing::SEPARATORS to not include '.', which is a totally bad way to solve the problem. This messes up generated URLs by appending ".:format" to them.
I also know I can add :requirements => { :id => regexp } to my config/routes.rb to match a domain name that includes '.' (without this, params[:id] is set to the part of the domain name before the first '.'), but this doesn't help in generating URLs/paths RESTfully.
Many thanks :)
Nick
Solved the problem, with a big thanks to http://poocs.net/2007/11/14/special-characters-and-nested-routes (and see http://dev.rubyonrails.org/ticket/6426 for additional reference)
I needed to add :requirements => { :website_id => regexp } for each nested route that was also going to include a domain name with periods in it.
Here's my working route:
map.resources :websites, :requirements => { :id => /[a-zA-Z0-9\-\.]+/ } do |websites|
websites.with_options :requirements => { :website_id => /[a-zA-Z0-9\-\.]+/ } do |websites_requirements|
websites_requirements.resources :dns_records
end
end
<%= link_to 'New DNS Record', new_website_dns_record_path(#website) %>
# Produces the URL
/websites/asd.com/dns_records/new
The call to
websites.with_options
is simply in keeping with DRY so that :requirements don't have to be specified for all nested routes for websites. So I could also have
websites_requirements.resources :accounts
websites_requirements.resources :monthly_bandwidth_records
etc.
This is an interesting issue. I don't think you can get rid of the bad '.:format' appended to the end if you do a basic map.resources. If you want periods in the names, you are not in accordance with the usual rails styles, and I think a custom route might be in order if you absolutely NEED the '.' in the URL.
However, maybe you should consider changing your definition of to_param. What would you think about using the following?
def to_param
domain_name.sub('.','_dot_')
end
I think, if you're using this to manage customer websites, it's a fairly elegant way to generate nice (and SEO friendly) URLs like
/websites/asd_dot_com/dns_records/new
/websites/asd_dot_com/
I had a similar issue some time ago and came to a similar solution. Using /.+/ as the requirement for the param in question worked fine for me.
http://zargony.com/2009/05/05/routing-parameters-with-a-dot

Resources