Managing URL Parameters in Rails - ruby-on-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.

Related

Multilingual routes - Advanced constraints not taken into consideration when paths are generated by url_for

I have a multilingual site in rails 3.2 that has some language-specific routes that map to the same action. Like:
For mydomain.fr
match "/bonjour_monde" => 'foo#bar'
For mydomain.de
match "/hallo_welt" => 'foo#bar'
To solve this I have used an advanced constraint when declaring the routes:
Application.routes.draw do
constraints(SiteInitializer.for_country("fr")) do
match "/bonjour_monde" => 'foo#bar'
end
constraints(SiteInitializer.for_country("de")) do
match "/hallo_welt" => 'foo#bar'
end
end
Where the SiteInitializer is just a class which responds to the matches? method and determines if the request is for the correct domain. This is actually only pseudo-code just demonstrating my setup.
class SiteInitializer
def initialize(country_code)
#country_code = country_code
end
def self.for_country(country_code)
new(country_code)
end
def matches?(request)
# based on the request, decide if this route should be declared
decide_which_country_code_from_request(request) == #country_code
end
end
This works fine. When requesting mysite.fr/bonjour_monde the app dispatches correctly and the paths are only bound to their specific domain.
mysite.fr/bonjour_monde => HTTP 200
mysite.fr/hallo_welt => HTTP 404
mysite.de/bonjour_monde => HTTP 404
mysite.de/hallo_welt => HTTP 200
Now, all is fine unless you start using things like url_for(:controller => 'foo', :action => 'bar'). If you do this, the constraints are not taken into consideration. This results in that the generated path from rails (Journey class), will be arbitrary.
If I somewhere in any view use url_for, like
url_for(:controller => 'foo', :action => 'bar')
rails will choose any arbitrary declared route that matches the controller action, probably the first one declared, skipping to check any advanced constraint.
If the user visits mysite.de/hallo_welt, and the view does something like:
= url_for(:controller => 'foo', :action => 'bar', :page => '2')
The output might be
mysite.de/bonjour_monde?page=2
^
wrong language
Actually, I don't use url_for specifically in code, but some gems like kaminari (paginator) do this and that's why you might be limited in using libraries that use the standard helper methods when generating paths.
Now, I'm not so sure if the Journey class should take the request context into consideration. But how would you approach this kind of problem?

Rails routes constraints based on model

Using Rails 3.1. I have the following in my route:
In my model Shop, I have a column called shop_type that has either boutique or saloon. Instead of having the url:
http://localhost/spots/1
I would like to separate it by the shop_type to:
http://localhost/boutique/1
http://localhost/saloon/2
So I added the following to my route:
resources :boutique, :controller => 'shops', :constraints => { :shop_type => 'boutique' }
resources :saloon, :controller => 'shops', :constraints => { :shop_type => 'saloon' }
The problem with this is I can access record ID 1 with shop_type = boutique with either of the URL. Ideally, it should return error when a user tries to access
http://localhost/saloon/1
But the above URL just works fine, which is not what I want.
Also, is there anyway to redirect all shops/1 to the new URL which is by shop_type?
Many thanks.
If you want to do this, then your application is probably telling you that it really wants two separate classes, Boutique and Saloon. Can you do that? If not, why not?
Maybe its better to tell Rails directo allow urls as:
get "/:shop_type/:id", :to => 'shop_controller#show'
And in controller check if the record exist, and return :status => 404 if not:
#shop = Shop.where(:id => params[:id], :shop_type => params[:shop_type]).first
render :status => 404 and return if #shop.nil?
Note that route provided is too greedy and put it after all other routes so it will not 'eat' other request.

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/

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

Rails Routing Conditional with multiple value options

I have a rails route that is based on two conditions, that the domain and subdomain are a specific value. The problem is that there are multiple possible values for subdomain to work, but I can't seem to be able to pass them as an array or hash.
map.with_options(:conditions => {:domain => AppConfig['base_domain'], :subdomain => 'www'..'www3'}) do |signup|
signup.plans '/signup', :controller => 'accounts', :action => 'plans'
...[truncated]...
end
The above example works as accepting www, www1, www2 & www3 as a value for the subdomain. However, that doesn't really solve my needs. I need to be able to accept a value of '' (nothing), 'www' and 'www2' so I tried something to the extend of:
map.with_options(:conditions => {:domain => AppConfig['base_domain'], :subdomain => ['','www','www2']}) do |signup|
That's similar to how you would set it up in ActiveRecord but it doesn't seem to be the same for routes.
Does anybody know now I can specify three values that aren't sequential?
If you can render it as a regular expression, you can use it as a condition. Converting an array to a regular expression is quite easy:
:subdomain => Regexp.new(%w[ www www3 ].collect { |p| Regexp.escape(p) }.join('|'))
Since you're just dealing with a simple pattern anyway, why not express it as this?
:subdomain => /www\d*/
It is important to note that the regular expressions used by routes are not supposed to be anchored using ^ or $ like you usually would. They must match completely to be valid, and partial matches are ignored.

Resources