Простая форма с пропуском has_many и присоединением к таблице

#ruby-on-rails #join #nested-attributes #has-many-through #cocoon-gem

#ruby-on-rails #Присоединиться #вложенные атрибуты #имеет много сквозных #cocoon-драгоценный камень

Вопрос:

У меня есть модели: Product, Parts и ProductParts

product.rb

 class Product < ApplicationRecord
  has_many :product_parts
  has_many :parts, through: :product_parts
  accepts_nested_attributes_for :product_parts, allow_destroy: true
end
  

part.rb

 class Part < ApplicationRecord
  has_many :product_parts
  has_many :products, through: :product_parts
end
  

product_part.rb

 class ProductPart < ApplicationRecord
  belongs_to :part
  belongs_to :product
end
  

Продукт может состоять из нескольких частей, например, product1 = 1 шт. часть 2 и 3 шт. часть 3.

У меня есть таблица объединения: product_parts

   create_table "product_parts", force: :cascade do |t|
    t.integer "part_id"
    t.integer "product_id"
    t.integer "quantity"
    t.index ["part_id"], name: "index_product_parts_on_part_id"
    t.index ["product_id"], name: "index_product_parts_on_product_id"
  end
  

Я хотел бы отобразить простую форму для нового продукта, где:

  • Я могу ввести название продукта
  • выберите детали (из всех деталей) и определите количество каждой детали

У меня есть _form.html.slim вот так:

 = simple_form_for(@product) do |f|
  = f.error_notification
  = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present?
  = f.input :name
  = f.simple_fields_for :parts do |part|
    = f.label :part.name
    = f.input :part.quantity
  = f.button :submit
  

но это ничего не отображает. Есть ли у вас какие-либо советы, как это сделать правильно?

Я также пытался сделать это следующим образом:

_form.html.slim

 = simple_form_for(@product) do |f|
  = f.input :name
  = f.simple_fields_for :parts do |part|
    == render 'part_fields'
  = f.button :submit
  

_part_fields.html.slim

 = :name
= part.input :quantity
  

Вывод журнала сервера:

 17:34:21 web.1       | Started GET "/products/new" for ::1 at 2020-08-24 17:34:21  0200
17:34:21 web.1       | Processing by ProductsController#new as HTML
17:34:21 web.1       |   Rendering products/new.html.slim within layouts/application
17:34:21 web.1       |   Part Load (0.1ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   CACHE Part Load (0.0ms)  SELECT "parts".* FROM "parts" LIMIT ?  [["LIMIT", 11]]
17:34:21 web.1       |   ↳ app/views/products/_form.html.slim:6
17:34:21 web.1       |   Rendered products/_part_fields.html.slim (Duration: 442.5ms | Allocations: 233999)
17:34:21 web.1       |   Rendered products/_form.html.slim (Duration: 449.1ms | Allocations: 240331)
17:34:21 web.1       |   Rendered products/new.html.slim within layouts/application (Duration: 451.5ms | Allocations: 242361)
17:34:21 web.1       | Completed 500 Internal Server Error in 454ms (ActiveRecord: 0.4ms | Allocations: 243658)
17:34:21 web.1       |
17:34:21 web.1       |
17:34:21 web.1       |
17:34:21 web.1       | ActionView::Template::Error - undefined local variable or method `part' for #<#<Class:0x00007f938cd03f28>:0x00007f938cd020b0>
17:34:21 web.1       | Did you mean?  @parts:
17:34:21 web.1       |   app/views/products/_part_fields.html.slim:2:in `view template'
  

Спасибо всем за ваши ответы. Я нашел образец на GitHub:
rails-рецепты. Теперь я использую cocoon gem и я почти там, где я хочу быть 🙂 но:

_form.html.slim

 = simple_form_for(@product) do |f|
  = f.input :name
  |Parts
  fieldset#parts
    = f.simple_fields_for :product_parts do |product_part|
      == render 'product_part_fields', f: product_part
      = link_to_add_association 'Add part', f, :product_parts, class: 'btn btn-primary btn-xs'
    = f.button :submit
  

_product_part.html.slim:

 .nested_fields.field
  = f.input :quantity
  = f.collection_select :part_id, Part.all, :id, :name
  = link_to_remove_association f
  

Когда форма отображается, у меня уже есть две части входных данных вместо одной (это должно быть правильно).
Я не хочу создавать новую деталь, просто выберите детали из списка и вставьте только количество.
Это выглядит следующим образом:
скриншот отображаемой формы

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

 12:58:14 web.1       | Started POST "/products" for ::1 at 2020-08-28 12:58:14  0200
12:58:14 web.1       | Processing by ProductsController#create as HTML
12:58:14 web.1       |   Parameters: {"authenticity_token"=>"dLVq3gUUeHnmFgVxQ7gXATOF3wdQ75a/csjmjzYN6HFrr9duoi50M4KzrSUNj/QHCrJgXO2myJPdpkybdTB0cA==", "product"=>{"name"=>"Product #1", "product_parts_attributes"=>{"0"=>{"quantity"=>"4", "part_id"=>"134", "_destroy"=>"false"}, "1"=>{"quantity"=>"5", "part_id"=>"168", "_destroy"=>"false"}, "2"=>{"quantity"=>"", "part_id"=>"266", "_destroy"=>"false"}, "3"=>{"quantity"=>"", "part_id"=>"260", "_destroy"=>"false"}, "4"=>{"quantity"=>"", "part_id"=>"262", "_destroy"=>"false"}, "5"=>{"quantity"=>"", "part_id"=>"259", "_destroy"=>"false"}}}, "commit"=>"Create Product"}
12:58:14 web.1       |    (0.2ms)  begin transaction
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
12:58:14 web.1       |   Part Load (0.2ms)  SELECT "parts".* FROM "parts" WHERE "parts"."id" = ? LIMIT ?  [["id", 134], ["LIMIT", 1]]
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
12:58:14 web.1       |   Part Load (0.1ms)  SELECT "parts".* FROM "parts" WHERE "parts"."id" = ? LIMIT ?  [["id", 168], ["LIMIT", 1]]
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
12:58:14 web.1       |   Part Load (0.1ms)  SELECT "parts".* FROM "parts" WHERE "parts"."id" = ? LIMIT ?  [["id", 266], ["LIMIT", 1]]
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
12:58:14 web.1       |   Part Load (0.1ms)  SELECT "parts".* FROM "parts" WHERE "parts"."id" = ? LIMIT ?  [["id", 260], ["LIMIT", 1]]
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
12:58:14 web.1       |   Part Load (0.1ms)  SELECT "parts".* FROM "parts" WHERE "parts"."id" = ? LIMIT ?  [["id", 262], ["LIMIT", 1]]
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
12:58:14 web.1       |   Part Load (0.1ms)  SELECT "parts".* FROM "parts" WHERE "parts"."id" = ? LIMIT ?  [["id", 259], ["LIMIT", 1]]
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
12:58:14 web.1       |    (0.2ms)  rollback transaction
12:58:14 web.1       |   ↳ app/controllers/products_controller.rb:29:in `block in create'
  

Что я делаю не так с этим?:

  • Мне нужно выбрать две части вместо одной в новой отображаемой форме
  • Продукты и products_parts не могут быть созданы

Комментарии:

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

2. Я обновил вопрос для с вашим ask

Ответ №1:

Я понял это!:) Я помещаю рабочий код ниже, если у кого-то возникнет такая же проблема.

product.rb

 class Product < ApplicationRecord
  has_many :product_parts
  has_many :parts, through: :product_parts
  accepts_nested_attributes_for :parts, reject_if: blank?, allow_destroy: false
  accepts_nested_attributes_for :product_parts, allow_destroy: true
end
  

product_part.rb

 class ProductPart < ApplicationRecord
  belongs_to :part
  belongs_to :product

  accepts_nested_attributes_for :part, reject_if: all_blank
end
  

part.rb

 class Part < ApplicationRecord
  has_many :product_parts
  has_many :products, through: :product_parts
end
  

products_controller.rb

 # GET /products/new
def new
  @product = Product.new
  @product.product_parts.build.build_part
end
  

продукты/_form.html.slim

 = simple_form_for(@product) do |f|
  = f.error_notification
  = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present?
  = f.input :name, label: false, class: 'form-control', placeholder: t('placeholders.input_product_name')
  fieldset#parts
    = f.simple_fields_for :product_parts do |product_part|
      .field
        == render 'product_part_fields', f: product_part
    = link_to_add_association 'Add part', f, :product_parts, class: 'btn btn-primary btn-xs'
    = f.button :submit 
  

продукты/_product_part_fields.html.slim

 .nested-fields.field
  .field.has-addons
    = link_to_remove_association f
    .field
      = f.input :quantity
      = f.collection_select :part_id, Part.all, :id, :name, {}, {class: 'select2'}
  

Ответ №2:

Конец журналов вашего сервера показывает, что ваш partial ищет переменную с именем ‘part’ и выдает ошибку 500.

Измените ваш _form.html.slim файл на:

 = simple_form_for(@product) do |f|
  = f.input :name
  = f.simple_fields_for :parts do |part|
    == render 'part_fields', part: part
  = f.button :submit
  

Это отправит объект part в partial и устранит ошибку.

Комментарии:

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

Ответ №3:

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

Итак, чтобы отображать входные данные для новых записей, вам нужно заполнить ассоциацию в вашем контроллере:

 class ProductsController < ApplicationController
  # ...

  # GET /products/new
  def new
    @product = Product.new do |p|
      3.times { p.parts.new } 
    end
  end

  # ...
end 
  

Также при вашей первой попытке в форме вы используете символ, :part который вызовет NoMethodError (undefined method 'name' for :part:Symbol) .

 = f.simple_fields_for :parts do |part|
  = f.label :part.name
  = f.input :part.quantity
  

Она должна гласить:

 = f.simple_fields_for :parts do |part|
  = f.label part.name
  = f.input part.quantity
  

И @marks answer устраняет недостающий локальный при вашей второй попытке.