Demian Sims
Demian Sims

Reputation: 921

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>

Upvotes: 1

Views: 1491

Answers (2)

max
max

Reputation: 101811

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.

Upvotes: 1

jean182
jean182

Reputation: 3505

What I normally do is this (the require (:account) is very important):

def account_params
 params.require(:account).permit(
  :account_name,
  # Rest of the params you want to permit...
  authorization_ids: []
 )
end

This will allow you to do a regular save in the controller action.

def create 
 @account = Account.new(account_params) 
 if @account.save 
  flash[:notice] = "Account created successfully"
  redirect_to account_path(@account) 
 else    
  flash[:error] = @item.errors.full_messages.to_sentence
  render :new 
 end
end

And in the form If you're displaying them as checkboxes.

<% Authorization.all.each do |authorization| %>
 <%= check_box_tag 'account[authorization_ids][]', authorization.id, @account.authorizations.include?(authorization), id: dom_id(authorization), type: "checkbox" %>
<% end %>

That will iterate through all the authorization records and display it on the screen and if you select them they will be stored in the authorization_ids params, and rails is smart enough to accept those params and create the join associations in that table.

Btw this is using simple form so the code should be modified a bit to get the desired inputs

Upvotes: 1

Related Questions