How do I update a nested resource without passing

2019-08-31 17:54发布

问题:

Models

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
  has_many :roles, :dependent => :destroy, :inverse_of => :user
  has_many :companies, :through => :roles
  accepts_nested_attributes_for :roles, :limit => 1, :allow_destroy => true

end

class Role < ActiveRecord::Base
  belongs_to :user, :inverse_of => :roles
  belongs_to :company, :inverse_of => :roles
  accepts_nested_attributes_for :company
end

class Company < ActiveRecord::Base
  has_many :roles, :dependent => :destroy, :inverse_of => :user
  has_many :users, :through => :roles
  validates :name, presence: true
end

Custom Devise Registration Controller

class RegistrationsController < Devise::RegistrationsController

    # GET /resource/sign_up
  def new
    build_resource({})
    @role = resource.roles.build(role: "owner", active: 1, default_role: 1)
    @company = @role.build_company
    set_minimum_password_length
    yield resource if block_given?
    respond_with self.resource
  end

  protected

  def sign_up_params
      params.require(:user).permit(:email, :password, :password_confirmation, roles_attributes: [ company_attributes: [ :id, :name ] ] )
  end

end

HTML

<%= form_for(resource, :html => {:class => "form-signin" }, as: resource_name, url: registration_path(resource_name)) do |f| %>
    <%= render partial: "shared/flash" %>
    <%= devise_error_messages! %>
    <h1 class="form-signin-heading text-muted">Register</h1>
    <%= f.email_field :email, class: "form-control", placeholder: "Email", autofocus: true %>
    <%= f.password_field :password, class: "form-control", placeholder: "Password", autocomplete: "off" %>
    <%= f.password_field :password_confirmation, class: "form-control", placeholder: "Password Confirmation", autocomplete: "off" %>

    <%= f.fields_for :roles, resource.roles.build do |r| %>
    <%= r.fields_for :company, resource.roles.build.build_company do |c| %>
          <%= c.text_field :name, class: "form-control", placeholder: "Company", autocomplete: "off" %>
    <% end %>
    <% end %>

    <button class="btn btn-lg btn-primary btn-block" type="submit">
        Register
    </button>
<% end %>

This works - my intermediate Role is created with the id_user and id_company. The problem is I want to set the some additional fields in the newly created Role. for example I have a :role column that I want to set to 'owner' as this is the a brand new company and the user that signed up is the owner.

I want to do this in the controller to prevent any mass assignment issues from the user submitted form.

Do I need to set this somehow in the custom devise registration controller and create a full custom create action?

I admit I am likely not explaining this well as I am a bit of a newbie on the whole nested forms and active record etc.

UPDATE

It's not pretty but I just pasted this at the end of my new controller:

  def set_minimum_password_length
    if devise_mapping.validatable?
      @minimum_password_length = resource_class.password_length.min
    end
  end

UPDATE 2

I had copied the master code vs the current version code. Once I fixed that it's all good.

回答1:

I would overwrite the create action so that you can hardcode (and the user can't mess with) the role attributes.

You're assigning them in the new action, but you'd need to have hidden fields so they get passed to create and persist into the database. However, that's not a good idea because anyone can edit the HTML and change those values. It's better to do this in the create action instead like so:

class RegistrationsController < Devise::RegistrationsController
  def create
    build_resource(sign_up_params)

    # The last role should be the one created in the form
    # We set the values here so they get saved and they aren't passed in the form
    resource.roles.last.assign_attributes(role: "owner", active: 1, default_role: 1)

    resource.save
    yield resource if block_given?
    if resource.persisted?
      if resource.active_for_authentication?
        set_flash_message :notice, :signed_up if is_flashing_format?
        sign_up(resource_name, resource)
        respond_with resource, location: after_sign_up_path_for(resource)
      else
        set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
        expire_data_after_sign_in!
        respond_with resource, location: after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource
      set_minimum_password_length
      respond_with resource
    end
  end
end