通过复选框添加多个嵌套属性 Rails 4(可能有多种形式)
Add Multiple Nested Attributes through checkboxes Rails 4 (maybe with multiple forms)
3/13 更新:
我用我的模型、控制器逻辑和几个表单版本做了一个小示例项目。
我正在构建一个表单,用户可以在其中添加 "Tasks" 和 "Milestones"。 (即任务 = 'Vacuum' 在里程碑 = 'clean House' 内)。它基本上是一个 Task/Subtask 类型的模型,父类是 'Milestone',子类是 'Task'。
任务和里程碑都属于 "Project"...所以我试图通过带有更新操作的嵌套表单添加任务和里程碑。我在想要走的路是为每个 @task_template 实例创建一个表单并一次更新多个表单。
我的问题是我还试图通过名为 "MilestoneTemplates" 和 "TaskTemplates" 的表动态设置 "starter milestones/tasks"...
用户打开 "Add Milestones/Task" 页面,根据他们的项目类型,他们会在复选框旁边看到一系列预建任务 (@task_templates) 和里程碑 (@milestone_templates)。然后用户选中他们想要添加的任务或里程碑旁边的复选框。这应该使用预建的@task_template.name、@task_template.description...等
为用户创建一个特定任务
我什至无法创建 1。我正在使用 Rails 4,我想我已经正确设置了我的 strong_params。以下是我的观点:
型号:
class Task < ActiveRecord::Base
belongs_to :user
belongs_to :project
belongs_to :milestone
class Milestone < ActiveRecord::Base
belongs_to :project
belongs_to :user
has_many :tasks, dependent: :destroy, inverse_of: :milestone
accepts_nested_attributes_for :tasks, allow_destroy: true
class Project < ActiveRecord::Base
has_many :milestones, dependent: :destroy
has_many :tasks, dependent: :destroy
accepts_nested_attributes_for :tasks, allow_destroy: true
accepts_nested_attributes_for :milestones, allow_destroy: true
#the "Starter Milestones & Tasks"
class MilestoneTemplate < ActiveRecord::Base
has_many :task_templates, dependent: :destroy, inverse_of: :milestone_template
class TaskTemplate < ActiveRecord::Base
belongs_to :milestone_template, inverse_of: :task_templates
控制器:
class ProjectsController < ApplicationController
def new_milestones
@project = Project.find(params[:p])
@project.milestones.build
@project.tasks.build
@milestones_templates = MilestoneTemplate.where(template_id: @project.template_id)
end
def create_milestones
@project.milestone_ids = params[:project][:milestones]
@project.task_ids = params[:project][:tasks]
@milestone = Milestone.new
@task = Task.new
@template = Template.find( @project.template_id)
if @project.update_attributes(project_params)
redirect_to view_milestones_path(p: @project.id)
flash[:notice] = "Successfully Added Tasks & Milestones"
else
redirect_to new_milestones_path(p: @project.id )
format.json { render json: @project.errors, status: :unprocessable_entity }
end
end
def project_params
params.require(:project).permit( :id, :name,
milestones_attributes: [:id, {:milestone_ids => []}, {:ids => []}, {:names => []}, :project_id, :user_id,
:name, :description, :due_date, :rank, :completed, :_destroy,
tasks_attributes: [:id, {:task_ids => []}, {:names => []}, {:ids => []}, :milestone_id, :project_id,
:user_id, :name, :description, :due_date, :rank, :completed, :_destroy]] )
end
end
表格测试 1:
<%= form_for @project, url: create_milestones_path(p: @project.id) do |f| %>
<label>Milestones</label><br>
<div class="row">
<%= hidden_field_tag "project[names][]", nil %>
<% @milestones_templates.each do |m| %>
<%= check_box_tag "project[names][]", m.name, @milestones_templates.include?(m), id: dom_id(m)%>
<%= label_tag dom_id(m), m.name %>
<%= hidden_field_tag "project[milestone][names][]", nil %>
<% m.task_templates.each do |t| %>
<%= check_box_tag "project[milestone][names][]", t.name, m.task_templates.include?(t), id: dom_id(t) %>
<%= label_tag dom_id(t), t.name %>
<% end %>
<% end %>
</div>
<%= f.submit %>
表单测试 2(尝试提交一组表单):
<label>Milestones</label><br>
<%= hidden_field_tag "project[milestone_ids][]", nil %>
<% @milestones_templates.each do |m| %>
<div>
<%= f.fields_for :milestones do |fm|%>
<%= check_box_tag "project[milestone_ids][]", @milestones_templates.include?(m), id: dom_id(m) %>
<%= label_tag dom_id(m), m.name %></div>
<%= hidden_field_tag :name, m.name %>
<%= hidden_field_tag "project[milestone][task_ids][]", nil %>
<% m.task_templates.each do |t| %>
<%= fm.fields_for :tasks do |ft| %>
<%= check_box_tag "project[milestone][task_ids][]", t.name, m.task_templates.include?(t), id: dom_id(t)%>
<%= label_tag dom_id(t), t.name %>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
根据 xcskier56 在评论中的要求,我添加了来自 Chrome 检查员的 POST 代码。如您所见,该表单甚至没有调用任务,只是调用父里程碑。里程碑显示在表单中,但任务没有....
project[formprogress]:2
project[milestone_ids][]:
project[milestone][names]:true
name:Milestone 1
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 2
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 3
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 4
project[milestone][task_ids][]:
这段代码我自己没法测试,但是我实现过类似的代码,所以思路应该是正确的。
这里的技巧是使用 each_with_index,然后将该索引传递给您的 fields_for
调用。这样,您通过复选框添加的每一个额外的 milestone_id
都会与之前的显着不同。你可以找到这个 here.
的另一个例子
使用这种方法,您的表单应该如下所示:
<%= form_for @project do |f| %>
<% @milestones_templates.each_with_index do |milestone, index| %>
<br>
<%= f.fields_for :milestones, index: index do |fm| %>
<%= fm.hidden_field :name, value: milestone.name %>
<!-- Create a checkbox to add the milestone_id to the project -->
<%= fm.label milestone.name %>
<%= fm.check_box :milestone_template_id,{}, milestone.id %>
<br>
<% milestone.task_templates.each_with_index do |task, another_index| %>
<%= fm.fields_for :tasks, index: another_index do |ft| %>
<!-- Create a checkbox for each task in the milestone -->
<%= ft.label task.name %>
<%= ft.check_box :task_ids, {}, task.id %>
<% end %>
<% end %>
<br>
<% end %>
<% end %>
<br>
<%= f.submit %>
<% end %>
# Working strong parameters.
params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )
这应该输出 milestone_template_ids,每个 task_template_ids 嵌套在里面。
编辑:我忘记了,如果您查看文档,check_boxes 需要中间的另一个参数 f.checkbox :task_ids, task.id
实际上应该是以下内容:f.checkbox :task_ids, {}, task.id
现在是答案的核心。虽然这种形式确实有效,并且如果有足够的摆弄,我认为您可以获得 rails 来自动更新您的项目并通过嵌套属性,并创建您想要的一切,但我认为这不是一个好的设计。
更好的设计是使用构建器 class。它只是一个 PORO(普通旧 Ruby 对象)。这将允许您围绕构建器编写良好的测试。所以您可以更加放心,它始终有效,并且对 rails 的某些更改不会破坏它。
这里有一些伪代码可以让你继续:
ProjectsController << ApplicationController
def update
@project = Project.find(params[:id])
# This should return true if everything works, and
result = ProjectMilestoneBuilder.perform(@project, update_params)
if result == false
# Something went very wrong in the builder
end
if result.errors.any?
#handle success
else
# handle failure
# The project wasn't updated, but things didn't explode.
end
end
private
def update_params
params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )
end
end
在/lib/project_milestone_builder.rb
class ProjectMilestoneBuilder
def self.perform(project, params)
milestone_params = params[:project][:milestones]
milestone_params.each do |m|
# Something like this
# Might be able to use nested attributes for this
# Milestone.create(m)
end
return project.update_attributes(params)
end
end
在/spec/lib/project_milestone_builder_spec.rb
descibe ProjectMilestoneBuilder do
# Create a template and project
let(:template) {FactoryGirl.create :template}
let(:project) {FactoryGirl.create :project, template: template}
# Create the params to update the project with.
# This will have to have dynamic code segments to get the appropriate milestone_template_ids in there
let(:params) { "{project: {milestones ..." })
descibe '#perform' do
let(:result) { ProjectMilestoneBuilder.perform(project, params) }
it {expect(result.id).to eq project.id}
# ...
end
end
使用此模式,您最终将得到一个封装良好、易于测试的 class,它将完全按照您的预期进行。编码愉快。
3/13 更新:
我用我的模型、控制器逻辑和几个表单版本做了一个小示例项目。
我正在构建一个表单,用户可以在其中添加 "Tasks" 和 "Milestones"。 (即任务 = 'Vacuum' 在里程碑 = 'clean House' 内)。它基本上是一个 Task/Subtask 类型的模型,父类是 'Milestone',子类是 'Task'。
任务和里程碑都属于 "Project"...所以我试图通过带有更新操作的嵌套表单添加任务和里程碑。我在想要走的路是为每个 @task_template 实例创建一个表单并一次更新多个表单。
我的问题是我还试图通过名为 "MilestoneTemplates" 和 "TaskTemplates" 的表动态设置 "starter milestones/tasks"...
用户打开 "Add Milestones/Task" 页面,根据他们的项目类型,他们会在复选框旁边看到一系列预建任务 (@task_templates) 和里程碑 (@milestone_templates)。然后用户选中他们想要添加的任务或里程碑旁边的复选框。这应该使用预建的@task_template.name、@task_template.description...等
我什至无法创建 1。我正在使用 Rails 4,我想我已经正确设置了我的 strong_params。以下是我的观点:
型号:
class Task < ActiveRecord::Base
belongs_to :user
belongs_to :project
belongs_to :milestone
class Milestone < ActiveRecord::Base
belongs_to :project
belongs_to :user
has_many :tasks, dependent: :destroy, inverse_of: :milestone
accepts_nested_attributes_for :tasks, allow_destroy: true
class Project < ActiveRecord::Base
has_many :milestones, dependent: :destroy
has_many :tasks, dependent: :destroy
accepts_nested_attributes_for :tasks, allow_destroy: true
accepts_nested_attributes_for :milestones, allow_destroy: true
#the "Starter Milestones & Tasks"
class MilestoneTemplate < ActiveRecord::Base
has_many :task_templates, dependent: :destroy, inverse_of: :milestone_template
class TaskTemplate < ActiveRecord::Base
belongs_to :milestone_template, inverse_of: :task_templates
控制器:
class ProjectsController < ApplicationController
def new_milestones
@project = Project.find(params[:p])
@project.milestones.build
@project.tasks.build
@milestones_templates = MilestoneTemplate.where(template_id: @project.template_id)
end
def create_milestones
@project.milestone_ids = params[:project][:milestones]
@project.task_ids = params[:project][:tasks]
@milestone = Milestone.new
@task = Task.new
@template = Template.find( @project.template_id)
if @project.update_attributes(project_params)
redirect_to view_milestones_path(p: @project.id)
flash[:notice] = "Successfully Added Tasks & Milestones"
else
redirect_to new_milestones_path(p: @project.id )
format.json { render json: @project.errors, status: :unprocessable_entity }
end
end
def project_params
params.require(:project).permit( :id, :name,
milestones_attributes: [:id, {:milestone_ids => []}, {:ids => []}, {:names => []}, :project_id, :user_id,
:name, :description, :due_date, :rank, :completed, :_destroy,
tasks_attributes: [:id, {:task_ids => []}, {:names => []}, {:ids => []}, :milestone_id, :project_id,
:user_id, :name, :description, :due_date, :rank, :completed, :_destroy]] )
end
end
表格测试 1:
<%= form_for @project, url: create_milestones_path(p: @project.id) do |f| %>
<label>Milestones</label><br>
<div class="row">
<%= hidden_field_tag "project[names][]", nil %>
<% @milestones_templates.each do |m| %>
<%= check_box_tag "project[names][]", m.name, @milestones_templates.include?(m), id: dom_id(m)%>
<%= label_tag dom_id(m), m.name %>
<%= hidden_field_tag "project[milestone][names][]", nil %>
<% m.task_templates.each do |t| %>
<%= check_box_tag "project[milestone][names][]", t.name, m.task_templates.include?(t), id: dom_id(t) %>
<%= label_tag dom_id(t), t.name %>
<% end %>
<% end %>
</div>
<%= f.submit %>
表单测试 2(尝试提交一组表单):
<label>Milestones</label><br>
<%= hidden_field_tag "project[milestone_ids][]", nil %>
<% @milestones_templates.each do |m| %>
<div>
<%= f.fields_for :milestones do |fm|%>
<%= check_box_tag "project[milestone_ids][]", @milestones_templates.include?(m), id: dom_id(m) %>
<%= label_tag dom_id(m), m.name %></div>
<%= hidden_field_tag :name, m.name %>
<%= hidden_field_tag "project[milestone][task_ids][]", nil %>
<% m.task_templates.each do |t| %>
<%= fm.fields_for :tasks do |ft| %>
<%= check_box_tag "project[milestone][task_ids][]", t.name, m.task_templates.include?(t), id: dom_id(t)%>
<%= label_tag dom_id(t), t.name %>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
根据 xcskier56 在评论中的要求,我添加了来自 Chrome 检查员的 POST 代码。如您所见,该表单甚至没有调用任务,只是调用父里程碑。里程碑显示在表单中,但任务没有....
project[formprogress]:2
project[milestone_ids][]:
project[milestone][names]:true
name:Milestone 1
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 2
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 3
project[milestone][task_ids][]:
project[milestone][names]:true
name:Milestone 4
project[milestone][task_ids][]:
这段代码我自己没法测试,但是我实现过类似的代码,所以思路应该是正确的。
这里的技巧是使用 each_with_index,然后将该索引传递给您的 fields_for
调用。这样,您通过复选框添加的每一个额外的 milestone_id
都会与之前的显着不同。你可以找到这个 here.
使用这种方法,您的表单应该如下所示:
<%= form_for @project do |f| %>
<% @milestones_templates.each_with_index do |milestone, index| %>
<br>
<%= f.fields_for :milestones, index: index do |fm| %>
<%= fm.hidden_field :name, value: milestone.name %>
<!-- Create a checkbox to add the milestone_id to the project -->
<%= fm.label milestone.name %>
<%= fm.check_box :milestone_template_id,{}, milestone.id %>
<br>
<% milestone.task_templates.each_with_index do |task, another_index| %>
<%= fm.fields_for :tasks, index: another_index do |ft| %>
<!-- Create a checkbox for each task in the milestone -->
<%= ft.label task.name %>
<%= ft.check_box :task_ids, {}, task.id %>
<% end %>
<% end %>
<br>
<% end %>
<% end %>
<br>
<%= f.submit %>
<% end %>
# Working strong parameters.
params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )
这应该输出 milestone_template_ids,每个 task_template_ids 嵌套在里面。
编辑:我忘记了,如果您查看文档,check_boxes 需要中间的另一个参数 f.checkbox :task_ids, task.id
实际上应该是以下内容:f.checkbox :task_ids, {}, task.id
现在是答案的核心。虽然这种形式确实有效,并且如果有足够的摆弄,我认为您可以获得 rails 来自动更新您的项目并通过嵌套属性,并创建您想要的一切,但我认为这不是一个好的设计。
更好的设计是使用构建器 class。它只是一个 PORO(普通旧 Ruby 对象)。这将允许您围绕构建器编写良好的测试。所以您可以更加放心,它始终有效,并且对 rails 的某些更改不会破坏它。
这里有一些伪代码可以让你继续:
ProjectsController << ApplicationController
def update
@project = Project.find(params[:id])
# This should return true if everything works, and
result = ProjectMilestoneBuilder.perform(@project, update_params)
if result == false
# Something went very wrong in the builder
end
if result.errors.any?
#handle success
else
# handle failure
# The project wasn't updated, but things didn't explode.
end
end
private
def update_params
params.require(:project).permit(:name, :milestones => [:name, :milestone_ids, :tasks => [:task_ids] ] )
end
end
在/lib/project_milestone_builder.rb
class ProjectMilestoneBuilder
def self.perform(project, params)
milestone_params = params[:project][:milestones]
milestone_params.each do |m|
# Something like this
# Might be able to use nested attributes for this
# Milestone.create(m)
end
return project.update_attributes(params)
end
end
在/spec/lib/project_milestone_builder_spec.rb
descibe ProjectMilestoneBuilder do
# Create a template and project
let(:template) {FactoryGirl.create :template}
let(:project) {FactoryGirl.create :project, template: template}
# Create the params to update the project with.
# This will have to have dynamic code segments to get the appropriate milestone_template_ids in there
let(:params) { "{project: {milestones ..." })
descibe '#perform' do
let(:result) { ProjectMilestoneBuilder.perform(project, params) }
it {expect(result.id).to eq project.id}
# ...
end
end
使用此模式,您最终将得到一个封装良好、易于测试的 class,它将完全按照您的预期进行。编码愉快。