带有 Virtus 的 Rails 表单对象:has_many 关联 [英] Rails Form Object with Virtus: has_many association

查看:49
本文介绍了带有 Virtus 的 Rails 表单对象:has_many 关联的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我很难弄清楚如何创建一个 form_object 来为 has_many

表单的 html 看起来不错:

当上述输入被提交时:这里是参数哈希:

参数:{"utf8"=>"✓", "authenticity_token"=>"abc123==", "user_form"=>{"name"=>"neil", "emails"=>{"0"=>{"email_text"=>"foofoo"}, "1"=>{"email_text"=>"bazzbazz"}, "2"=>{"email_text"=>""}}}, "commit"=>"创建用户表单"}

params 散列在我看来没问题.

在日志中,我收到两个弃用警告,这让我认为 virtus 可能已经过时,因此不再是 Rails 中表单对象的有效解决方案:

<块引用>

弃用警告:方法 to_hash 已弃用,将在 Rails 5.1 中删除,因为 ActionController::Parameters 不再从 hash 继承.使用这种已弃用的行为会暴露潜在的安全问题.如果您继续使用此方法,您可能会在您的应用中创建一个可被利用的安全漏洞.相反,请考虑使用这些未弃用的记录方法之一:http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html(从 new 在 (pry):1 处调用)弃用警告:方法 to_a 已弃用,将在 Rails 5.1 中删除,因为 ActionController::Parameters 不再从哈希继承.使用这种已弃用的行为会暴露潜在的安全问题.如果您继续使用此方法,您可能会在您的应用中创建一个可被利用的安全漏洞.相反,请考虑使用这些未弃用的记录方法之一:http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html(从 new 在 (pry):1 处调用)NoMethodError:预期 ["0", "foofoo"} 允许:true>] 响应 #to_hash来自/Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb:196:in `coerce'

然后整个事情出错并显示以下消息:

预期 ["0", <ActionController::Parameters {"email_text"=>"foofoo"} allowed: true>] 响应 #to_hash

我觉得我要么很接近并且缺少一些小东西以使其正常工作,要么我意识到 virtus 已过时且不再可用(通过弃用警告).

我看过的资源:

我确实尝试使用相同的表单,但使用 reform-rails gem.我在那里也遇到了问题.这个问题是发布在这里.

提前致谢!

解决方案

我只是将 user_form.rb 中 user_form_params 的 emails_attributes 设置为 setter 方法.这样您就不必自定义表单字段.

完整答案:

模型:

#app/modeles/user.rb类用户<申请记录has_many :user_emails结尾#app/modeles/user_email.rb类 UserEmail <申请记录# 包含属性:#email归属地:用户结尾

表单对象:

# app/forms/user_form.rb类用户窗体包括 ActiveModel::Model包括 Virtus.model属性:名称,字符串验证:名称,存在:真验证:all_emails_validattr_accessor : 电子邮件def emails_attributes=(属性)@emails ||= []attributes.each do |_int, email_params|email = EmailForm.new(email_params)@emails.push(email)结尾结尾定义保存如果有效?坚持!真的别的错误的结尾结尾私人的def坚持!用户 = 用户.new(名称:名称)new_emails = emails.map 做 |email|UserEmail.new(电子邮件:email.email_text)结尾user.user_emails = new_emails用户.保存!结尾def all_emails_validemails.each 做 |email_form|errors.add(:base, "电子邮件必须存在") 除非 email_form.valid?结尾throw(:abort) 如果errors.any?结尾结尾# app/forms/email_form.rb#嵌入值"表单对象.在 user_form 对象中使用.类电子邮件表单包括 ActiveModel::Model包括 Virtus.model属性:email_text,字符串验证:email_text,存在:真结尾

控制器:

# app/users_controller.rb类用户控制器 <应用控制器定义索引@users = User.all结尾定义新@user_form = UserForm.new@user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]结尾定义创建@user_form = UserForm.new(user_form_params)如果@user_form.saveredirect_to users_path,注意:用户已成功创建."别的渲染:新结尾结尾私人的def user_form_paramsparams.require(:user_form).permit(:name, {emails_attributes: [:email_text]})结尾结尾

观看次数:

#app/views/users/new.html.erb<h1>新用户</h1><%= render 'form', user_form: @user_form %>#app/views/users/_form.html.erb<%= form_for(user_form, url: users_path) 做 |f|%><% if user_form.errors.any?%><div id="error_explanation"><h2><%=pluralize(user_form.errors.count, "error") %>禁止保存此用户:</h2><ul><% user_form.errors.full_messages.each do |message|%><li><%=消息%></li><%结束%>

<%结束%><div class="field"><%= f.label :name %><%= f.text_field :name %>

<%= f.fields_for :emails do |email_form|%><div class="field"><%= email_form.label :email_text %><%=email_form.text_field :email_text %>

<%结束%><div class="actions"><%= f.submit %>

<%结束%>

I am having a tough time figuring out how to make a form_object that creates multiple associated objects for a has_many association with the virtus gem.

Below is a contrived example where a form object might be overkill, but it does show the issue I am having:

Lets say there is a user_form object that creates a user record, and then a couple associated user_email records. Here are the models:

# models/user.rb
class User < ApplicationRecord
  has_many :user_emails
end

# models/user_email.rb
class UserEmail < ApplicationRecord
  belongs_to :user
end

I proceed to create a a form object to represent the user form:

# 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

One will notice in the UserForm class that I have the attribute :emails, Array[EmailForm]. This is an attempt to validate and capture the data that will be persisted for the associated user_email records. Here is the Embedded Value form for a user_email record:

# 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

Now I will go ahead and show the users_controller which sets up the user_form.

# 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

The new.html.erb:

<h1>New User</h1>

<%= render 'form', user_form: @user_form %>

And the _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 %>

Note: If there is an easier, more conventional way to display the inputs for the user_emails in this form object: let me know. I could not get fields_for to work. As shown above: I had to write out the name attributes by hand.

The good news is that the form does render:

The html of the form looks ok to me:

When the above input is submitted: Here is the params hash:

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"}

The params hash looks ok to me.

In the logs I get two deprecation warnings which makes me think that virtus might be outdated and thus no longer a working solution for form objects in 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'

And then the whole thing errors out with the following message:

Expected ["0", <ActionController::Parameters {"email_text"=>"foofoo"} permitted: true>] to respond to #to_hash

I feel like I am either close and am missing something small in order for it to work, or I am realizing that virtus is outdated and no longer usable (via the deprecation warnings).

Resources I looked at:

I did attempt to get the same form to work but with the reform-rails gem. I ran into an issue there too. That question is posted here.

Thanks in advance!

解决方案

I would just set the emails_attributes from user_form_params in the user_form.rb as a setter method. That way you don't have to customize the form fields.

Complete Answer:

Models:

#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

Form Objects:

# 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

Controller:

# 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

Views:

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

这篇关于带有 Virtus 的 Rails 表单对象:has_many 关联的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆