root path for multiple controllers on rails routes - ruby-on-rails-3

I have two resource controllers where I am using a slug to represent the ID. (friendly_id gem).
I am able to have the show path for one resource on the route but not for two at the same time. ie.
root :to => 'home#index'
match '/:id' => "properties#show"
match '/:id' => "contents#show"
Basically I want urls like,
# Content
domain.com/about-us
domain.com/terms
# Property
domain.com/unique-property-name
domain.com/another-unique-property-name
Whatever resource I put on top works. Is there a way to do this?
Thanks in advace if you can help.

This is untested, but try utilizing a constraint on your route.
root :to => 'home#index'
match '/:id', :to => "properties#show",
:constraints => lambda { |r| Property.find_by_id(r.params[:id]).present? }
match '/:id', :to => "contests#show",
:constraints => lambda { |r| Contest.find_by_id(r.params[:id]).present? }
Alternatively, you can create a separate class that responds to matches? instead of defining a lambda proc. (I recommend placing these classes into separate files that will autoload within your Rails app.)
# app/constraints/property_constraint.rb
class PropertyConstraint
def self.matches?(request)
property = Property.find_by_id(request.params[:id])
property.present?
end
end
# app/constraints/contest_constraint.rb
class ContestConstraint
def self.matches?(request)
contest = Contest.find_by_id(request.params[:id])
contest.present?
end
end
# config/routes.rb
root :to => 'home#index'
match '/:id', :to => "properties#show", :constraints => PropertyConstraint
match '/:id', :to => "contests#show", :constraints => ContestConstraint
Unfortunately this results in an extra DB query (once in the routes, and once more in your controller). If anyone has a suggestion on minimizing this, please share. :)

This Rails Engine does what you want:
Slug Engine at Github
Basically, the author's approach was to mount a Rails Engine inside his main app. This Engine incorporates both a controller for handling slugs that exist and a piece of middleware for filtering out and abstaining on slugs that don't exist.
He explains why he took this approach and other aborted solutions in a rather detailed and interesting blog post. This blog post and the slug engine source code should be enough detail for you to get your own code up and running, but that open-source engine seems to be exactly what you're looking for if you want a drop-in solution.

You can do it in a middleware
Detect slug in path
If Content with this slug exists - change request path to "contents/:id"
If Property with this slug exists - change request path to "properties/:id"
in your routing set:
match 'contents/:id' => "properties#show"
match 'properties/:id' => "contents#show"

You could write another controller which takes the id from the router and checks if the id belongs to properties or content and renders the appropriate view.
match '/:id' => "router#show"
The controller would do something like this:
def show
#property = Property.find(params[:id])
if #property then
render 'property/show'
else
#content = Content.find(params[:id])
render 'content/show
end
end
Havn't tested this code, but this idea should work.

I would suggest that you do this in a more RESTful way, if possible. Basically, you have two different resources and you should separate them:
match 'properties/:id' => "properties#show"
match 'contents/:id' => "contents#show"
This will give you many advantages down the road. One immediate benefit is that you can avoid clashes between ids for properties and content. (Note that friendly_id will not help you with inter-model slug clashes in your original scheme.)

Related

How do I create routes for a controller that does nothing with models in Rails?

Still pretty new to Rails, so if I'm taking the completely wrong approach, please feel free to straiten me out.
How do I make routes.rb aware that there's a root controller full of actions that don't manipulate models, while preserving the route helper methods? I'd like it to respond to requests like these:
http://localhost/download
http://localhost/share
With route helpers like
download_app_path
share_path
but without static named routes like these:
match '/download' => 'site#download', :as => :download_app
match '/share' => 'site#share', :as => :share
from a SiteController that doesn't create, show, or otherwise manipulates models from my app.
I've tried using an approach like this, but it works without generating the route helpers ( naturally )
match '/:action', :controller => 'site'
I could theoretically do without the route helpers, but I think they're a bit easier to read than passing hashes of url options to link_to or form methods.
Is there a way to accomplish a more resourceful root controller, or is what I'm trying to do unconventional for Rails?
Edit
For clarity, here's what this SiteController class looks like:
class SiteController < ApplicationController
def download
#custom_options = { .. }
end
def share
#custom_options = { .. }
end
def about
end
end
Its purpose is to allow me to collect pages that don't interact with resources ( such as Users or Friendships ) into a single controller and maintain them all in one place. I'm trying to set this controller up as the application root controller - so all paths from this controller will be directly off the app host ( myapp.com/download )
Thanks in advance!
routes and resources are not tied to models. it's just a RESTful convention. if you just want to use the index actions, in your example download and share could be done like
resouce :download, only: [:index]
resouce :share, only: [:index]
see all the examples in the guides http://guides.rubyonrails.org/routing.html
if you want to add the download and share functionality to some "resource" like, say a picture, then you would do something like:
resources :pictures do
get 'download', :on => :member
get 'share', :on => :member
end
a resource always has and endpoint /pictures for example. so if you want to have paths directly to your host, then you need to provide custom matchers in your routes like you did in your examples.

Questions about rails3 routes

I'm upgrading my app to rails 3, and I am a bit confused about some of the routes. The resourceful ones are easy enough, but how can I set a generic rule for all actions in a specific controller. I tried something like this:
get 'custom/:action/' => {:controller => :custom}
But that didn't work. It seems the new format is "controller#action", but how can I specify the action to be variable?
Also, other than using named routes or resources, is it possible to do shorthand notation to name routes in a specific controller?
i.e. rather than:
get '/tasks', :controller => :home, :action => :tasks, :as => 'tasks_home'
get '/accounts', :controller => :home, :action => :accounts, :as => 'accounts_home'
is it possible to do something a little cleaner, like:
controller => :home do
get :tasks
get :accounts
end
And that would automatically created the named routes?
You can use action as a variable like this:
resource :custom do
match ':action'
end
This will generate
/custom/:action(.:format) customs#:action
custom POST /custom(.:format) customs#create
new_custom GET /custom/new(.:format) customs#new
edit_custom GET /custom/edit(.:format) customs#edit
GET /custom(.:format) customs#show
PUT /custom(.:format) customs#update
DELETE /custom(.:format) customs#destroy
So it will handle your action as a variable URL-s and will add some default CRUD actions as well.
Note that the controller name here is in plural. If you would like to use a route for a controller which name is in singular, use resources instead of resource.
The answer to the second question is almost identical to the first one, use resource:
resource :home do
get :tasks
get :accounts
end
generates:
tasks_home GET /home/tasks(.:format) homes#tasks
accounts_home GET /home/accounts(.:format) homes#accounts
home POST /home(.:format) homes#create
new_home GET /home/new(.:format) homes#new
edit_home GET /home/edit(.:format) homes#edit
GET /home(.:format) homes#show
PUT /home(.:format) homes#update
DELETE /home(.:format) homes#destroy
Note that the matched controller names are in plural again, because of the convention.
Looks like this is related to the persisted field being set to false on nested ActiveResource objects: https://github.com/rails/rails/pull/3107

Naming params of nested routes

resources :leagues do
resources :schedule
end
This generates:
leagues/:id
leagues/:league_id/schedule/:id
How can I keep the league ID from changing param names?
So it'll be:
leagues/:id
leagues/:id/schedule/:schedule_id
No, please do not do this.
The reason for it being this way is that it provides a common interface for nested resources across every single application. By making it different in your application, you're effectively going "against the grain" of Rails. Rails has a strict set of conventions that you should stick to. When you stray from this path, things get messy.
However, if you do want to shoot yourself in the foot, metaphorically speaking, you will need to define the routes manually. Here's the routes for the seven standard actions in a controller:
get 'leagues/:id/schedules', :to => "schedules#index", :as => "league_schedules"
get 'leagues/:id/schedule/:schedule_id', :to => "schedules#show", :as => "league_schedule"
get 'leagues/:id/schedules/new', :to => "schedules#new", :as => "new_league_schedule"
post 'leagues/:id/schedules', :to => "schedules#create"
get 'leagues/:id/schedule/:schedule_id/edit', :to => "schedules#edit", :as => "ed it_league_schedule"
put 'leagues/:id/schedule/:schedule_id', :to => "schedules#update"
delete 'leagues/:id/schedule/:schedule_id', :to => "schedules#destroy"
As you can see, it's quite ugly. But, if you really really really want to do it this way, that's how you'd do it.
You can set "param" option on resource route to override the default "id" param:
resources :leagues do
resources :schedule, param: schedule_id
end
refs to the Rails Routing Doc: http://guides.rubyonrails.org/routing.html#overriding-named-route-parameters
It appends the ID to the nested_param which is a bummer because I would like mine to be without the singular name. It looks like they really don't want you to make it only like :id as it could have conflicts. Plus it would be a bit of a diff from the normal restful routing that rails likes to use.
https://github.com/rails/rails/blob/5368f2508651c92fbae40cd679afbafdd7e98e77/actionpack/lib/action_dispatch/routing/mapper.rb#L1207
namespace :account, defaults: { type: 'account' }do
resources :auth, param: :lies_id, only: [] do
get :google
end
end
Rake routes returns the following
$ rake routes | grep /account/auth
account_auth_google GET /account/auth/:auth_lies_id/google(.:format)
So the solution which seams simpler is to just change the controller to use the nested param name it creates.

Rails 3 routes: How to avoid conflict with 'show' action?

I currently have the following routes set up for photos:
resources :photos
match 'photos/:user' => 'photos#user', :as => :user_photo
match 'photos/:user/:key' => 'photos#show', :as => :show_photo
Those two match routes give me URLs like:
http://example.com/photos/joe_schmoe
http://example.com/photos/joe_schmoe/123xyz
...similar to the way Flickr formats its URLs.
The problem I'm having, though, is that the photos/:user route is either interpreted as the show method, or if I put those custom routes before the resources then routes like /new get interpreted as the user method.
How can I get around that without having to do a ton of custom routes?
You'll want to put this custom route above the resources :users, so that it is matched first in the routes.
match 'photos/:user' => 'photos#user', :as => :user_photo
resources :photos
Additionally you can disable the show action altogether:
resources :photos, :except => :show
It's not a good idea to mix restful routes with custom match routes on the same resource. As you observed these two routes will intercept each others actions. Your best choice is to pick only one routing system for a resource and stick with it. If you want flickr style routes you should remove the restful route and add the other necessary match routes for new/create/etc you might need. If you desperately want to keep both of these routes You either need to disable show from the rest route with the rest route being on top, or you disable new from the match route while match being on top. You can use regexp to filter out some requests from match like this:
match 'photos/:user' => 'photos#user', :as => :user_photo, :constraints => { :user => /.*[^n][^e][^w].*/ }
This gets ugly really fast tho and I suggest just not using the rest route at all.

How to do this with routing in Rails?

Say we have a simple resource called news:
resources :news
The generated paths are in this form /news/:id. I would like to provide a shortcut for this by dropping the /news/, so that /1/ goes to news#show with id 1 and the same for all the other resourceful actions of news.
I figured it's probably something along the lines of
match '/:id(/:action)', :controller => 'news'
but this isn't working.
To change the path to a resource use :path =>
resources :news, :path => "/"
Try this at the very bottom of your routes file:
match ':id', :to => "news#show"
Placing a custom route at the bottom of your routes.rb should work, that will give it lowest priority and allow valid routes to work first:
match '/:id', :to => 'news#show'
It's important to note that this will basically route anything that wasn't previously caught, and does not exist as an actual static file, to that controller/action. You will want to make sure you render your 404 error page if the news record does not exist.