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-форма выглядит хорошо для меня:
Когда приведенный выше ввод представлен: Вот хэш параметров:
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: '...' },
{ ... }
]
}
Который вы можете внести в белый список с вышеупомянутым решением.