Добавьте несколько вложенных атрибутов через флажки Rails 4 (возможно, с несколькими формами)


3/13 обновление:
Я сделал небольшой примерный проект с моими моделями, логикой контроллера и несколькими версиями формы.



Я создаю форму, в которой пользователь может добавлять "задачи" и "вехи" вместе. (т. Задача = "вакуум" находится внутри Milestone = "чистый дом"). Это в основном модель типа задачи / подзадачи, где родителем является "Milestone", а дочерним - "Task".

И задачи, и вехи относятся к "проекту".... поэтому я пытаюсь добавить задачи и вехи через вложенную форму с действием обновления. Я думаю, что нужно создать форму для каждого экземпляра @task_template и обновить сразу несколько форм.

Моя проблема заключается в том, что я также пытаюсь динамически устанавливать "стартовые вехи/задачи" через таблицы, называемые "MilestoneTemplates" и "TaskTemplates"....

Пользователь открывает страницу "Add Milestones/Task" и, в зависимости от типа проекта, видит массив готовых задач (@task_templates) & вехи (@milestone_templates) рядом с флажками. Затем пользователь устанавливает флажок рядом с задачей или вехой, которую он хотел бы добавить. Это должно создать определенную задачу для пользователя с предварительно построенным @task_template.name, @task_template.описание...и т.д.

Я не могу заставить это даже создать 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 inspector. Как вы можете видеть, форма даже не вызывает задачи, а только родительские вехи. Вехи отображаются в форме, но задачи-нет....

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][]:
1 3

1 ответ:

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

Фокус здесь заключается в использовании each_with_index, а затем передаче этого индекса вашему вызову fields_for. Таким образом, каждый дополнительный milestone_id, который вы добавляете с помощью флажка, будет значительно отличаться от предыдущего. Вы можете найти другой пример этого здесь .

Используя этот подход, ваша форма должна выглядеть примерно так:

<%= 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.

Edit: я забыл, что если вы посмотрите на документы, то check_boxes нуждаются в другом param в середине f.checkbox :task_ids, task.id => f.checkbox :task_ids, {}, task.id

Теперь перейдем к мясу ответа. Хотя эта форма действительно работает, и при достаточном количестве манипуляций я думаю, что вы могли бы заставить rails автоматически обновлять ваш проект и через вложенные атрибуты, и создавать все, что вы хотите, я не думаю, что это хороший дизайн.

Что гораздо лучший дизайн-это использование класса builder. Это просто PORO (простой старый рубиновый объект). Что это позволит вам сделать, так это написать хорошие тесты вокруг строителя. Так что вы можете быть гораздо более уверены, что он всегда будет работать, и что некоторые изменения в рельсах не сломали его.

Вот какой-то псевдокод, чтобы вы начали:

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

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