Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't mass-assign protected attributes for creating a has_many nested model with Devise

I've watched the RailsCast, another nested attributes video, lots of SO posts, and fought with this for a while, but I still can't figure it out. I hope it's something tiny.

I have two models, User (created by Devise), and Locker (aka, a product wishlist), and I'm trying to create a Locker for a User when they sign up. My login form has a field for the name of their new Locker (aptly called :name) that I'm trying to assign to the locker that gets created upon new user registration. All I'm ever greeted with is:

WARNING: Can't mass-assign protected attributes: locker

I've tried every combination of accepts_nested_attributes and attr_accesible in both of my models, yet still nothing works. I can see from the logs that it's being processed by the Devise#create method, and I know Devise isn't smart enough to create my models how I want :)

Here's the relevant bits of my two models:

# user.rb    
class User < ActiveRecord::Base
  attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :locker_attributes

  # Associations
  has_many :lockers
  has_many :lockups, :through => :lockers

  # Model nesting access
  accepts_nested_attributes_for :lockers
end

and

# locker.rb
class Locker < ActiveRecord::Base
  belongs_to :user
  has_many :lockups
  has_many :products, :through => :lockups 

  attr_accessible :name, :description
end

# lockers_controller.rb (create)
    @locker = current_user.lockers.build(params[:locker])
    @locker.save

I'm assuming I need to override Devise's create method to somehow get this to work, but I'm quite new to rails and am getting used to the black box "magic" nature of it all.

If anyone can help me out, I'd be incredibly thankful. Already spent too much time on this as it is :)

EDIT: I realized I omitted something in my problem. My Locker model has three attributes - name, description (not mandatory), and user_id to link it back to the User. My signup form only requires the name, so I'm not looping through all the attributes in my nested form. Could that have something to do with my issue too?

EDIT 2: I also figured out how to override Devise's RegistrationsController#create method, I just don't know what to put there. Devise's whole resource thing doesn't make sense to me, and browsing their source code for the RegistrationsController didn't help me much either.

And for bonus points: When a user submits the login form with invalid data, the Locker field always comes back blank, while the regular Devise fields, username & email, are filled in. Could this also be fixed easily? If so, how?

like image 290
Kyle Carlson Avatar asked May 25 '13 06:05

Kyle Carlson


2 Answers

first, you have a typo :

attr_accessible :locker_attributes

should be plural :

attr_accessible :lockers_attributes

then, the standard way to use nested_attributes is :

<%= form_for @user do |f| %>
  <%# fields_for will iterate over all user.lockers and 
      build fields for each one of them using the block below,
      with html name attributes like user[lockers_attributes][0][name].
      it will also generate a hidden field user[lockers_attributes][0][id]
      if the locker is already persisted, which allows nested_attributes
      to know if the locker already exists of if it must create a new one
  %>
  <% f.fields_for :lockers do |locker_fields| %>
    <%= locker_fields.label      :name %>
    <%= locker_fields.text_field :name %>
  <% end %>
<% end %>

and in you controller :

def new
  @user = User.new
  @user.lockers.build
end

def create
  # no need to use build here : params[:user] contains a
  # :lockers_attributes key, which has an array of lockers attributes as value ;
  # it gets assigned to the user using user.lockers_attributes=, 
  # a method created by nested_attributes
  @user = User.new( params[:user] )
end

as a side note, you can avoid building a new locker for new users in controller in different ways:

  • create a factory method on User, or override new, or use an after_initialize callback to ensure every new user instantiated gets a locker builded automatically

  • pass a specific object to fields_for :

    <% f.fields_for :lockers, f.object.lockers.new do |new_locker_fields| %>
    
like image 175
m_x Avatar answered Nov 09 '22 12:11

m_x


Someone helped me figure out the solution in a more "Rails 4'y" way with strong attributes & how to override Devise's sign_up_params (to catch all the data coming from my signup form).

  def sign_up_params
    params.require(:user).permit(:username, :email, :password, :lockers_attributes)
 end

Gemfile addition: gem 'strong_parameters'

Commenting out the attr_accessible statement in my user.rb file, since apparently strong parameters eliminate the need for attr_accessible declarations.

 # attr_accessible :username, :email, :password, :password_confirmation, :lockers

And the/a correct way of building a Locker before submitting the form: at the beginning of the nested form:

<%= l.input :name, :required => true, label: "Locker name", :placeholder => "Name your first locker" %>

Thanks again for all your help. I know a question like this is difficult to answer without seeing the whole codebase.

like image 2
Kyle Carlson Avatar answered Nov 09 '22 10:11

Kyle Carlson