Absolutely stuck trying to create nested association in rails form with has_many through

11,808

Instead of calling:

@production = @miniature.productions.create

Try Rails' "build" method:

def new
  @miniature = Miniature.new(miniature_params)
  @miniature.productions.build
end

def create
  @miniature = Miniature.new(miniature_params)
  if @miniature.save
    redirect_to @miniature
  else
    render 'new'
  end
end

Using the build method uses ActiveRecord's Autosave Association functionality.

See http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html

You also need to update your params method, e.g.

def miniature_params
  params.require(:miniature).permit(:name, :release_date, :material, :scale, productions_attributes: [:manufacturer_id])
end

Also your fields_for should be plural (I think)...

<%= f.fields_for :productions do |production_fields| %>
Share:
11,808
Ossie
Author by

Ossie

Stop being a waiter, get a meal yourself.

Updated on June 18, 2022

Comments

  • Ossie
    Ossie almost 2 years

    I posted an earlier question about this and was advised to read lots of relevant info. I have read it and tried implementing about 30 different solutions. None of which have worked for me.

    Here's what I've got.

    I have a Miniatures model. I have a Manufacturers model. Miniatures have many manufacturers THROUGH a Productions model.

    The associations seem to be set up correctly as I can show them in my views and create them via the console. Where I have a problem is in letting the Miniatures NEW and EDIT views create and update to the Productions table.

    In the console the command @miniature.productions.create(manufacturer_id: 1) works, which leads me to believe I should be able to do the same in a form.

    I THINK my problem is always in the Miniatures Controller and specifically the CREATE function. I have tried out a ton of other peoples solutions there and none have done the trick. It is also possible that my field_for stuff in my form is wrong but that seems less fiddly.

    I've been stuck on this for days and while there are other things I could work on, if this association isn't possible then I'd need to rethink my entire application.

    The form now creates a line in the Productions table but doesn't include the all important manufacturer_id.

    Any help VERY much appreciated.

    My New Miniature form

    <% provide(:title, 'Add miniature') %>
    <h1>Add a miniature</h1>
    
    <div class="row">
      <div class="span6 offset3">
        <%= form_for(@miniature) do |f| %>
          <%= render 'shared/error_messages', object: f.object %>
          <%= f.label :name %>
          <%= f.text_field :name %>
           <%= f.fields_for :production do |production_fields| %>
          <%= production_fields.label :manufacturer_id, "Manufacturer" %>
          <%= production_fields.select :manufacturer_id, options_from_collection_for_select(Manufacturer.all, :id, :name) %>
          <% end %>
          <%= f.label :release_date %>
          <%= f.date_select :release_date, :start_year => Date.current.year, :end_year => 1970, :include_blank => true %>
    
          <%= f.submit "Add miniature", class: "btn btn-large btn-primary" %>
        <% end %>
      </div>
    </div>
    

    Miniatures controller

    class MiniaturesController < ApplicationController
       before_action :signed_in_user, only: [:new, :create, :edit, :update]
       before_action :admin_user,     only: :destroy
    
      def productions
        @production = @miniature.productions
      end
    
      def show
        @miniature = Miniature.find(params[:id])
      end
    
      def new
        @miniature = Miniature.new 
      end
    
      def edit
        @miniature = Miniature.find(params[:id])
      end
    
      def update
        @miniature = Miniature.find(params[:id])
        if @miniature.update_attributes(miniature_params)
          flash[:success] = "Miniature updated"
          redirect_to @miniature
        else
          render 'edit'
        end
      end
      def index
        @miniatures = Miniature.paginate(page: params[:page])
      end
    
      def create
        @miniature = Miniature.new(miniature_params)
        if @miniature.save
          @production = @miniature.productions.create
          redirect_to @miniature
        else
          render 'new'
        end
      end
    
      def destroy
        Miniature.find(params[:id]).destroy
        flash[:success] = "Miniature destroyed."
        redirect_to miniatures_url
      end
    
    private
        def miniature_params
          params.require(:miniature).permit(:name, :release_date, :material, :scale, :production, :production_attributes)
        end
    
        def admin_user
          redirect_to(root_url) unless current_user.admin?
        end
    
        def signed_in_user
          unless signed_in?
            store_location
            redirect_to signin_url, notice: "Please sign in."
          end
        end
    end
    

    Miniature model

    class Miniature < ActiveRecord::Base
      has_many :productions, dependent: :destroy
      has_many :manufacturers, :through => :productions
      accepts_nested_attributes_for :productions
    
        validates :name, presence: true, length: { maximum: 50 }
        validates :material, presence: true
        validates :scale, presence: true
        validates_date :release_date, :allow_blank => true
    
      def name=(s)
        super s.titleize
      end
    
    end
    

    Production model

    class Production < ActiveRecord::Base
        belongs_to :miniature
        belongs_to :manufacturer
    
    
    
    end
    

    Manufacturer model

    class Manufacturer < ActiveRecord::Base
        has_many :productions
        has_many :miniatures, :through => :productions
        validates :name, presence: true, length: { maximum: 50 }
        accepts_nested_attributes_for :productions
    end
    
  • Ossie
    Ossie over 10 years
    Yeah that allows me to shift the line up above the "if @miniature save" line but it works just the same. I get a new line in the table but it doesn't contain the manufacturer_id. It must either be that I need to compose my form differently or I need to force the controller to use that particular parameter. Oof.
  • Ossie
    Ossie over 10 years
    I'd tried various combinations in the params bit. This looks great but still doesn't pass the manufacturer_id to productions. It is very possible right?
  • Helios de Guerra
    Helios de Guerra over 10 years
    Can you tell from your log if its passing the parameter, but it is being filtered out? Or is it not passing it at all? Sorry I haven't been too much help, I try not to use nested forms in favor of using form objects... Check out pattern #3 on Form Objects, here: blog.codeclimate.com/blog/2012/10/17/…
  • Ossie
    Ossie over 10 years
    Interesting. The log has manufacturer_id under Paramters but before 'begin transaction' it gives 'Unpermitted parameters: production'. So then only reads 'INSERT INTO "productions" ("created_at", "miniature_id", "updated_at") VALUES (?, ?, ?) [["created_at", Mon, 07 Oct 2013 17:44:55 UTC +00:00], ["miniature_id", 89], ["updated_at", Mon, 07 Oct 2013 17:44:55 UTC +00:00]]'. So it isn't passing it. I just can't understand why it plays nice in console but not in forms. Had no idea there was alternative to nested forms. Will investigate. This ought to be workable though right?
  • Ossie
    Ossie over 10 years
    After some toggling of production/productions in the form I can report that in plural it doesn't see the manufacturer_id param at all and thusly doesn't throw up the 'Unpermitted parameters: production' error. Probably not progress? Found this [link] (karolgalanciak.com/blog/2013/10/06/…) also reccomending Form Objects but bit daunted. No step by step guides. Thanks so much by the way.
  • Helios de Guerra
    Helios de Guerra over 10 years
    What do your request params look like in your log w/ the fields_for productions being plural?
  • Helios de Guerra
    Helios de Guerra over 10 years
    Also, try production_attributes in the miniature params permit method instead of just productions... see latest edit
  • Ossie
    Ossie over 10 years
    I switched to production_attributes: and that works just the same. If I try productions plural in the fields_for, my dropdown doesn't appear at all so can't check the log. So I'm back to having 'Unpermitted parameters: production' before 'begin transaction' in the log. Head against wall.
  • Helios de Guerra
    Helios de Guerra over 10 years
    I think you definitely want productions plural in the fields_for. I think its some small issue that we're missing... Maybe the select isn't being properly formed or something. You might try replacing the select :manufacturer_id with a number_field and just type in the id and see what you get. Also, have you reviewed the Rails guide, guides.rubyonrails.org/form_helpers.html#building-complex-fo‌​rms. If all else fails, maybe post a Gist of your current controller and views and I'll work thru it. We're making this harder than it should be somewhere along the line.
  • Ossie
    Ossie over 10 years
    Well productions plural loses my select. I'll have another look through the Rails Guide. The current branch is here github.com/WorldOfProper/mini_legions/tree/manufacturers but without that plural right now.
  • Helios de Guerra
    Helios de Guerra over 10 years
    Alright, I got it working for me... The reason you were losing your select, was that no production was initialized for a new Miniature instance... @miniature = Miniature.new needed to be followed by, @miniature.productions.build... See latest edit
  • Ossie
    Ossie over 10 years
    OHMIGOD that worked! You're a genius. The addition of the new productions meant that the preductions plural DID show the select and it worked. It works! This means I can go on with my whole project without starting again. You are my hero Helios de Guerra.
  • Helios de Guerra
    Helios de Guerra over 10 years
    :-) Glad to help. Those little things can be infuriating... Good luck w/ the rest of the project.