Get resource name from URL when using a custom controller in Rails - ruby-on-rails

I have a set of routes that are generated dynamically at runtime, but that all point to the same controller i.e.
map.resources :authors, :controller => 'main'
map.resources :books, :controller => 'main'
These all work fine, producing routes like /authors/1, /books, /books/55, etc and then all end up being processed by the 'main' controller.
However, I can't seem to find how to get the name of the resource in the controller i.e. in the index action when the URL is /authors or /books I'd like to be able to determine which resource it is, i.e. Author or Book
I cannot use separate controllers for this.
Is this at all possible ?

EDIT: complete change of answer because it was waaay off.
So because it changes the params that you see in your action you'll have to get at the actual uri. It is really just as simple as what Terry suggested.
def index
if request.request_uri =~ /books/
#...
else
# if it is a author
end
end
This compares the request uri (the part that would be after localhost:3000) to books and so you can see what the user has requested.

I don't think there's anything like a .resource method, but you could look at request.request_uri, which in your case would return things like /authors or /books, and could act accordingly.

See the "Defaults routes and default parameters" section of the ActionController::Routing documentation. You can program into your routes arbitrary extra parameters you would like sent to your controller.
Looking at the request URI will force you to keep routes and controllers in sync, which will make your code more fragile and less easily re-used. Avoid if you possibly can.

Related

Rails 5 routing: Dynamic controller

Is it possible for the route below to dynamically select different controllers or at least for a single controller to dynamically call another controller?
get '*path' => 'routing#show
For example:
/name-of-a-person => persons#show
/name-of-a-place => places#show
I recall reading something about Rails 5 that would enable this but I can't find it again to save my life. It's possible I imagined it.
Another options is to have a RoutingController that depending on which path is received will call different controllers.
The use case is I have URLs in the database with a type, and the controller depends on what type is the URL. I'm thinking something like this:
get '*path' do |params|
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
'persons#show'
when 'place'
'places#show'
end
end
I post my second best solution so far; still waiting to see if anyone knows how to do this efficiently within the routes.
class RoutingController < ApplicationController
def show
url = Url.find_by!(path: params[:path])
url.controller_class.dispatch('show', request, response)
end
end
Hat tip to André for the idea.
You could define one controller and inside its action make something like this:
def generic_show
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
controller = PersonController.new
controller.request = request
controller.response = response
controller.show
when 'place'
...
end
end
However, I would recommend you to move the code you want to reuse to other classes and use them in both controllers. It should be easier to understand and maintain.
I think you may be able to do it using advanced routing constraints.
From: http://guides.rubyonrails.org/routing.html#advanced-constraints
If you have a more advanced constraint, you can provide an object that responds to matches? that Rails should use. Let's say you wanted to route all users on a blacklist to the BlacklistController. You could do:
class BlacklistConstraint
def initialize
#ips = Blacklist.retrieve_ips
end
def matches?(request)
#ips.include?(request.remote_ip)
end
end
Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
I don't think the Rails guide example is particularly good, because this problem could essentially be solved in your application controllers before_action.
In this example, the constraint is used for IP filtering, but you could also implement matches? to check if it's a person. I would imagine something like
def matches?(request)
Person.where(slug: request.params[:path]).any?
end
And as such, the Rails router can decide whether or not to dispatch the request to the persons#show action.

change routing name dynamically

I have to modify the routes file in order to have SEO improvement.
This is my context, a rails backend generate a JSON feed with the route's name in, I have to read it and change the default name.
For example, I have this:
get '/people' => 'people#show', as: :people
and I'd like to change /people in some value read from my JSON feed.
I created a class to get the JSON object in my app
class JSONDatabase
def initialize(kind_of_site)
#kind_of_site = kind_of_site
end
def fetch_database_remote(url)
JSON.parse(open(url).read)
end
end
but how can i access it in routes file?
Thank you
You don't necessarily need to modify your application's routes. What you can do is define a wild card route that leads to a unique controller where you read the updated route. This approach is kind of hackish but gives you the unlimited routes you need without modifying the routes.
Your config/routes.rb file would look something like this:
resources :defined_models
root to: 'controller#action'
# At last we define the wildcard route
get '/:route' => 'routing_controller#routing_action'
Then, at this routing action we can do the job of seeing if this route (now defined in the params[:route] variable) corresponds to the modified one. Just remember to redirect to a 404 if the route given is not defined, since with this approach you loose the Rails default way of dealing with undefined routes.

Add new view to a controller in Rails

I have a controller, clients_controller, with corresponding index, show, edit, delete, new & form views. Is there a way to create a new view like clients/prospects.html.erb that acts the same way as clients/index.html.erb, except is routed at clients/prospects/?
I've tried this:
match '/clients/prospects' => 'clients#prospects'
And some other things in routes.rb, but of course get the error "Couldn't find Client with id=prospects".
The goal here is basically to have a prospects view and a clients view, and by simply switching the hidden field to a 1, it (in the user's mind) turns a prospect into a client (it's a CRM-like app).
There's a couple of things you need to do. First you need to put the your custom route before any generic route. Otherwise Rails assumes the word "prospects" is an id for the show action. Example:
get '/clients/prospects' => 'clients#prospects' # or match for older Rails versions
resources :clients
Also you need to copy / paste the index method in your ClientsController and name it prospects. Example:
class ClientsController < ApplicationController
def index
#clients = Client.where(prospect: false)
end
def prospects
#prospects = Client.where(prospect: true)
end
end
Lastly, you need to copy the index.html.erb view and name the copy prospects.html.erb. In the example above you would have to work with the #prospects instance variable.
Create a new action in clients controller named prospects. And then define a collection route in routes.rb for it as either resource full way. Or u directly use match as you were doing.
What you're doing is not wrong (although I'd change match to get, otherwise POST and DELETE requests to that url will also render your prospects view). Presumably you have
resources :clients
in your routes file? If so, what you have will probably work if you just move the line you quoted above the resources declaration -- the problem is that /clients/prospects matches the show route for the clients resource, so if it's defined first then that's the route that gets matched.
However, there's a more idiomatic way to define this route
resources :clients do
collection do
get :prospects
end
end
See Rails Routing documentation for more
Also see migu's answer for what else needs to be done once the url is being routed correctly (though there are other things you can do -- if you the two views are similar enough, you can reuse the view template, for example).

Obtaining ID of containing resource via params[:id] for custom actions

I have the following routes in my config/routes.rb file:
resources :employees do
get 'dashboard'
get 'orientation'
end
employees refers to a regular resource handling the standard RESTful actions. dashboard and orientation are what I currently refer to "custom actions" which act on Employee instances. I apologize if I have my terminology mixed up and dashboard and orientation are really something else. These custom actions respond to URLs as follows:
http://myhost/employees/1/dashboard
i.e. They're "member" actions much like show, edit etc.
Anyway, this all works well enough. Regular actions such as show on EmployeesController obtain the ID of the associated Employee through params[:id]. However, with this current structure, dashboard and orientation have to use params[:employee_id] instead. This is not too difficult to deal with, but does lead to some additional code complexity as my regular before_filters which expect params[:id] don't work for these two actions.
How do I have the routing system populate params[:id] with the ID for these custom actions in the same way as show etc.? I've tried various approaches with member instead of get for these actions but haven't got anything to work the way I would like yet. This app is built using Ruby on Rails 3.2.
This might help you:
resources :employees do
member do
get 'dashboard'
get 'orientation'
end
end
and the above will generate routes like below, and then you will be able to use params[:id] in your EmployeesController.
dashboard_employee GET /employees/:id/dashboard(.:format) employees#dashboard
orientation_employee GET /employees/:id/orientation(.:format) employees#orientation
I haven't tested this example, but you can set the resourceful paths explicitly.
Something like this might work:
resources :employees, path: '/employees/:id' do
get 'dashboard', path: '/dashboard'
get 'orientation', path: '/orientation'
end

Trying to make urls of the form <base_url>/boards/<name> in ruby on rails and getting errors

I am using ruby on rails to make a simple social networking site that includes different message boards for each committee of a student group. I want the url structure for each board to look like https://<base_url>/boards/<committee_name> and this will bring the user to the message board for that committee.
My routes.rb file looks like:
resources :committees, only: [:index]
match '/boards/:name', to: 'committees#index(name)'
My index function of committees_controller.rb file looks like:
def index(name)
#posts = Committee.where(name: name)
end
And then I'll use the #posts variable on the page to display all of the posts, but right now when I navigate to https://<base_url>/boards/<committee_name> I get an Unknown Action error, and it says The action 'index(name)' could not be found for CommitteesController.
Could someone guide me through what I have done wrong?
Once I get this working, how would I make a view that reflects this url structure?
Set up your routes like this:
resources :committees, only: [:index]
match '/boards/:name', to: 'committees#show'
and the controller like this:
def index
#committees = Committee.all
end
def show
#committee = Committee.find_by_name!(params[:name])
end
You can't really pass arguments to controller actions the way you were trying to with index(name). Instead, you use the params hash that Rails provides you. The :name part of the route declaration tells Rails to put whatever matches there into params[:name].
You also should be using separate actions for the listing of committees and displaying single committees. Going by Rails conventions, these should be the index and show actions, respectively.
When routing, you only specify the method name, not the arguments:
match '/boards/:name', to: 'committees#show'
Generally you will declare something with resources or match but not both. To stay REST-ful, this should be the show method. Index is a collection method, usually not taking any sort of record identifier.
Arguments always come in via the params structure:
def show
#posts = Committee.where(name: params[:name])
end
Controller methods that are exposed via routes do not take arguments. You may construct private methods that do take arguments for other purposes.

Resources