Search code examples
ruby-on-railsnested-attributesstrong-parameters

Unpermitted parameters in Rails strong params from related Model on create


I'm using a form in Rails to create an instance of one Model and create a join table to another model on create. In my Accounts controller, after verifying the new instance of Account has been created, I create a new join table based on checkboxes chosen in the form from another model.

Here is my code:

Models:

class Account < ApplicationRecord
    has_many :account_authorizations
    has_many :authorizations, through: :account_authorizations
    accepts_nested_attributes_for :authorizations
end
class Authorization < ApplicationRecord

    has_many :account_authorizations
    has_many :accounts, through: :account_authorizations
end
class AccountAuthorization < ApplicationRecord
    belongs_to :account 
    belongs_to :authorization 
end

My Accounts controller:

class AccountsController < ApplicationController
  before_action :find_account, only: [:show, :update, :destroy]

  def index
    @accounts = Account.all

  end

  def show 
  end

  def new 
     @authorizations = Authorization.all
  end

  def create 
    byebug 
    @account = Account.new(account_params) 
    if @account.save 
      authorization_ids = params.permit![:authorization][:ids]
      authorization_ids.each do |auth_id| 
      AccountAuthorization.create(authorization_id: auth_id, account_id: params[:id]) if !auth_id.empty?
      end
      flash[:notice] = "Account created sucsessfully"
      redirect_to account_path(@account) 
    else    
      flash[:error] = @item.errors.full_messages.to_sentence
      render :new 
    end
  end

  def edit   
  end

  def update
     @account = Account.update(account_params) 
    if @account.valid?
      flash[:notice] = "Account created sucsessfully"
      redirect_to accounts_path(@account) 
    else    
      flash[:error] = @item.errors.full_messages.to_sentence
      render :edit 
    end
  end

  def destroy
    @account.delete
    redirect_to accounts_path
  end



private 

def find_account 
  @account = Account.find(params[:id])
end

def account_params
    params.permit(
      :account_name,
      :account_address_line1,
      :account_address_line2,
      :account_city,
      :account_state,
      :account_zipcode,
      :account_ppu,
      :account_notes,
      :contact_first_name,
      :contact_last_name,
      :contact_phone,
      :contact_email
    )
  end

end 

I've tried using .permit! on params but this does not allow the the ids from Authorization to pass into a new join instance of AccountAuthorization.

The attributes that don't pass are:

Unpermitted parameters: :authenticity_token, :authorization, :commit

I've also tried putting those in the permit hash in the strong_params method.

** UPDATE **

My Views page with form:

<% states = [ 'AL', 'AK', 'AS', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DC', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MH', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'MP', 'OH', 'OK', 'OR', 'PA' 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT' 'VA', 'WA', 'WV', 'WI', 'WY'] %>
<% ppu = ['Employment', 'Insurance', 'Law Enforement'] %>

<div class="row justify-content-center">
    <div class="col-lg-16">
        <h3>Create New Account</h3>
    </div>
</div>
<div class="row justify-content-center">
    <div class="col-lg-16">

            <%= form_with(url: accounts_path, model: @account, local: true) do |f| %>

            <%= f.text_field :account_name, class: "form-control", placeholder: "Account Name" %>
            <%= f.text_field :account_address_line1, class: "form-control", placeholder: "address line 1" %>
            <%= f.text_field :account_address_line2, class: "form-control", placeholder: "address line 2" %>
            <%= f.text_field :account_city, class: "form-control", placeholder: "account city" %>
            <%= f.select :account_state, options_for_select(states), { :prompt => true }, class: "form-control", include_blank: true, placeholder: "state" %>
            <%= f.text_field :account_zipcode, class: "form-control", placeholder: "account zipcode" %>
            <%= f.text_field :contact_first_name, class: "form-control", placeholder: "contact first name" %>
            <%= f.text_field :contact_last_name, class: "form-control", placeholder: "contact last name" %>
            <%= f.text_field :contact_phone, class: "form-control", placeholder: "conact phone" %>
            <%= f.text_field :contact_email, class: "form-control", placeholder: "contact email" %>
            <%= f.select :account_ppu, options_for_select(ppu), { :prompt => true }, class: "form-control", include_blank: true, placeholder: "ppu" %>
            <%= f.text_area :account_notes, class: "form-control", placeholder: "Notes..." %>

            <div class="d-flex justify-content-between flex-wrap">
             <%= f.fields_for :authorization do |auth| %>
            <div class="order-3 p-2 bd-highlight">
                <%= auth.collection_check_boxes :ids, Authorization.all, :id, :auth_name %>
            </div>
            <% end %>
             </div>
            <%= f.submit "Create", class: 'btn btn-success' %>
            <% end %>



    </div>
</div>

Solution

  • You don't need to manually create join table records.

    For every has_many and has_many_and_belongs_to_many association Rails creates a _ids setter that takes an array of ids. Rails will automatically create/delete join table rows from the input.

    This mates up perfectly with the collection helpers.

    app/views/accounts/_form.html.erb

    <%= form_with(model: account, local: true) do |f| %>
      <div class="field">
         <%= f.label :account_name %>
         <%= f.text_field :account_name %>
      </div>
      # ... more inputs
    
      <div class="field">
        <%= f.label :authorization_ids, 'Authorizations' %>
        <%= f.collection_checkboxes :authorization_ids, 
                                    Authorization.all, # the collection 
                                    :id, # value method
                                    :name # label method
        %>
      </div>
    
      <div class="actions">
        <%= f.submit %>
      <div>
    <% end %>
    

    app/views/accounts/new.html.erb

    <%= render partial: 'form', account: @account %>
    

    accounts/edit.html.erb

    <%= render partial: 'form', account: @account %>
    

    app/controllers/accounts_controller.rb

    class AccountsController < ApplicationController
      before_action :find_account, only: [:show, :edit, :update, :destroy]
    
      # ...
    
      def create 
        @account = Account.new(account_params) 
        if @account.save 
          flash[:notice] = "Account created sucsessfully"
          redirect_to @account 
        else 
          # render :new does not redirect so you need to use flash.now
          # to display the flash message in this request
          flash.now[:error] = @item.errors.full_messages.to_sentence
          render :new 
        end
      end
    
      def edit   
      end
    
      # This is how you update a resource.
      def update
        # don't check .valid? - it just tells you if the validations passed
        # not if the actual DB update query was a successes
        if @account.update(account_params)
          flash[:notice] = "Account updated successfully"
          redirect_to @account
        else    
          flash.now[:error] = @item.errors.full_messages.to_sentence
          render :edit 
        end
      end
    
      # ...
    
      private 
    
      def account_params
        params.require(:account).permit(
          :account_name,
          :account_address_line1,
          :account_address_line2,
          :account_city,
          :account_state,
          :account_zipcode,
          :account_ppu,
          :account_notes,
          :contact_first_name,
          :contact_last_name,
          :contact_phone,
          :contact_email,
          account_ids: []
        )
      end
    end
    

    Since this slices out the key :account from the params hash you won't get the unpermitted parameters error. authenticity_token is the Rails CSRF protection token and commit is the value of the submit button which was clicked to submit the form. These should not be whitelisted or sent to your model

    You whitelist the an array of authorization ids by passing a keyword argument (account_ids: []) to .permit with an empty array as the value. Since this is a keyword argument it must come after the list of positional arguments.

    You also don't need accepts_nested_attributes_for unless you're doing something more advanced then just selecting existing records. But you're not quite ready for accepts_nested_attributes_foruntil you have got the basics of doing CRUD down.