通过复选框Rails 4添加多个嵌套属性(可能有多种形式)

Posted

技术标签:

【中文标题】通过复选框Rails 4添加多个嵌套属性(可能有多种形式)【英文标题】:Add Multiple Nested Attributes through checkboxes Rails 4 (maybe with multiple forms) 【发布时间】:2015-05-09 20:24:48 【问题描述】:

3/13 更新: 我用我的模型、控制器逻辑和几个表单版本制作了一个小型示例项目。

我正在构建一个表单,用户可以在其中一起添加“任务”和“里程碑”。 (即任务='真空'在里程碑='干净的房子'内)。它基本上是一个任务/子任务类型模型,父是“里程碑”,子是“任务”。 任务和里程碑都属于“项目”......所以我试图通过具有更新操作的嵌套表单添加任务和里程碑。我认为要走的路是为每个 @task_template 实例创建一个表单并一次更新多个表单。 我的问题是我还试图通过名为“MilestoneTemplates”和“TaskTemplates”的表动态设置“启动里程碑/任务”.... 用户打开“添加里程碑/任务”页面,根据他们的项目类型,他们会在复选框旁边看到一组预构建任务(@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 在 cmets 中的请求,我从 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][]:

【问题讨论】:

发往服务器的 post 请求的结构是什么样的?查看检查器中(在 chrome 中)form-data 下的网络事件。 我已经更新了我的帖子请求。谢谢。 【参考方案1】:

我自己没能测试过这段代码,但是我实现了类似的代码,所以思路应该是对的。

这里的技巧是使用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 自动更新您的项目并通过嵌套属性,并创建您想要的所有内容,但我认为这不是一个好的设计。

更好的设计是使用构建器类。它只是一个 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

使用这种模式,您最终会得到一个封装良好、易于测试的类,该类将完全按照您的预期进行。编码愉快。

【讨论】:

我有一个未定义的方法 `merge' 用于 18:Fixnum 错误....但是我已经使用您的代码和种子数据制作了一个示例应用程序。我还在应用程序中制作了 2 个版本的表单,一个带有 :milestone_ids,一个带有 project[milestone_ids][],它是:github.com/e-wing/sample-task-app 我一直在使用您的代码,我对构建器感到非常兴奋......但是我无法打开表单而不出现错误......当我把更新的task_ids => 在那里。错误显示“#<0x007fbff836a410>

以上是关于通过复选框Rails 4添加多个嵌套属性(可能有多种形式)的主要内容,如果未能解决你的问题,请参考以下文章

使用带有 Rails 的改革 gem,我如何填充 has_many :通过嵌套模型

Rails 6嵌套形式茧不会保存子对象

带有has_many的Rails嵌套表单:通过,如何编辑连接模型的属性?

我的 rails 嵌套模型无法识别 _destroy 属性

Rails:用按钮隐藏记录,找不到ID错误的记录

带有has_many的模型中的属性总和:通过关联在Rails 5中不起作用