Rails Devise Build nested model in controller

2019-09-09 14:17发布

问题:

I'm using Devise and am trying to build nested models in my RegistrationsController.

Though this is not working. I can build nested models via

resource.build_nested_model

in the view, but not in the controller itself.

This is my registrationscontrollers' new-action

  def new
    super
    resource.build_user_info
    resource.user_info.languageskills.build
    if params[:is_driver].to_i == 1
      resource.build_driver
    end
    Rails.logger.debug(resource.build_user_info.inspect)
  end

This is the output it generates:

Started GET "/en/sign_up?is_driver=1" for 127.0.0.1 at 2014-02-13 13:20:01 +0100
Processing by RegistrationsController#new as JS
  Parameters: {"is_driver"=>"1", "locale"=>"en"}
  Rendered registrations/_new_user_fields.html.erb (12.1ms)
  Rendered registrations/new.html.erb within layouts/application (27.8ms)
  Rendered layouts/_header.html.erb (2.8ms)
  Rendered layouts/_messages.html.erb (0.2ms)
  Rendered layouts/_footer.html.erb (0.6ms)
   (0.2ms)  begin transaction
   (0.1ms)  commit transaction
#<UserInfo id: nil, user_id: nil, first_name: nil, last_name: nil, year_of_birth: nil, city: nil, created_at: nil, updated_at: nil, gender_id: nil, interests: nil, about: nil, country_alpha2: nil>
Completed 200 OK in 117ms (Views: 96.5ms | ActiveRecord: 0.4ms)

Why is it not possible? I guess devise isn't saving my changes to the resource, when I build the associated models. The only way out I see is defining my own variable holding a whole copy of the updated resource. This is not a good practice, though.

What would you do?

回答1:

I think the problem is that the default RegistrationsController.new is just:

def new
  build_resource({})
  respond_with self.resource
end

(It depends on your version of devise, but it seems to be this or equivalent for quite a long way back.)

This means that the view is rendered (by respond_with) before you add your nested models in your overridden new because you do this after calling super. I think you have two options:

  1. Don't call super. Just put the first line from the default new at the start of your new, and the last line from the default new at the end of yours.
  2. Override build_resource instead of new. At the beginning of your overridden build_resource, call super and then add the code that was in your new after the super in your build_resource. The only extra thing you'll need to do is check whether build_resource was called with nil or an empty hash, in which case you build your blank user_info etc., or if it was called with a non-empty hash then don't add your blank user_info etc., because build_resource must have been called from create, so the hash will contain whatever your user entered on the form for user_info, so you don't want to overwrite with a blank version! (you could perhaps check the current url or something like that instead of checking the hash parameter, but I personally like that a little less.)

I have used option 2 in the past. I like it because it still calls super, so we're still using devise's implementation of build_resource, whereas option 1 completely ignores devise's implementation of new -- and what if they make some important change to their new in future which you are then missing out on? (For example, there used to be a resource variable local to new, but it's now on self as you can see in the code above.) Option 2 is just a bit more fiddly because you need to check whether you should add your blank user_info etc. So it's up to your taste!