3

我很难弄清楚如何创建一个 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记录保留的数据。这是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

现在我将继续展示users_controller设置 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

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 对我来说看起来不错:

表单的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 中表单对象的有效解决方案:

弃用警告:方法 to_hash 已弃用,将在 Rails 5.1 中删除,因为ActionController::Parameters不再继承自 hash。使用这种已弃用的行为会暴露潜在的安全问题。如果您继续使用此方法,您可能会在您的应用程序中创建一个可被利用的安全漏洞。相反,请考虑使用以下未弃用的记录方法之一: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>] 响应来自 /Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb 的#to_hash :196:在“强制”中

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

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

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

我看过的资源:

我确实试图让相同的表格工作,但使用了改革轨道 gem。我在那里也遇到了问题。这个问题张贴在这里

提前致谢!

4

3 回答 3

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 %>
于 2017-03-22T00:44:48.447 回答
0

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

您在变量中传递给它的 JSONuser_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
于 2017-03-25T15:58:28.693 回答
0

您有一个问题,因为您没有将 . 下的任何属性列入白名单:emails。这很令人困惑,但是Pat Shaughnessy 的这个绝妙技巧应该可以帮助您弄清楚

不过,这正是您要寻找的:

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

注意id属性:更新记录很重要。您需要确保在表单对象中考虑到这种情况。

如果 Virtus 的所有这些表单对象都变得太多,请考虑改革。它有类似的方法,但它存在的理由是将形式与模型分离。


您的表单也有问题......我不确定您希望使用您使用的语法实现什么,但是如果您查看您的 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: '...' },
    { ... }
  ]
}

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

于 2017-03-17T16:01:48.293 回答