Unknown Attribute on Rails Model for nested attributes of a hash key

237 Views Asked by At

I'm building a rails app that users get quotes on rental equipment. A user can have many quotes, but only 1 open quote. A quote can have many quote_line_items and each quote_line_item has the possibility of having 1 or many quote_line_item_components which are essentially just a self referential association through a Join Model

class Quote < ApplicationRecord
  belongs_to :user, required: false
  has_many :quote_line_items, autosave: true, inverse_of: :quote
  has_many :quote_line_item_components, through: :quote_line_items

  after_update :quote_after_update, if: :submitted_at_changed?
  validates :contact_name,            presence: true, length: { maximum: 255 }, if: :submitting_quote?
  validates :contact_phone,           presence: true, numericality: { only_integer: true }, length: { is: 10 }, if: :submitting_quote?
  validates :contact_email,           presence: true, length: { maximum: 255 }, email_format: { message: 'is not a valid email'}, if: :submitting_quote?
  validates :business_name,           presence: true, length: { maximum: 255 }, if: :submitting_quote?
  validates :pickup_date,             presence: true, if: :submitting_quote?
  validates :return_date,             presence: true, if: :submitting_quote?
  validates :job_name,                presence: true, length: { maximum: 255 }, if: :submitting_quote?

  scope :latest, -> { order(:created_at) }
  scope :submitted_quotes, -> { where.not(submitted_at: nil) }

  accepts_nested_attributes_for :quote_line_items
  accepts_nested_attributes_for :quote_line_item_components

  # If the value of the submitted_at field is changing, we're submitting this quote.
  def submitting_quote?
    submitted_at_changed?
  end

  def quote_line_items_attributes=(quote_line_items_attributes)
    quote_line_items_attributes.each do |i, quote_line_item_attributes|
      self.quote_line_items.build(quote_line_item_attributes)
    end
  end

  def add_or_update_item_quantity(**opts)
    line_item = quote_line_items.where(inventory_id: opts[:inventory_id]).first_or_initialize
    if line_item.new_record?
      line_item.assign_attributes(opts)
    elsif opts[:set_quantity].to_s =~ /true/i
      line_item.update(quantity: opts.dig(:quantity)&.to_i)
    else
      new_quantity = line_item.quantity.to_i + opts.dig(:quantity).try(:to_i)
      line_item.update(quantity: new_quantity)
    end
  end

end

Quote Line Item Model

class QuoteLineItem < ApplicationRecord
  belongs_to :quote
  has_many :quote_line_item_components, foreign_key: "quote_line_item_id", class_name: "QuoteLineItem"
  accepts_nested_attributes_for :quote_line_item_components

  validates :inventory_id, presence: true
  validates :quantity, presence: true, numericality: { integer_only: true, greater_than_or_equal_to: 1 }

  # This is a boolean/stringified boolean to tell the Quote model if we want to SET a quantity.
  # VS add to it.
  attr_accessor :set_quantity

  def reject_components(attributes)
    attributes['quantity'].to_i == 0
  end
end

The QuoteLineItemComponent

class QuoteLineItemComponent < ApplicationRecord
  belongs_to :quote_line_item, foreign_key: "quote_line_item_id", class_name: "QuoteLineItem"

  validates :inventory_id, presence: true
  validates :quantity, presence: true, numericality: { integer_only: true, greater_than_or_equal_to: 1 }
end

The form that is filled out looks like this. Using formtastic have the form referencing the current quote and the item, and if there are nested items semantic_fields_for is used.

= semantic_form_for current_quote, as: :quote, url: users_quote_path(id: :current) do |f|
  = f.inputs
    = f.semantic_fields_for(:quote_line_items, current_quote.quote_line_items.build) do |i|
      = i.input :inventory_name, as: :hidden, input_html: { value: @item.description }
      = i.input :inventory_id, as: :hidden, input_html: { value: @item.inventory_id }
      = i.input :quantity, as: :number, min: 1, step: 1,  wrapper_html: { class: "d-flex align-items-center gap-2 mb-3 formtastic-select"}, input_html: { class: "form-select form-select-sm"}
      = i.input :price, as: :hidden, wrapper_html: { class: 'mb-0'}, input_html: { value: @item.daily_rate(@item.inventory_id) }
      - if display_item_components?(@item)
        li.copy-heading Components
        - @item.parts.each do |part|
          = i.semantic_fields_for(:quote_line_item_components, i.object.quote_line_item_components.build) do |c| 
            li.mb-4
              = part.descriptions
              - if part.included?
                b * included
                span.d-block
                - if current_user 
                  = price_for_item(part)
                - else
                  | Sign in to to view prices
              - if !part.included?
                span.d-block
                  - if current_user
                    = price_for_item(part)
                  - else
                    | Sign in to view prices
                ul.p-0.mt-3
                  = c.input :quote_id, as: :hidden
                  = c.input :inventory_id , as: :hidden, input_html: { value: part.inventory_ids }
                  = c.input :inventory_name, as: :hidden, input_html: { value: part.descriptions }
                  = c.input :quantity, as: :number, min: 0, step: 1,  wrapper_html: { class: "d-flex align-items-center gap-2 formtastic-select m-0"}, input_html: { class: "form-select form-select-sm"}


  = f.actions do
    = f.action :submit, as: :button, label: "Add to Quote", button_html: { class: 'btn btn-pink hover-bg-white px-5' }, :wrapper_html => { :data => { :controller => "sidebar"} }

The opts/params submitted are as follows

Single Item

{"0"=>{"inventory_id"=>"00004OCW", "inventory_name"=>"1-TON GRIP VAN", "quantity"=>"1", "price"=>"325.0"}}

Item with Child Items

{"0"=>{"inventory_id"=>"0000SQ8J", "inventory_name"=>"JOLEKO 400 - 400 JOKER BUG-A-BEAM ELLIPSOIDAL KIT", "quantity"=>"1", "price"=>"210.0", "quote_line_item_components_attributes"=>{"3"=>{"inventory_id"=>"00004ILK", "inventory_name"=>"SOURCE FOUR 19 DEGREE BARREL", "quantity"=>"0", "quote_id"=>""}, "4"=>{"inventory_id"=>"00004ILS", "inventory_name"=>"SOURCE FOUR 26 DEGREE BARREL", "quantity"=>"0", "quote_id"=>""}, "5"=>{"inventory_id"=>"00004ILZ", "inventory_name"=>"SOURCE FOUR 36 DEGREE BARREL", "quantity"=>"0", "quote_id"=>""}, "6"=>{"inventory_id"=>"00004IM6", "inventory_name"=>"SOURCE FOUR 50 DEGREE BARREL", "quantity"=>"0", "quote_id"=>""}}}}

I had this working before I added nested items, but now the nested hashes are throwing the unknown attribute on the key "0".

Where it's failing is when it's assigning the opts here line_item.assign_attributes(opts)

def add_or_update_item_quantity(**opts)
    line_item = quote_line_items.where(inventory_id: opts[:inventory_id]).first_or_initialize
    if line_item.new_record?
      line_item.assign_attributes(opts)
    elsif opts[:set_quantity].to_s =~ /true/i
      line_item.update(quantity: opts.dig(:quantity)&.to_i)
    else
      new_quantity = line_item.quantity.to_i + opts.dig(:quantity).try(:to_i)
      line_item.update(quantity: new_quantity)
    end
  end
unknown attribute '0' for QuoteLineItem.
          raise UnknownAttributeError.new(self, k.to_s)

If anyone can point out the issue, that'd be much appreciated.

1

There are 1 best solutions below

0
manibi101 On

The name of the join table is created by using the lexical order of the class names. So a join between author and book models will give the default join table name of 'authors_books' because 'a' outranks 'b' in lexical ordering.