16

I have a nested resource that belongs to many different models. For instance:

resources :users do
  resources :histories, only: [:show]
end

resources :publications do
  resources :histories, only: [:show]
end

resources :events do
  resources :histories, only: [:show]
end

In the HistoriesController, I want to find the parent object, though I'm having trouble thinking of a dry way to handle this. At the moment, the best I can come up with is:

if params[:user_id].present?
  @parent = User.find(params[:user_id])
elsif params[:publication_id].present?
  @parent = Publication.find(params[:publication_id])
elsif . . . .

I've got literally dozens of models I have to branch through in this way, which seems sloppy. Is there a better (perhaps baked-in) approach that I'm not considering?

nullnullnull
  • 8,039
  • 12
  • 55
  • 107

3 Answers3

16

The way I am doing this is adding the parent model class name as a default param in the route.

For the question example this should be something like:

resources :users, model_name: 'User' do
  resources :histories, only: [:show]
end

resources :publications, model_name: 'Publication' do
  resources :histories, only: [:show]
end

resources :events, model_name: 'Event' do
  resources :histories, only: [:show]
end

This will add the model name in the params hash.

Then in the controller/action you can get your parent model like:

params[:model_name].constantize # Gives you the model Class (eg. User)

and the foreign key like:

params[:model_name].foreign_key # Gives you column name (eg. user_id)

So you can do something like:

parent_class = params[:model_name].constantize
parent_foreing_key = params[:model_name].foreign_key

parent_object = parent_class.find(params[parent_foreing_key])
Not Entered
  • 193
  • 1
  • 8
  • 3
    This is great, cleanest answer IMO – niborg Oct 06 '16 at 20:48
  • This is a nice solution, however when using it I ran into problems in my controller specs. I didn't find an existing Rails way to set a value for `model_name` param for a whole controller spec, so I would have to pass a value in each of my spec methods. – Matt Oct 25 '17 at 14:05
  • 1
    How do you prevent clients from changing the default by requesting with a different model_name param? – BC. Aug 17 '18 at 15:39
  • Yes, the proposed method can actually open a security vulnerability. I guess it will be better to check params[:model_name] according to your authorization policy or to create separate model factory. – Not Entered Aug 21 '18 at 18:46
  • This is a very elegant solution, and I had no idea you could insert a param from the routes. Exactly the type of solution I was hoping for. – RudyOnRails Jan 15 '19 at 16:46
13

not really a solution but you can get away with

parent_klasses = %w[user publication comment]
if klass = parent_klasses.detect { |pk| params[:"#{pk}_id"].present? }
  @parent = klass.camelize.constantize.find params[:"#{klass}_id"]
end

if you are using a convention between your parameter name and your models

jvnill
  • 29,479
  • 4
  • 83
  • 86
1

As an alternative to the accepted answer, you could use a dynamic route like this:

get ':item_controller/:item_id/histories/:id', to: 'histories#show'

This should then should allow you to access the parent class something like this in your histories_controller.rb

parent_controller = params[:item_controller]
parent_class = parent_controller.singularize.camelize.constantize
@parent = parent_class.find(params[:item_id])

You may be able to add a constraint on item_controller in the routes as well if you need to.

Phil
  • 2,797
  • 1
  • 24
  • 30