Rails Routing: how to separate a token from surrounding static segments - ruby-on-rails

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/

Related

Managing URL Parameters in Rails

Right now I am finding routing and URL constructing within rails to be semi-confusing. I have currently matched the following for tags that are passed in when displaying/filtering data.
match '/posts/standard/' => 'posts#standard'
match '/posts/standard/:tags' => 'posts#standard', :as => :post_tag
match '/posts/standard/:tags' => redirect { |params| "/posts/standard/#{params[:tags].gsub(' ', '+')}" }, :tags => /.+/
However, now I want to add a 'skill' parameter that can only take one state; however, I am very confused by how I want to construct this within my URL. I cannot simply have...
match '/posts/standard/:tags/:skill' => 'posts#standard', as => post_tag, as: => post_skill
So, I am very confused by this at this point, does Rails offer any type of help for constructing URL's?
One way is to just keep your main route
match '/posts/standard/:tags' => 'posts#standard', :as => :post_tag
and handle the additional URL params as params. The url would look like:
/posts/standard/1?skill=something
and it is easy enough to inject the additional params, such as by
link_to post_tag_path(:skill=> 'something')
and your controller would then do
def standard
if params[:skill] == 'something'
...
else
...
end
end
Also, not sure about this, but your first line in your routes 'match '/posts/standard/' => 'posts#standard' may catch all of your routes since there is a match. If this is the case, simply move it to after the first line.

How to match hash (deep nested) params in Rails3 to make a pretty URL?

If I have this route (in routes.rb):
match 'posts', :to => 'posts#index'
It will show and match the following routes:
# Case 1: non nested hash params
posts_path(:search => 'the', :category => 'old-school')
#=> "/posts?search=the&category=old-school"
# Case 2: nested hash params
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts?filter[search]=the&filter[category]=old-school"
If I want to make the category param part of the main URL, I could do this for the Case 1.
match 'posts(/:category)', :to => 'posts#index'
that will show and match the following routes:
# Case 1: non nested hash params
posts_path(:search => 'the', :category => 'old-school')
#=> "/posts/old-school?search=the"
But how could I do the same if the param is nested (Case 2)?
I would expect the next route definition:
match 'posts(/:filter[category])', :to => 'posts#index'
to work this way:
# Case 2: nested hash params
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts/old-school?filter[search]=the"
But it does not work.
I found this same question in two places with no righ answer:
how-to-specify-nested-parameters-in-the-routes
how-to-accept-hash-parameters-in-routes
The Rails Guides don't specify anthing about this.
Should I assume that this can not be done in rails? really?
you could just make two different routes instead
match 'posts', :to => 'posts#index'
match 'posts/:category', :to => 'posts#index'
The next route will not work as you intended it.
match 'posts(:filter[category])', :to => 'posts#index'
The :filter is just a place holder for either the first argument thats passed into the url helper or the value for the key :filter in a has that is passed in. Any expressions in the route string will not be evaluated.
I guess the answer to your question is that you cannot do this in rails. I would suggest to you though that you do this in another way. It is very helpful in rails to follow the convention and make things easier on yourself.
Looks like you are doing three things here. The base post routes
match 'posts', :to => 'posts#index'
A route that has the category nested in it. Most likely to give the user a better url
match 'posts/:category', :to => 'posts#index'
And a search url which can be the same as the first, or to make your action cleaner, a different one
match 'posts/search', :to => 'posts#search'
There is really no reason I can think of to complicate the routes in the way your are suggesting. A search query url doesn't look nice anyways so why bother handling two urls for searches. Just one will do.
You should definitely take a look at running
rake routes
as this will tell you exactly what you have defined in your routes file. You can also set up routing tests to ensure your custom routes are performing correctly.
Your example does not work (as you indicated)
# Case 2: nested hash params
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts/old-school?filter[search]=the"
But what you should be looking for is this
posts_path(:filter => {:search => 'the', :category => 'old-school'})
#=> "/posts?filter[search]=the&filter[category]=old-school"
This is ok to do it this way.
If you want to keep posts/:category just use this for navigation only, not for search.
Hope that helps

restful routes rename :id

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"

Rails Routing with Query String

I have a problem where I need values passed in from a GET request and I don't know how to set up the routing definition.
My Category object has a type(string),a color(string) and many products. I want to create a simple web service that lets the caller get all of a Category's products by passing in the Category's type and color:
http://www.myapp.com/getProducts?catType=toy&color=red
or ?
http://www.myapp.com/categories/getProducts?catType=toy&color=red
How do I define the correct routing for this situation? Are there better ways to do this in a Restful manner... because I know that Rails is Restful, so if there is a way to do it "correctly" then that would be even better.
Thanks
Your first example:
map.getproduct '/getProduct', :controller => 'your_controller', :action => 'your_action'
In controller you will have catType and color in params hash:
params[:catType]
=> 'toy'
params[:color]
=> 'red'
Is there better way? Probably yes, but it depends on your needs. If you will always have catType and color parameters, than you can add route like this:
map.getproduct '/getProduct/:catType/:color', :controller => 'your_controller', :action => 'your_action'
You will have access to those parameters with params hash like in previous example. And your urls will look like this:
www.myapp.com/getProduct/toy/red
If your parameters may change, you can use route globbing:
map.getproduct '/getProduct/*query', :controller => 'your_controller', :action => 'your_action'
Then it will catch all request that has www.my.app.com/getProduct/... at the begining. But you will have more work in controller. You will have access to query with this:
params[:query]
and for www.myapp.com/getProduct/color/red/catType/toy it will give:
params[:query]
=> ['color', 'red', 'catType', 'toy]
So you have to parse it manualy.
One RESTful way to to do this would involve a product resource nested beneath a category resource, like so:
http://www.myapp.com/categories/toy/products?color=red
Your routes.rb would need to contain:
map.resources :categories do |category|
category.resources :products
end
Since my url above using the Category's type attribute for routing, I'm implying that each type is unique, like an id. It'll mean that whenever you're loading a category in the Categories controller (or anywhere else) you'll need to load the category with Category.find_by_type(params[:id]) instead of Category.find(params[:id]). I like routing categories this way whenever possible.
Your ProductsController controller index action would find products using lines like:
#category = Category.find_by_type(params[:category_id])
#products = #category.products.find(:all, :conditions => { :color => params[:color]} )
Remember, your Category model must contain the line:
has_many :products
It's probable a good idea to enforce that in the model with validations:
validates_presence_of :type
validates_uniqueness_of :type
To make routing work you should also overwrite the to_param method in the Category model to return type instead of id:
def to_param
self.type
end

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