Search code examples
ruby-on-railsdevisenested-formsnested-attributesstrong-parameters

Rails 4.0 with Devise. Nested attributes Unpermited parameters


I am working on a web-app using Devise and Rails 4. I have a User model which I have extended with 2 extra form fields such that when a user signs up he can also submit his first/last names. (based on http://blog.12spokes.com/web-design-development/adding-custom-fields-to-your-devise-user-model-in-rails-4/). I now want to add a Institution model. This model has_many :users, and a user belongs_to :institution. I want to be able to register the institution's name on the same form I register the user. I know I need a nested_attribute in my Institution model, since this is the parent, which I will show in a bit. When I try to sign up the user I get in the console: Unpermited parameters: Institutions.

My hint is that I cannot update my parent class(Institution) based upon my child class (User). Might there be a solution to this? Or has anyone experienced something similar?

class Institutions < ActiveRecord::Base
    has_many :users, 
    accepts_nested_attributes_for :users
end

class User < ActiveRecord::Base
     devise :database_authenticatable, :registerable,
     :recoverable, :rememberable, :trackable, :validatable
     belongs_to :institution
end

registrations/new.html.erb Here I have the nested form

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f|     %>
<%= devise_error_messages! %>
.
. 
    <%= f.fields_for :institutions do |i| %>
        <p><%= i.label :name %><br />
        <%= i.text_field :institutions_attr %></p>
    <% end %>

Based on the tutorial I have linked earlier, I have created a new User::ParameterSanitizer which inherits from the Devise::ParameterSanitizer and overridden the sign_up method as follows:

lib/user_sanitizer.rb

private
def sign_up
    default_params.permit(:first_name, :last_name ,:email, :password,  :password_confirmation, :current_password, institutions_attributes: [:id, :name])
end

Finally, my application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  protected
  def devise_parameter_sanitizer
    if resource_class == User
    User::ParameterSanitizer.new(User, :user, params)
    else 
    super
    end
  end
end

Thank you for reading!

Console params output:

{"utf8"=>"✓",
 "authenticity_token"=>"JKuN6K5l0iwFsj/25B7GKDj7WEHR4DO3oaVyGxGJKvU=",
 "user"=>{"email"=>"abc@foo.com",
 "first_name"=>"abc",
 "last_name"=>"xyz",
 "institutions"=>{"name"=>"Government"},
 "password"=>"[FILTERED]",
 "password_confirmation"=>"[FILTERED]"},
 "commit"=>"Sign up"}

EDIT

As suggested, I have added

params.require(resource_name).permit( :email, :first_name, :last_name, institution:  [:name], :password, :password_confirmation ) and I get an *error syntax error, unexpected ',', expecting => ...nstitution: [:name], :password, :password_confirmation )*

BUT, if I re-edit to

params.require(resource_name).permit( :email, :first_name, :last_name, :password, :password_confirmation, institution:  [:name] ) 

I get NO syntax error but I get Unpermited parameters: Institutions in the Request.

My belief is that this happens because User is a child of Institution. I have, however, been unable to find a work-around this.


Solution

  • config/routes.rb

    Create your own registration controller like so ... (see Devise documentation for the details of overriding controllers here ...) ... which is more elegant way as opposed to doing it via the ApplicationController

    devise_for :users, controllers: {registrations: 'users/registrations'}
    

    app/controllers/users/registrations_controller.rb

    Override the new method to create a Profile associated with the User model as below ... run the configure_permitted_parameters method before to sanitize the parameters (note how to add nested parameters)

    class Users::RegistrationsController < Devise::RegistrationsController
    
      before_filter :configure_permitted_parameters
    
      # GET /users/sign_up
      def new
    
        # Override Devise default behaviour and create a profile as well
        build_resource({})
        resource.build_profile
        respond_with self.resource
      end
    
      protected
    
      def configure_permitted_parameters
        devise_parameter_sanitizer.for(:sign_up) { |u|
          u.permit(:email, :password, :password_confirmation, :profile_attributes => :fullname)
        }
      end
    end
    

    db/migrate/xxxxxxxxxxxxxx_create_profiles.rb

    This is the migration that generates the Profile model (note the reference to User) ... this example profile only keeps fullname as an extension of the User but feel free to add as you wish!

    class CreateProfiles < ActiveRecord::Migration
      def change
        create_table :profiles do |t|
           t.references :user
           t.string :fullname
           t.timestamps
        end
      end
    end
    

    app/models/user.rb

    class User < ActiveRecord::Base
    
      # Associations
      has_one :profile, dependent: :destroy, autosave: true
    
      # Allow saving of attributes on associated records through the parent,
      # :autosave option is automatically enabled on every association
      accepts_nested_attributes_for :profile
    
      # Devise
      # Include default devise modules. Others available are:
      # :confirmable, :lockable, :timeoutable and :omniauthable
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :trackable, :validatable
    end
    

    app/models/profile.rb

    class Profile < ActiveRecord::Base
    
      # Associations
      belongs_to :user
    
      # Validations
      validates :fullname, presence: true
    end
    

    app/views/devise/registrations/new.html

    <% resource.build_profile if resource.profile.nil? %>
    <%= form_for(resource, :as => resource_name,
                           :url => registration_path(resource_name)) do |f| %>
      <ul>
    
        <%= devise_error_messages! %>
    
        <li class="fullname">
          <%= f.fields_for :profile do |profile_fields| %>
            <%= profile_fields.label :fullname %>
            <%= profile_fields.text_field :fullname %>
          <% end %>
        </li>
        <li class="email">
          <%= f.label :email %>
          <%= f.email_field :email, :autofocus => true %>
        </li>
        <li class="password">
          <%= f.label :password %>
          <%= f.password_field :password %>
        </li>
        <li class="password">
          <%= f.label :password_confirmation %>
          <%= f.password_field :password_confirmation %>
        </li>
        <li>
          <%= f.submit %>
        </li>
        <li>
          <p><%= render "devise/shared/links" %></p>
        </li>
      </ul>
    <% end %>