无法使用 Devise 批量分配受保护的属性以创建 has_many 嵌套模型

Posted

技术标签:

【中文标题】无法使用 Devise 批量分配受保护的属性以创建 has_many 嵌套模型【英文标题】:Can't mass-assign protected attributes for creating a has_many nested model with Devise 【发布时间】:2013-05-20 18:21:38 【问题描述】:

我看过 RailsCast,另一个嵌套属性视频,很多 SO 帖子,并为此奋斗了一段时间,但我仍然无法弄清楚。我希望它是微小的。

我有两个模型,User(由 Devise 创建)和 Locker(又名产品愿望清单),当他们注册时,我正在尝试为 User 创建一个 Locker。我的登录表单有一个字段是他们新的Locker(恰当地称为:name)的名称,我试图将其分配给在新用户注册时创建的储物柜。我所受到的只是:

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

我已经在我的两个模型中尝试了accepts_nested_attributesattr_accesible 的所有组合,但仍然没有任何效果。我可以从日志中看到 Devise#create 方法正在处理它,而且我知道 Devise 不够聪明,无法按照我的意愿创建模型:)

这是我的两个模型的相关部分:

# 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

# 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

我假设我需要重写 Devise 的 create 方法才能以某种方式使其工作,但我对 Rails 很陌生,并且已经习惯了它的黑匣子“魔法”性质。

如果有人可以帮助我,我将非常感激。已经花太多时间在这上面了:)

编辑:我意识到我在我的问题中遗漏了一些东西。我的 Locker 模型具有三个属性 - namedescription(非强制性)和 user_id 以将其链接回 User。我的注册表单只需要name,所以我没有遍历嵌套表单中的所有属性。这也与我的问题有关吗?

编辑 2: 我还想出了如何覆盖 Devise 的 RegistrationsController#create 方法,我只是不知道该放什么。 Devise 的整个 resource 对我来说毫无意义,浏览他们的 RegistrationsController 源代码也对我没有多大帮助。

对于奖励积分:当用户提交带有无效数据的登录表单时,Locker 字段总是空白,而常规的设计字段、用户名和电子邮件被填写。这也可以轻松解决吗?如果有,怎么做?

【问题讨论】:

你是如何创建储物柜的?你能发布控制器代码吗? 我发布了我的 Lockers#create 方法,但我认为我的代码甚至没有达到那个目的。我认为 Devise 正在尝试创建用户并绕过我的控制器代码。不过我可能是错的。 这应该很容易检查,只需在控制器中添加一个debugger 行,看看它是否停在那里。 @mihai 如果我所要做的就是安装调试器 gem 并将 debugger 添加到我的控制器中构建新储物柜的上方的行中,它不会触发。正如我所怀疑的那样,看来我必须重写 Devise 的 User#create 方法。 是的,关于调试器这就是你所要做的一切 【参考方案1】:

首先,你有一个错字:

attr_accessible :locker_attributes

应该是复数:

attr_accessible :lockers_attributes

那么,使用nested_attributes 的标准方法是:

<%= 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 %>

在你的控制器中:

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

作为旁注,您可以通过不同的方式避免在控制器中为新用户构建新的储物柜:

User 上创建工厂方法,或覆盖new,或使用after_initialize 回调来确保实例化的每个新用户都能自动构建一个储物柜

将特定对象传递给fields_for

<% f.fields_for :lockers, f.object.lockers.new do |new_locker_fields| %>

【讨论】:

我非常感谢您的回答,并且我认为您走在正确的轨道上(因为我了解您的目标),但是在 Devise#create 中发生了质量分配错误,我不知道那在哪里。我已经可以创建UsersLockers 等,这只是注册时自动为用户创建储物柜的新功能不起作用。似乎我需要深入研究 Devise,但我不知道如何。 哦,不明白问题出在设计上。你知道你可以覆盖设计控制器吗?参见devise wiki,“配置控制器”部分 哦,你需要覆盖的方法是new_with_session on User。有一个关于它的railscast,但不幸的是它不是免费的......另一个选择是在你的控制器上覆盖build_resource 嗯,我检查了 Devise 中的两个 new_with_session 实例:一个处理 facebook 登录,另一个只有这一行 new(params)。不知道我将如何使用其中任何一个。还尝试修改提到的所有三个地方build_resource,但我只是把事情搞砸了。一旦用户点击提交按钮,似乎设计只是扔掉与Locker相关的任何参数。【参考方案2】:

有人用strong attributes 和如何覆盖Devise 的sign_up_params 以更“Rails 4'y”的方式帮助我找出解决方案(以捕获来自我的注册表单的所有数据)。

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

Gemfile 添加:gem 'strong_parameters'

注释掉我的user.rb 文件中的attr_accessible 语句,因为显然强参数消除了对attr_accessible 声明的需要。

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

以及在提交表单之前构建Locker的正确方法:在嵌套表单的开头:

&lt;%= l.input :name, :required =&gt; true, label: "Locker name", :placeholder =&gt; "Name your first locker" %&gt;

再次感谢您的帮助。我知道如果不查看整个代码库就很难回答这样的问题。

【讨论】:

以上是关于无法使用 Devise 批量分配受保护的属性以创建 has_many 嵌套模型的主要内容,如果未能解决你的问题,请参考以下文章

无法批量分配受保护的属性

嵌套受保护模型的“无法批量分配受保护属性”

“警告:无法批量分配受保护的属性”

无法批量分配受保护的属性:配置文件,

ActiveAdmin:无法批量分配受保护的属性:电子邮件、密码、密码确认

尝试将 Jcrop 添加到 Paperclip 时无法批量分配虚拟属性