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

2

8

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?

Pamilapammi answered 25/5, 2013 at 6:18 Comment(6)
how are you creating the Locker? Can you post the controller code?Primitivism
I posted my Lockers#create method, but I don't think my code is even getting to that. I think Devise is trying to create the User and just bypassing my controller code. I could be wrong though.Pamilapammi
that should be easy to check, just add a debugger line in the controller and see if it stops there.Primitivism
@Primitivism If all I had to do was install the debugger gem and add debugger to the line in my controller above where my new Lockers are built, it didn't fire. As I suspected, it looks like I'll have to override Devise' User#create method.Pamilapammi
yes, about the debugger that's all you had to doPrimitivism
Forgot to update you. The debugger breakpoint never triggered, again confirming that I need to somehow hack into Devise's RegistrationsController#create method, but I don't know how. Thanks for the suggestion though!Pamilapammi
P
2

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.

Pamilapammi answered 26/5, 2013 at 16:22 Comment(0)
A
2

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| %>
    
Avaricious answered 25/5, 2013 at 9:7 Comment(4)
I completely appreciate your answer, and I think you're on the right track (because I understand where you're going with it), but the mass-assignment error is happening in Devise#create, and I have no idea where that is. I can already create Users, Lockers, etc, it's just the new feature of automagically creating a locker for the user upon registration isn't working. Seems like I need to dig into Devise, but I don't know how.Pamilapammi
oh, did not understand that the problem was from Devise. Did you know you can override Devise controllers ? see devise wiki, section "configuring controllers"Avaricious
oh, and the method you need to override is new_with_session on User. There's a railscast about it, but unfortunately it's not free... another option is to override build_resource on your controller.Avaricious
Hmm, I checked both the new_with_session instances in Devise: one deals with facebook login, and the other only has this line new(params). Not sure how I'd work with either of those. Also tried my hand at modifying all three places build_resource is mentioned, but I just screwed things up. Seems like Devise just tosses out any parameters related to Locker once the user hits the submit button.Pamilapammi
P
2

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.

Pamilapammi answered 26/5, 2013 at 16:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.