2

Question

For a nested model, how do I create multiple copies of it, as specified in it's quantity attribute.

Usecase

This is especially done when making invoices. If your invoice is a model, and the line items are nested models with a quantity field. How do you save n copies of the line-items in the databases, where n is given in the quantity field?

Details

I have two models:

class PurchaseOrder < ApplicationRecord
  has_many :containers
  accepts_nested_attributes_for :containers, reject_if: :all_blank, allow_destroy: true
end

class Container < ApplicationRecord
  belongs_to :purchase_order
end

For my PurchaseOrdersController#create action, the params it receives are in the form:

params = {
  purchase_order: ...
  containers_attributes: [
     <container>:
        size: ...
        price: ...
        quantity: ...

  }

if i get a container_attribute in the form:

{
  size: 'large'
  price: 20
  quantity: 3
}

I need to create 3 containers of the form:

{
  size: 'large'
  price: 20
}

Essentially, how do I mutate the params without running into:

DEPRECATION WARNING: Method to_a is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from hash.

Or, if mutating params is a bad idea, how do I edit container attributes before they're saved!

Amin Shah Gilani
  • 8,675
  • 5
  • 37
  • 79

1 Answers1

0

Okay, the solution turned out to be slightly more complicated, especially because of some changes in Rails 5.

Model

  1. Add accepts_nested_attributes_for in your parent Model
  2. Because of the way Rails 5 handles belongs_to insert inverse_of: in both parent and child
  3. Add a validation of the parent to the child, presence: true

Optionally: if you're using simple form, add an attr_reader to your model to make getting the quantity easier.

class PurchaseOrder < ApplicationRecord
  belongs_to :supplier
  has_many :containers, inverse_of: :purchase_order
  accepts_nested_attributes_for :containers, reject_if: :all_blank, allow_destroy: true
end

class Container < ApplicationRecord
  belongs_to :purchase_order, inverse_of: :containers
  validates :purchase_order, presence: true

  attr_reader :quantity
end

Controller

What I did was:

  1. Build the Parent model with its own attributes
  2. Converted the child's attributes into a hash and mutated the hash into the state I needed
  3. Passed the attributes through parent.children_attributes=
  4. Called parent.save

Final code:

  def create
    @purchase_order = PurchaseOrder.new purchase_order_params
    @purchase_order.containers_attributes = filtered_containers
    if @purchase_order.save
      flash[:alert] = 'Purchase order successfully created!'
      return redirect_to purchase_order_path(@purchase_order)
    end
    flash[:alert] = "Error, #{@purchase_order.errors.full_messages.join(', ')}"
    redirect_to new_purchase_order_path
  end

  private

  def filtered_params
    params.require(:purchase_order).permit(
      :supplier_id,
      containers_attributes: %i(length height width colour note mods done _destroy quantity)
    )
  end

  def purchase_order_params
    filtered_params.permit :supplier_id
  end

  def filtered_containers
    multiple_by_quantity = proc do |e|
      x = e.delete('quantity').to_i
      [e] * x
    end

    filtered_params
      .dig(:containers_attributes)
      .to_h
      .values
      .map(&multiple_by_quantity)
      .flatten
  end

Applicability

If you're building a POS app, your parent would be Invoice and the children would be Item or LineItem

For my usecase, the parent was PurchaseOrder and the child was Container

Amin Shah Gilani
  • 8,675
  • 5
  • 37
  • 79