Rails Routing with Query String - ruby-on-rails

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

Related

Rails routing URL folders for product categories on show actions

I'm trying to formulate some better urls for a "product" model I have, only on the show action. I'm currently using friend_id to generate pretty slugs, which is fine, but I'm trying to improve the URL flow if I can.
Current my paths work like this
example.com/products/pretty-url-slug
When saving a parictular product (to the Product Model), I also save a type_of attribute. Which could be android, iphone, windows
So I am trying to ultimately have robust URLS like this
example.com/products/iphone/pretty-url-slug
The problem is, that I don't have or believe I want an actual "iphone", "android", etc controller. But I'd rather just update a combination of the routes and show action to handle this properly.
So far I've attempted to solve this by using a catch all on the routes, but is not working correctly. Any suggestions or different ways to handle this elegantly?
routes
resources :products
# at the bottom of my routes a catch all
match '*products' => 'products#show'
# match routes for later time to do something with to act like a
# normal category page.
match 'products/iphone' => 'products#iphone_index'
match 'products/android' => 'products#android_index'
match 'products/windows' => 'products#windows_index'
show action in the products controller
def show
# try to locate the product
if params[:product].present?
slug_to_lookup = params[:product].split("/").last
type_of = params[:product].split("/").second
#product = Product.find_by_slug(slug_to_lookup)
else
#product = Product.find_by_slug(params[:id])
end
# redirect if url is not the slug value
if #product.blank?
redirect_to dashboard_path
elsif request.path != product_path(#product)
redirect_to product_path(#product)
end
end
This way to handle the problem sort of works, but I can't fiqure out how to append the type_of attribute and generate a valid URL.
What about defining your routes like this:
get ':controller/:action/:id/:user_id'
Here, Anything other than :controller or :action will be available to the action as part of params.
Thanks for the suggestion. I was actually able to solve this and pretty simple when I thought it over. This might be helpful for others.
In my routes I just created a route for every type of category I have. so every time a new category, I would need to add an additional route, example:
# match for each product category
match 'shop/iphone/:slug' => 'products#show', :as => :product_iphone
match 'shop/android/:slug' => 'products#show', :as => :product_android
match 'shop/windows/:slug' => 'products#show', :as => :product_windows
Then in the show action for products instead of directing, you would just render the products/show if the slug matches an existing product
#product = Product.find_by_slug(params[:slug])
Then in your views, you could link to a particular category like this
link_to "product", product_android_path(#product)

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 specify nested parameters in the routes

In routes.rb I have described how the search will look like this
match "results//:transaction/:city(.:format)" => "search#index", :as => :seo_search_index
which generates me this kind of routes
seo_search_index /results/:transaction/:city(.:format) {:action=>"index", :controller=>"search"}
And the params object is filled with
params[:transaction]
params[:city]
params[:zip5]
But I want the param object to be filled like this
params[:search][:transaction]
params[:search][:city]
params[:search][:zip5]
Is there a way to specify this like this
Just an example:
match "results//:search[transaction]/:search[city](.:format)" => "search#index", :as => :seo_search_index
I'm not sure there's a way to tell the Rails routing system that you want your parameters nested. You could work around this issue with a before filter in your controller:
class MyController < ApplicationController
before_filter do
params[:search] = params.slice(:transaction, :city, :zip5)
end
end
Update
To answer your real question, you could do either:
seo_search_index_url(#search)
or
seo_search_index_url(#search.slice(:transaction, :city, :zip5))
depending on whether the #search hash contains only the keys you want or some additional ones.
With the help routing filter you can make what ever you want with the urls https://github.com/svenfuchs/routing-filter

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.

Param name and value (independantly) as part of Rails Route

DocumentsController#common_query can handle multiple different request styles.
i.e. all docs in batch 4 or all docs tagged "happy"
I want a single route to make em pretty, so:
/documents/common_query?batch=4
/documents/common_query?tag=happy
become:
/documents/batch/4
/documents/tag/happy
So the end result is that #common_query is called but part of the url was used as the param name and part as it's value.
The second option, with two routes, is almost certainly the better way to go, because it will only match the kinds of URLs that you want to support, while the first option will also "match" URLs like /documents/foo/bar, which will likely cause your #common_query method to, at best, return a RecordNotFound (404) response. At worst, if you're not ready to not see any of your expected params, you'll get a 500 error instead...
Of course, if you start having a lot of variations, you end up with a lot of routes. And if you need to use them in combination, e.g., /documents/batch/4/tag/happy, then you'll need to use a wildcard route, and do the parameter processing in your controller. This might look something like:
map.connect 'documents/*specs', :controller => "documents_controller", :action => "common_query"
The various elements of the URL will be available your controller as params[:specs]. You might turn that into a find like so:
#items = Item.find(:all, :conditions => Hash[params[:specs]])
That Hash[] technique converts the one dimensional array of options into a key-value hash, which might be useful even if you're not feeding it directly to a find().
As a single route:
ActionController::Routing::Routes.draw do |map|
map.connect "documents/:type/:id", :controller => "documents_controller",
:action => "common_query"
end
Then params[:type] will either be "batch" or "tag", and params[:id] either "4" or "happy". You will have to make sure that other actions for the DocumentsController come before this in the routes because this will match any url that looks like "documents/*/*".
But why does it have to be a single route? You could use two routes like this:
map.with_options(:controller => "documents_controller",
:action => "common_query") do |c|
c.connect "documents/batch/:page", :type => "batch"
c.connect "documents/tag/:tag", :type => "tag"
end
which will have the same effect, but is more specific, so you wouldn't have to worry about the priority order of the routes.

Resources