Setting up Rails routes based on QueryString - ruby-on-rails

I've seen similar questions on this, but not quite what I'm looking for...
Forgetting for a moment the wisdom of doing this, is it possible to do this?...
/object/update/123?o=section # ==> route to SectionController#update
/object/update/456?o=question # ==> route to QuestionController#update
...and if so, how would that be done?

Assuming you're using Rails 3+, you can use an "Advanced Constraint" (read more about them at http://guides.rubyonrails.org/routing.html#advanced-constraints).
Here's how to solve your example:
module SectionConstraint
extend self
def matches?(request)
request.query_parameters["o"] == "section"
end
end
module QuestionConstraint
extend self
def matches?(request)
request.query_parameters["o"] == "question"
end
end
Rails.application.routes.draw do
match "/object/update/:id" => "section#update", :constraints => SectionConstraint
match "/object/update/:id" => "question#update", :constraints => QuestionConstraint
end

More concise than #moonmaster9000's answer for routes.rb only:
match "/object/update/:id" => "section#update",
:constraints => lambda { |request| request.params[:o] == "section" }
match "/object/update/:id" => "question#update",
:constraints => lambda { |request| request.params[:o] == "question" }

Setting aside the question of whether it is wise to do so, the answer to "is this possible" is 'yes':
class QueryControllerApp
def self.call(env)
controller_name = env['QUERY_STRING'].split('=').last
controller = (controller_name.titleize.pluralize + "Controller").constantize
controller.action(:update).call(env)
rescue NameError
raise "#{controller_name} is an invalid parameter"
end
end
MyRailsApp::Application.routes.draw do
put 'posts/update/:id' => QueryControllerApp
end
Basically the route mapper can accept any Rack application as an endpoint. Our simple app parses the query string, builds the controller name and calls the ActionController method action (which is itself a Rack application). Not shown: how to deal with query strings with any format other than 'o=<controller_name>'

Related

Get constraint based url using url_for based on mounted engine

Is there any way that i can make url_for to return the url based on the request.host during action dispatch routing ?
mount Collaborate::Engine => '/apps/collaborate', :constraints => {:host => 'example.com' }
mount Collaborate::Engine => '/apps/worktogether'
Example:
When the user is on example.com host
collaborate_path => /apps/collaborate
When the user is on any other host
collaborate_path => /apps/worktogether
After a lot of research, i realize that RouteSet class has named_routes which does not consider the constraints to return the url.
I've tried overriding #set in action_dispatch/routing/route_set.rb to pickup from rails application but dint work as expected
#search_set = Rails.application.routes.set.routes.select{|x| x.defaults[:host] == options[:host] }[0]
#set = #search_set unless #search_set.blank?
Remove .com in your example
mount Collaborate::Engine => '/apps/collaborate', :constraints => {:host => 'examplesite' }
mount Collaborate::Engine => '/apps/worktogether'
Should just work
If you need a more advanced constraint, make your own constraint:
class CustomConstraint
def initialize
# Things you need for initialization
end
def matches?(request)
# Do your thing here with the request object
# http://guides.rubyonrails.org/action_controller_overview.html#the-request-object
request.host == "example"
end
end
Rails.application.routes.draw do
get 'foo', to: 'bar#baz',
constraints: CustomConstraint.new
end
You can also specify constraints as a lambda:
Rails.application.routes.draw do
get 'foo', to: 'foo#bar',
constraints: lambda { |request| request.remote_ip == '127.0.0.1' }
end
source: http://guides.rubyonrails.org/routing.html#advanced-constraints
As for as my concern if you handle it at middleware level then it would be good. This is what my assumption.
Add this line in config/application.rb
config.middleware.insert_before ActionDispatch::ParamsParser, "SelectiveStack"
Add a middleware in app directory with middleware directory as a Convention
app/middleware/selective_stack.rb
class SelectiveStack
def initialize(app)
#app = app
end
def call(env)
debugger
if env["SERVER_NAME"] == "example.com"
"/apps/collaborate"
else
"/apps/worktogether"
end
end
end
Hope this will solve your issue.!!!
Alright, here's a shot in the dark; maybe you've tried it already or maybe I'm really missing something. On the surface, it really looks like you're just trying to override a path helper method for apps. So why not set up an override in the application_helper.rb? Something like:
module ApplicationHelper
def collaborate_path
if request.domain == "example.com"
"/apps/collaborate"
else
"/apps/worktogether"
end
end
end

Custom parameters in URL for show action

I'm working on implementing a SEO-hiarchy, which means that I need to prepend parameters for a show action.
The use-case is a search site where the URL-structure is:
/cars/(:brand)/ => a list page
/cars/(:brand)/(:model_name)?s=query_params => a search action
/cars/:brand/:model_name/:variant/:id => a car show action
My problem is to make the show action URLs work without having to provide :brand, :model_name and :variant as individual arguments. They are always available from as values on the resource.
What I have:
/cars/19330-Audi-A4-3.0-TDI
What I want
/cars/Audi/A4/3.0-TDI/19330
Previously, this was how the routes.rb looked like:
# Before
resources :cars. only: [:show] do
member do
get 'favourize'
get 'unfavourize'
end
Following was my first attempt:
# First attempt
scope '/cars/:brand/:model_name/:variant' do
match ":id" => 'cars_controller#show'
match ":car_id/favourize" => 'cars_controller#favourize', as: :favourize_car
match ":car_id/unfavourize" => 'cars_controller#unfavourize', as: :unfavourize_car
end
This makes it possible to do:
cars_path(car, brand: car.brand, model_name: car.model_name, variant: car.variant)
But that is obviously not really ideal.
How is it possible to setup the routes (and perhaps the .to_param method?) in a way that doesn't make it a tedious task to change all link_to calls?
Thanks in advance!
-- UPDATE --
With #tharrisson's suggestion, this is what I tried:
# routes.rb
match '/:brand/:model_name/:variant/:id' => 'cars#show', as: :car
# car.rb
def to_param
# Replace all non-alphanumeric chars with - , then merge adjacent dashes into one
"#{brand}/#{model_name}/#{variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-')}/#{id}"
end
The route works fine, e.g. /cars/Audi/A4/3.0-TDI/19930 displays the correct page. Generating the link with to_param, however, doesn't work. Example:
link_to "car link", car_path(#car)
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>#<Car id: 487143, (...)>})
link_to "car link 2", car_path(#car, brand: "Audi")
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>"Audi", :model_name=>#<Car id: 487143, (...)>})
Rails doesn't seem to know how to translate the to_param into a valid link.
I do not see any way to do this with Rails without tweaking either the URL recognition or the URL generation.
With your first attempt, you got the URL recognition working but not the generation. The solution I can see to make the generation working would be to override the car_path helper method.
Another solution could be, like you did in the UPDATE, to override the to_param method of Car. Notice that your problem is not in the to_param method but in the route definition : you need to give :brand,:model_name and :variant parameters when you want to generate the route. To deal with that, you may want to use a Wildcard segment in your route.
Finally you can also use the routing-filter gem which make you able to add logic before and after the url recognition / generation.
For me, it looks like all theses solutions are a bit heavy and not as easy as it should be but I believe this came from your need as you want to add some levels in the URL without strictly following the rails behavior which will give you URL like /brands/audi/models/A3/variants/19930
OK, so here's what I've got. This works in my little test case. Obviously some fixups needed, and I am sure could be more concise and elegant, but my motto is: "make it work, make it pretty, make it fast" :-)
In routes.rb
controller :cars do
match 'cars', :to => "cars#index"
match 'cars/:brand', :to => "cars#list_brand", :as => :brand
match 'cars/:brand/:model', :to => "cars#list_model_name", :as => :model_name
match 'cars/:brand/:model/:variant', :to => "cars#list_variant", :as => :variant
end
In the Car model
def to_param
"#{brand}/#{model_name}/#{variant}"
end
And obviously fragile and non-DRY, in cars_controller.rb
def index
#cars = Car.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #cars }
end
end
def list_brand
#cars = Car.where("brand = ?", params[:brand])
respond_to do |format|
format.html { render :index }
end
end
def list_model_name
#cars = Car.where("brand = ? and model_name = ?", params[:brand], params[:model])
respond_to do |format|
format.html { render :index }
end
end
def list_variant
#cars = Car.where("brand = ? and model_name = ? and variant = ?", params[:brand], params[:model], params[:variant])
respond_to do |format|
format.html { render :index }
end
end
You just need to create two routes, one for recognition, one for generation.
Updated: use the routes in question.
# config/routes.rb
# this one is used for path generation
resources :cars, :only => [:index, :show] do
member do
get 'favourize'
get 'unfavourize'
end
end
# this one is used for path recognition
scope '/cars/:brand/:model_name/:variant' do
match ':id(/:action)' => 'cars#show', :via => :get
end
And customize to_param
# app/models/car.rb
require 'cgi'
class Car < ActiveRecord::Base
def to_param
parts = [brand,
model_name,
variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-'),
id]
parts.collect {|p| p.present? ? CGI.escape(p.to_s) : '-'}.join('/')
end
end
Sample of path helpers:
link_to 'Show', car_path(#car)
link_to 'Edit', edit_car_path(#car)
link_to 'Favourize', favourize_car_path(#car)
link_to 'Unfavourize', unfavourize_car_path(#car)
link_to 'Cars', cars_path
form_for(#car) # if resources :cars is not
# restricted to :index and :show
You want bounded parameters to be passed to url of which some parameters are optional and some of them strictly needs to be present.
Rails guides shows you can have strict as well as optional parameters and also you can give name to particular route in-order to simplify its usage.
Guide on rails routing
bound parameters
Example usage -
In below route,
brand is optional parameter as its surrounded by circular bracket
Also please note there can be optional parameters inside route but they needs to added at last /cars(/:brand)(/:make)(/:model)
match '/cars/(:brand)', :to => 'cars#index', :as => cars
here cars_url will map to index action of cars controller..
again cars_url("Totoya") will route index action of cars controller along-with params[:brand] as Toyota
Show url route can be as below where id is mandatory and others can be optional
match '/cars/:id(/:brand(/:model_name/)(/:variant)', :to => "cars#show", :as => car
In above case, id is mandatory field. Other parameters are optional.
so you can access it like car_url(car.id) or car_url(12, 'toyota') or car_url(12, 'toyota', 'fortuner') or car_url(12, 'toyota', 'fortuner', 'something else)

path not working properly when using capybara

I'm using rails 3.0.5, rspec2 with latest capybara.
Routes setup like:
scope "(:locale)", :locale => /de|fr|it|en/ do
resources :dossiers
end
In application_controller I have this:
def default_url_options(options={})
options[:locale] = "es"
options
end
So in my views I can use
link_to 'test', dossier_path(1)
without any problems.
But when I do the same in capybara's visit it tries to use the 1 for the locale and not for the id. It only works when I use
visit dossier_path(nil, 1)
or
visit dossier_path(:id => 1)
But both are ugly and looks like a dirty hack. So why do I need to use this dirty hack and what do I jave to do, so that I can use the path methods just like in the views (so without the dirty hack of having to add nil or explicitly pass :id => ...)? :)
I ran into a similar issue. You can set the default_url_options in a before block like this in request specs:
before :each do
app.default_url_options = { :locale => :es }
end
Unfortunately the route generation happens outside of Application Controller. And Capybara doesn't do any magic to provide default url options from it to route helpers.
But you can specify default locale inside your routes.rb
scope "(:locale)", :locale => /de|fr|it|en/, :defaults => { :locale => "es" } do
resources :dossiers
end
And now if you don't pass :locale option to a route helper it will default to "es". Actually, it isn't necessary to keep def default_url_options anymore in your controller.
I'm running rails 3.2.6 and I use a technique that I found here https://github.com/rspec/rspec-rails/issues/255 under Phoet's comment. Just put this somewhere in /spec/support and it should cover all your specs
class ActionView::TestCase::TestController
def default_url_options(options={})
{ :locale => I18n.default_locale }
end
end
class ActionDispatch::Routing::RouteSet
def default_url_options(options={})
{ :locale => I18n.default_locale }
end
end
Opposite as shown here under Using Capybara with RSpec the only way I've been able to get it working is writing
visit user_path(:id => myuser.id.to_s)
so for you it should be
visit dossier_path(:id => "1")
Does it work?

Advanced Routing with Rails3

I want to use regular expressions inside my routes. I have an Products controller, but I want a different URL structure to access the products
http://host/this/
http://host/that/
http://host/andthat/
These URLs should call a action in my controller (Products:show_category(:category))
Is something like this possible?
match "(this|that|andthat)" => "products#show_category", :category => $1
the action should look like this
def show_category
puts params[:category] # <-- "this" if http://host/this/ is called
# ...
end
I haven't actually tested it, but try out:
match ':category' => 'products#show_category', :constraints => { :category => /this|that|andthat/ }
I'm not too sure if this answers your question, but you could add a collection to routes.rb:
resources :products do
collection do
get :category1
get :category2
get :category3
end
end
If you then run rake routes, you'll see that you have urls like /products/category1 and products/category2. Category1, 2 and 3 can be defined in your controller as usual:
def category1
#custom code here
end
def category2
#custom code here
end
def category3
#custom code here
end
As I said, I'm not too sure if that's what you're looking to do, but hope that helps a bit!

Pretty (dated) RESTful URLs in Rails

I'd like my website to have URLs looking like this:
example.com/2010/02/my-first-post
I have my Post model with slug field ('my-first-post') and published_on field (from which we will deduct the year and month parts in the url).
I want my Post model to be RESTful, so things like url_for(#post) work like they should, ie: it should generate the aforementioned url.
Is there a way to do this? I know you need to override to_param and have map.resources :posts with :requirements option set, but I cannot get it all to work.
I have it almost done, I'm 90% there. Using resource_hacks plugin I can achieve this:
map.resources :posts, :member_path => '/:year/:month/:slug',
:member_path_requirements => {:year => /[\d]{4}/, :month => /[\d]{2}/, :slug => /[a-z0-9\-]+/}
rake routes
(...)
post GET /:year/:month/:slug(.:format) {:controller=>"posts", :action=>"show"}
and in the view:
<%= link_to 'post', post_path(:slug => #post.slug, :year => '2010', :month => '02') %>
generates proper example.com/2010/02/my-first-post link.
I would like this to work too:
<%= link_to 'post', post_path(#post) %>
But it needs overriding the to_param method in the model. Should be fairly easy, except for the fact, that to_param must return String, not Hash as I'd like it.
class Post < ActiveRecord::Base
def to_param
{:slug => 'my-first-post', :year => '2010', :month => '02'}
end
end
Results in can't convert Hash into String error.
This seems to be ignored:
def to_param
'2010/02/my-first-post'
end
as it results in error: post_url failed to generate from {:action=>"show", :year=>#<Post id: 1, title: (...) (it wrongly assigns #post object to the :year key). I'm kind of clueless at how to hack it.
Pretty URLs for Rails 3.x and Rails 2.x without the need for any external plugin, but with a little hack, unfortunately.
routes.rb
map.resources :posts, :except => [:show]
map.post '/:year/:month/:slug', :controller => :posts, :action => :show, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
application_controller.rb
def default_url_options(options = {})
# resource hack so that url_for(#post) works like it should
if options[:controller] == 'posts' && options[:action] == 'show'
options[:year] = #post.year
options[:month] = #post.month
end
options
end
post.rb
def to_param # optional
slug
end
def year
published_on.year
end
def month
published_on.strftime('%m')
end
view
<%= link_to 'post', #post %>
Note, for Rails 3.x you might want to use this route definition:
resources :posts
match '/:year/:month/:slug', :to => "posts#show", :as => :post, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
Is there any badge for answering your own question? ;)
Btw: the routing_test file is a good place to see what you can do with Rails routing.
Update: Using default_url_options is a dead end. The posted solution works only when there is #post variable defined in the controller. If there is, for example, #posts variable with Array of posts, we are out of luck (becase default_url_options doesn't have access to view variables, like p in #posts.each do |p|.
So this is still an open problem. Somebody help?
It's still a hack, but the following works:
In application_controller.rb:
def url_for(options = {})
if options[:year].class.to_s == 'Post'
post = options[:year]
options[:year] = post.year
options[:month] = post.month
options[:slug] = post.slug
end
super(options)
end
And the following will work (both in Rails 2.3.x and 3.0.0):
url_for(#post)
post_path(#post)
link_to #post.title, #post
etc.
This is the answer from some nice soul for a similar question of mine, url_for of a custom RESTful resource (composite key; not just id).
Ryan Bates talked about it in his screen cast "how to add custom routes, make some parameters optional, and add requirements for other parameters."
http://railscasts.com/episodes/70-custom-routes
This might be helpful. You can define a default_url_options method in your ApplicationController that receives a Hash of options that were passed to the url helper and returns a Hash of additional options that you want to use for those urls.
If a post is given as a parameter to post_path, it will be assigned to the first (unnassigned) parameter of the route. Haven't tested it, but it might work:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:year].is_a?Post
post = options[:year]
{
:year => post.created_at.year,
:month => post.created_at.month,
:slug => post.slug
}
else
{}
end
end
I'm in the similar situation, where a post has a language parameter and slug parameter. Writing post_path(#post) sends this hash to the default_url_options method:
{:language=>#<Post id: 1, ...>, :controller=>"posts", :action=>"show"}
UPDATE: There's a problem that you can't override url parameters from that method. The parameters passed to the url helper take precedence. So you could do something like:
post_path(:slug => #post)
and:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:slug].is_a?Post
{
:year => options[:slug].created_at.year,
:month => options[:slug].created_at.month
}
else
{}
end
end
This would work if Post.to_param returned the slug. You would only need to add the year and month to the hash.
You could just save yourself the stress and use friendly_id. Its awesome, does the job and you could look at a screencast by Ryan Bates to get started.

Resources