使用Virtus的Rails表单对象:has_many association

时间:2017-03-17 15:27:19

标签: ruby-on-rails ruby virtus

我很难搞清楚如何创建一个form_object,为与virtus gemhas_many关联创建多个关联对象。

下面是一个人为的例子,表单对象可能过度,但它确实显示了我遇到的问题:

假设有一个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记录保留的数据。以下是Embedded Value记录的user_email表单:

# 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属性。

好消息是表单确实呈现:

rendered form

表单的html对我来说没问题:

html of the form

提交上述输入时:这是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 hash对我来说没问题。

在日志中我得到两个弃用警告,这让我觉得virtus可能已经过时,因此不再是rails中表单对象的工作解决方案:

  

DEPRECATION WARNING:方法to_hash已弃用,将在Rails 5.1中删除,因为ActionController::Parameters不再继承哈希值。使用此弃用行为会暴露潜在的安全问题。如果您继续使用此方法,则可能会在您的应用中创建可被利用的安全漏洞。相反,请考虑使用其中一种未弃用的文档化方法:http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html(从new at(pry)调用:1)   弃用警告:方法to_a已弃用,将在Rails 5.1中删除,因为ActionController::Parameters不再继承哈希值。使用此弃用行为会暴露潜在的安全问题。如果您继续使用此方法,则可能会在您的应用中创建可被利用的安全漏洞。相反,请考虑使用其中一种未弃用的文档化方法:http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html(从new at(pry)调用:1)   NoMethodError:预期[“0”,“foofoo”}允许:true&gt;]响应#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

提前致谢!

3 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:0)

您遇到问题,因为您尚未将:emails下的任何属性列入白名单。这很令人困惑,但是this wonderful tip from Pat Shaughnessy should help set you straight

这就是你要找的东西:

params.require(:user_form).permit(:name, { emails: [:email_text, :id] })

请注意id属性:更新记录非常重要。您需要确保在表单对象中考虑该情况。

如果Virtus的所有这些形式对象malarkey变得过多,请考虑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: '...' },
    { ... }
  ]
}

您可以使用上述解决方案将其列入白名单。

答案 2 :(得分:0)

问题是传递给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