Rails 表单对象与 Virtus:has_many 关联
Rails Form Object with Virtus: has_many association
我很难弄清楚如何制作一个 form_object 来为 has_many
与 virtus gem 关联创建多个关联对象。
下面是一个人为设计的示例,其中表单对象可能有点矫枉过正,但它确实显示了我遇到的问题:
假设有一个 user_form
对象创建了一个 user
记录,然后是一对关联的 user_email
记录。以下是模型:
# models/user.rb
class User < ApplicationRecord
has_many :user_emails
end
# models/user_email.rb
class UserEmail < ApplicationRecord
belongs_to :user
end
我继续创建一个表单对象来表示用户表单:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
attribute :emails, Array[EmailForm]
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
# user = User.create(name: name)
# emails.each do |email_form|
# UserEmail.create(user: user, email: email_form.email_text)
# end
end
end
人们会注意到 UserForm
class 我有 attribute :emails, Array[EmailForm]
。这是尝试验证和捕获将为关联的 user_email
记录保留的数据。这是 user_email
记录的 Embedded Value
形式:
# app/forms/email_form.rb
# Note: this form is an "Embedded Value" Form Utilized in user_form.rb
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
现在我将继续展示设置 user_form 的 users_controller
。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
@user_form = UserForm.new
@user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
@user_form = UserForm.new(user_form_params)
if @user_form.save
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails: [:email_text]})
end
end
new.html.erb
:
<h1>New User</h1>
<%= render 'form', user_form: @user_form %>
和 _form.html.erb
:
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<% unique_index = 0 %>
<% f.object.emails.each do |email| %>
<%= label_tag "user_form[emails][#{unique_index}][email_text]","Email" %>
<%= text_field_tag "user_form[emails][#{unique_index}][email_text]" %>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
注意: 如果有更简单、更传统的方法来显示此表单对象中 user_emails
的输入:请告诉我。我无法让 fields_for
工作。如上所示:我不得不手写出 name
属性。
好消息是表单确实呈现了:
表格的 html 我觉得没问题:
提交上述输入时:这是参数散列:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "emails"=>{"0"=>{"email_text"=>"foofoo"}, "1"=>{"email_text"=>"bazzbazz"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
params 散列对我来说看起来没问题。
在日志中,我收到两个弃用警告,这让我觉得 virtus 可能已经过时,因此不再是 rails:
中表单对象的有效解决方案
DEPRECATION WARNING: Method to_hash is deprecated and will be removed in Rails 5.1, as ActionController::Parameters
no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1)
DEPRECATION WARNING: Method to_a is deprecated and will be removed in Rails 5.1, as ActionController::Parameters
no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1)
NoMethodError: Expected ["0", "foofoo"} permitted: true>] to respond to #to_hash
from /Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb:196:in `coerce'
然后整个过程出错并显示以下消息:
Expected ["0", <ActionController::Parameters {"email_text"=>"foofoo"} permitted: true>] to respond to #to_hash
我觉得我要么很接近并且缺少一些小东西以使其正常工作,要么我意识到 virtus 已经过时并且不再可用(通过弃用警告)。
我查看的资源:
我确实尝试过使用相同的表格,但使用 reform-rails gem. I ran into an issue there too. That question is 。
提前致谢!
您遇到了一个问题,因为您没有将 :emails
下的任何属性列入白名单。这令人困惑,但是 this wonderful tip from Pat Shaughnessy should help set you straight。
不过,这就是您要查找的内容:
params.require(:user_form).permit(:name, { emails: [:email_text, :id] })
注意 id
属性:它对更新记录很重要。您需要确保在表单对象中考虑到这种情况。
如果 Virtus 的所有这些表单对象恶意代码变得太多,请考虑 Reform。它有类似的方法,但它存在的理由是将形式与模型分离。
您的表单也有问题……我不确定您希望使用您使用的语法实现什么,但是如果您查看您的 HTML,您会发现您的输入名称不会成功。试试更传统的东西:
<%= f.fields_for :emails do |ff| %>
<%= ff.text_field :email_text %>
<% end %>
有了这个你会得到像 user_form[emails][][email_text]
这样的名字,Rails 可以方便地切成这样的东西:
user_form: {
emails: [
{ email_text: '...', id: '...' },
{ ... }
]
}
你可以用上面的解决方案加入白名单。
我只是将 user_form.rb 中的 user_form_params 中的 emails_attributes 设置为 setter 方法。这样您就不必自定义表单域。
完整答案:
型号:
#app/modeles/user.rb
class User < ApplicationRecord
has_many :user_emails
end
#app/modeles/user_email.rb
class UserEmail < ApplicationRecord
# contains the attribute: #email
belongs_to :user
end
表单对象:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
validates :name, presence: true
validate :all_emails_valid
attr_accessor :emails
def emails_attributes=(attributes)
@emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
@emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email|
UserEmail.new(email: email.email_text)
end
user.user_emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
# app/forms/email_form.rb
# "Embedded Value" Form Object. Utilized within the user_form object.
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
控制器:
# app/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
def new
@user_form = UserForm.new
@user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
@user_form = UserForm.new(user_form_params)
if @user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
观看次数:
#app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: @user_form %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
问题是传递给 UserForm.new()
的 JSON 的格式不是预期的。
您传递给它的 JSON,在 user_form_params
变量中,当前具有以下格式:
{
"name":"testform",
"emails":{
"0":{
"email_text":"email1@test.com"
},
"1":{
"email_text":"email2@test.com"
},
"2":{
"email_text":"email3@test.com"
}
}
}
UserForm.new()
实际上需要这种格式的数据:
{
"name":"testform",
"emails":[
{"email_text":"email1@test.com"},
{"email_text":"email2@test.com"},
{"email_text":"email3@test.com"}
}
}
您需要更改 JSON 的格式,然后再将其传递给 UserForm.new()
。如果您将 create
方法更改为以下内容,您将不会再看到该错误。
def create
emails = []
user_form_params[:emails].each_with_index do |email, i|
emails.push({"email_text": email[1][:email_text]})
end
@user_form = UserForm.new(name: user_form_params[:name], emails: emails)
if @user_form.save
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end
我很难弄清楚如何制作一个 form_object 来为 has_many
与 virtus gem 关联创建多个关联对象。
下面是一个人为设计的示例,其中表单对象可能有点矫枉过正,但它确实显示了我遇到的问题:
假设有一个 user_form
对象创建了一个 user
记录,然后是一对关联的 user_email
记录。以下是模型:
# models/user.rb
class User < ApplicationRecord
has_many :user_emails
end
# models/user_email.rb
class UserEmail < ApplicationRecord
belongs_to :user
end
我继续创建一个表单对象来表示用户表单:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
attribute :emails, Array[EmailForm]
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
# user = User.create(name: name)
# emails.each do |email_form|
# UserEmail.create(user: user, email: email_form.email_text)
# end
end
end
人们会注意到 UserForm
class 我有 attribute :emails, Array[EmailForm]
。这是尝试验证和捕获将为关联的 user_email
记录保留的数据。这是 user_email
记录的 Embedded Value
形式:
# app/forms/email_form.rb
# Note: this form is an "Embedded Value" Form Utilized in user_form.rb
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
现在我将继续展示设置 user_form 的 users_controller
。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
@user_form = UserForm.new
@user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
@user_form = UserForm.new(user_form_params)
if @user_form.save
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails: [:email_text]})
end
end
new.html.erb
:
<h1>New User</h1>
<%= render 'form', user_form: @user_form %>
和 _form.html.erb
:
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<% unique_index = 0 %>
<% f.object.emails.each do |email| %>
<%= label_tag "user_form[emails][#{unique_index}][email_text]","Email" %>
<%= text_field_tag "user_form[emails][#{unique_index}][email_text]" %>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
注意: 如果有更简单、更传统的方法来显示此表单对象中 user_emails
的输入:请告诉我。我无法让 fields_for
工作。如上所示:我不得不手写出 name
属性。
好消息是表单确实呈现了:
表格的 html 我觉得没问题:
提交上述输入时:这是参数散列:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "emails"=>{"0"=>{"email_text"=>"foofoo"}, "1"=>{"email_text"=>"bazzbazz"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
params 散列对我来说看起来没问题。
在日志中,我收到两个弃用警告,这让我觉得 virtus 可能已经过时,因此不再是 rails:
中表单对象的有效解决方案DEPRECATION WARNING: Method to_hash is deprecated and will be removed in Rails 5.1, as
ActionController::Parameters
no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1) DEPRECATION WARNING: Method to_a is deprecated and will be removed in Rails 5.1, asActionController::Parameters
no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1) NoMethodError: Expected ["0", "foofoo"} permitted: true>] to respond to #to_hash from /Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb:196:in `coerce'
然后整个过程出错并显示以下消息:
Expected ["0", <ActionController::Parameters {"email_text"=>"foofoo"} permitted: true>] to respond to #to_hash
我觉得我要么很接近并且缺少一些小东西以使其正常工作,要么我意识到 virtus 已经过时并且不再可用(通过弃用警告)。
我查看的资源:
我确实尝试过使用相同的表格,但使用 reform-rails gem. I ran into an issue there too. That question is
提前致谢!
您遇到了一个问题,因为您没有将 :emails
下的任何属性列入白名单。这令人困惑,但是 this wonderful tip from Pat Shaughnessy should help set you straight。
不过,这就是您要查找的内容:
params.require(:user_form).permit(:name, { emails: [:email_text, :id] })
注意 id
属性:它对更新记录很重要。您需要确保在表单对象中考虑到这种情况。
如果 Virtus 的所有这些表单对象恶意代码变得太多,请考虑 Reform。它有类似的方法,但它存在的理由是将形式与模型分离。
您的表单也有问题……我不确定您希望使用您使用的语法实现什么,但是如果您查看您的 HTML,您会发现您的输入名称不会成功。试试更传统的东西:
<%= f.fields_for :emails do |ff| %>
<%= ff.text_field :email_text %>
<% end %>
有了这个你会得到像 user_form[emails][][email_text]
这样的名字,Rails 可以方便地切成这样的东西:
user_form: {
emails: [
{ email_text: '...', id: '...' },
{ ... }
]
}
你可以用上面的解决方案加入白名单。
我只是将 user_form.rb 中的 user_form_params 中的 emails_attributes 设置为 setter 方法。这样您就不必自定义表单域。
完整答案:
型号:
#app/modeles/user.rb
class User < ApplicationRecord
has_many :user_emails
end
#app/modeles/user_email.rb
class UserEmail < ApplicationRecord
# contains the attribute: #email
belongs_to :user
end
表单对象:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
validates :name, presence: true
validate :all_emails_valid
attr_accessor :emails
def emails_attributes=(attributes)
@emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
@emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email|
UserEmail.new(email: email.email_text)
end
user.user_emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
# app/forms/email_form.rb
# "Embedded Value" Form Object. Utilized within the user_form object.
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
控制器:
# app/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
def new
@user_form = UserForm.new
@user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
@user_form = UserForm.new(user_form_params)
if @user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
观看次数:
#app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: @user_form %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
问题是传递给 UserForm.new()
的 JSON 的格式不是预期的。
您传递给它的 JSON,在 user_form_params
变量中,当前具有以下格式:
{
"name":"testform",
"emails":{
"0":{
"email_text":"email1@test.com"
},
"1":{
"email_text":"email2@test.com"
},
"2":{
"email_text":"email3@test.com"
}
}
}
UserForm.new()
实际上需要这种格式的数据:
{
"name":"testform",
"emails":[
{"email_text":"email1@test.com"},
{"email_text":"email2@test.com"},
{"email_text":"email3@test.com"}
}
}
您需要更改 JSON 的格式,然后再将其传递给 UserForm.new()
。如果您将 create
方法更改为以下内容,您将不会再看到该错误。
def create
emails = []
user_form_params[:emails].each_with_index do |email, i|
emails.push({"email_text": email[1][:email_text]})
end
@user_form = UserForm.new(name: user_form_params[:name], emails: emails)
if @user_form.save
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end