DollarChills
DollarChills

Reputation: 1086

undefined method with nested attributes

I have a Trial model, which creates treatment_selection and repetition on create.

With repetition I'm looking to save the trial_id and treatment_selection_id which i'm trying to do via a :through association.

When the Trial form is submitted I getting the following error:

undefined method `repetitions' for #<TreatmentSelection:0x007ff76b8c3830>

Trial Controller

def create
    @trial = Trial.new(trial_params)
    @trial.treatment_selections.build.repetitions.build
end

private
    def trial_params
        params.require(:trial).permit(:year, :cultivation_id, :region_id, :cooperator_id, :field_id, :property_id, treatment_selections_attributes: [TreatmentSelection.attribute_names.map(&:to_sym).push(:_destroy), repetitions_attributes: [:trial_id, :treatment_selection_id]])
  end

Models

class Trial < ApplicationRecord
  has_many :treatment_selections, dependent: :destroy

  has_many :repetitions, through: :treatment_selections, source: :treatment

  accepts_nested_attributes_for :repetitions, allow_destroy: true
  accepts_nested_attributes_for :treatment_selections, allow_destroy: true
end

class TreatmentSelection < ApplicationRecord
  belongs_to :trial
  belongs_to :treatment
end

class Repetition < ApplicationRecord
  belongs_to :treatment_selection
end

The treatment_selection model has a helper as follow, which the user can add dynamically. trials_helper

module TrialsHelper
  def link_to_add_row(name, f, association, **args)
    new_object = f.object.send(association).klass.new
    id = new_object.object_id
    fields = f.fields_for(association, new_object, child_index: id) do |builder|
      render(association.to_s.singularize, f: builder)
    end
    link_to(name, '#', class: "add_fields " + args[:class], data: {id: id, fields: fields.gsub("\n", "")})
  end
end

Schema

 create_table "repetitions", force: :cascade do |t|
    t.integer "trial_id"
    t.integer "treatment_selection_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

Log

Started POST "/trials" for 127.0.0.1 at 2018-10-22 14:45:57 +1000
Processing by TrialsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"I2QheHAPOuhnv61Ghs0UoVgXcbYvX73AA7+nI/JTvRU4lTRYJbOdCstuTt/i7/4H2dV6wv4Vdt6zcQ7oo1id6w==", "trial"=>{"year(2i)"=>"1", "year(3i)"=>"1", "year(1i)"=>"2018", "region_id"=>"3", "cooperator_id"=>"1", "property_id"=>"1", "field_id"=>"5", "cultivation_id"=>"1", "treatment_selections_attributes"=>{"0"=>{"treatment_id"=>"3", "ambassador"=>"0", "_destroy"=>"false"}, "1"=>{"treatment_id"=>"", "ambassador"=>"0", "_destroy"=>"false"}, "2"=>{"treatment_id"=>"", "ambassador"=>"0", "_destroy"=>"false"}}}, "commit"=>"Submit"}
Completed 500 Internal Server Error in 65ms (ActiveRecord: 11.6ms)


NoMethodError (undefined method `repetitions' for #<TreatmentSelection:0x007ff76b8c3830>):

app/controllers/trials_controller.rb:48:in `block in create'
app/controllers/trials_controller.rb:47:in `times'
app/controllers/trials_controller.rb:47:in `create'

Trial Form

<%= form_with(model: trial, local: true) do |f| %>
    <%= f.date_select :year, {order: [:year] %>
    ... Other Trial fields...
    <%= f.fields_for :treatment_selections do |builder| %>
        <%= render 'treatment_selection', f: builder %>
    <% end %>

    <%= f.submit 'Submit' %>
<% end %>

Treatment selection partial

<%= f.collection_select :treatment_id, Treatment.order('name'), :id, :name %>
  <%= f.check_box :ambassador %>
  <%= f.hidden_field :_destroy, as: :hidden %>
  <%= link_to 'Delete', '#', class: "remove_treatment" %>

Upvotes: 0

Views: 1268

Answers (3)

max
max

Reputation: 101811

Your assocations are not quite right.

In order to join Trial to Repetition you need to setup an association in TreatmentSelection to go through:

class TreatmentSelection < ApplicationRecord
  belongs_to :trial
  belongs_to :treatment
  has_many :repetitions, dependent: :destroy
end

This lets you properly go through this assocation:

class Trial < ApplicationRecord
  has_many :treatment_selections, dependent: :destroy
  has_many :repetitions, through: :treatment_selections
end

You are also using the source option improperly. Its used when you have to tell ActiveRecord which assocation on the join model corresponds to the desired model (when the names don't match).

You should also remove the repetions.trial_id column as it creates an undesirable duplication.

You also need to add the assocations to repetitions that go back up the chain:

class Repetition < ApplicationRecord
  belongs_to :treatment_selection
  has_one :treatment, through: :treatment_selection
  has_one :trial, through: :treatment_selection
end

You also should setup the nested attributes as follows:

class Trial < ApplicationRecord
  # ...
  accepts_nested_attributes_for :treatment_selections, allow_destroy: true
end 

class TreatmentSelection < ApplicationRecord
  # ...
  accepts_nested_attributes_for :repetitions, allow_destroy: true
end

This is crucial since the attributes should be nested two levels.

{ 
   trial: {
     treatment_selection_attributes: [
        {
          treatment_id: 1,   
          repetition_attributes: [
            { foo: 'bar' }
          ]
        },
        {
          treatment_id: 1,   
          repetition_attributes: [
            { foo: 'bar' }
          ]
        }
     ]
   }
}

ActiveRecord does not understand that the nested attributes go through another assocation.

You also should nest the calls to fields_for.

<%= form_with(model: trial, local: true) do |f| %>
  <%= f.fields_for :treatment_selections do |tsf| %>
    <%= tsf.fields_for :repetitions do |rf| %>
    <% end %>
  <% end %>
<% end %>

Upvotes: 1

Mr. Perfect
Mr. Perfect

Reputation: 1

you are building repetitions object on TreatmentSelection model which does not specify relationship between TreatmentSelection and Repetition model , you need to add has_many :repetitions in TreatmentSelection model to make this working.

Upvotes: 0

neume
neume

Reputation: 186

Your code in create action @trial.treatment_selections.build will return a TreatmentSelection object which doesn't have a definition for repetitions as defined in its model.

Maybe you mean:

@trial.treatment_selections.build
@trial.repetitions.build

Upvotes: 2

Related Questions