Rails Form Object с помощью Virtus: has_many

Мне трудно разобраться, как сделать form_object, который создает несколько связанных объектов для has_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

Теперь я пойду вперед и покажу 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"}

Хэш параметров выглядит нормально для меня.

В журналах я получаю два предупреждения об устаревании, которые заставляют меня думать, что virtus может быть устаревшим и, следовательно, больше не рабочим решением для объектов формы в рельсах:

ПРЕДУПРЕЖДЕНИЕ О УСТАРЕВАНИИ: Метод 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: Expected ["0", "foofoo"} разрешено: true>] отвечать на #to_hash из /Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb:196: в "принуждении"

И затем все это выдает следующее сообщение:

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

Мне кажется, что я либо близко, и мне не хватает чего-то маленького, чтобы это сработало, или я осознаю, что virtus устарел и больше не может использоваться (из-за предупреждений об устаревании).

Ресурсы, на которые я смотрел:

Я пытался заставить работать ту же форму, но с жемчужиной реформ. Я тоже столкнулся с проблемой. Этот вопрос размещен здесь.

Заранее спасибо!

3 ответа

Решение

Я бы просто установил emails_attributes из user_form_params в user_form.rb в качестве метода установки. Таким образом, вам не нужно настраивать поля формы.

Полный ответ:

Модели:

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

Проблема в том, что формат JSON передается в UserForm.new() это не то, что ожидается.

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

У вас есть проблема, потому что вы не занесены в белый список каких-либо атрибутов :emails, Это сбивает с толку, но этот замечательный совет от Пэт Шонесси должен помочь вам разобраться.

Это то, что вы ищете, хотя:

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: '...' },
    { ... }
  ]
}

Который вы можете внести в белый список с вышеупомянутым решением.

Другие вопросы по тегам