【问题标题】:Rails Form Object with Virtus: has_many association带有 Virtus 的 Rails 表单对象:has_many 关联
【发布时间】:2017-08-09 05:47:55
【问题描述】:

我很难弄清楚如何创建一个 form_object,它为 has_manyvirtus 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 类中注意到我有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 对我来说看起来不错:

当上述输入提交时:这里是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"}

params 哈希对我来说看起来不错。

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

弃用警告:方法 to_hash 已弃用,将在 Rails 5.1 中删除,因为ActionController::Parameters 不再继承自散列。使用这种已弃用的行为会暴露潜在的安全问题。如果您继续使用此方法,您可能会在您的应用程序中创建一个可被利用的安全漏洞。相反,请考虑使用以下未弃用的记录方法之一:http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html(在 (pry):1 处调用新方法) 弃用警告:方法 to_a 已弃用,将在 Rails 5.1 中删除,因为ActionController::Parameters 不再继承自散列。使用这种已弃用的行为会暴露潜在的安全问题。如果您继续使用此方法,您可能会在您的应用程序中创建一个可被利用的安全漏洞。相反,请考虑使用以下未弃用的记录方法之一:http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html(在 (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'

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

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

我觉得我要么很接近并且缺少一些小东西才能让它工作,要么我意识到 virtus 已经过时并且不再可用(通过弃用警告)。

我查看的资源:

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

提前致谢!

【问题讨论】:

    标签: ruby-on-rails ruby virtus


    【解决方案1】:

    我只是将 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 %>
    

    【讨论】:

    • 请接受编辑,我很乐意将您的答案标记为已接受的答案。进行一些小的更改以使验证按预期工作,然后为方便起见:为所有主要部分提供代码。我想感谢你,因为你发布的答案让我度过了我被卡住的部分。
    • 应该注意的是,应该考虑将 virtus gem 用于表单对象的好处。机会是:您可以很容易地创建表单对象,而无需将 virtus gem 依赖项添加到您的项目中。 See Rails form object implementation here
    • 感谢您的回答。这正是我一直在寻找的。不过,我是 Rails 新手。有人可以向我解释为什么我们需要定义emails_attributes=(attributes)。这叫什么?我看到如果未定义此方法,则 @user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new] 无法按预期工作。在 Rails 中,这种魔法发生在哪里?
    【解决方案2】:

    您遇到了问题,因为您没有将: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"=&gt;{"name"=&gt;"neil", "emails"=&gt;""}。它仍然这样说:Unpermitted parameter: emails
    • 我没有意识到您在emails 下没有得到任何值。它不允许空字符串,因为它现在需要一个散列。查看更新的答案。
    【解决方案3】:

    问题在于传递给UserForm.new() 的JSON 格式不是预期的。

    您在 user_form_params 变量中传递给它的 JSON 目前具有以下格式:

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

    【讨论】:

      猜你喜欢
      • 2012-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-20
      • 1970-01-01
      • 2014-12-26
      • 2015-09-04
      相关资源
      最近更新 更多