Grimhamr
Grimhamr

Reputation: 41

Where is my "unpermitted parameter: :passenger" Rails error coming from?

I am building a flight booking app with Rails that lets you select airports, date and number of passengers. Once you select the airports and dates, it gives you radio buttons to select which flight you want and, once you click submit, you are taken to a booking confirmation page where you are asked to provide passenger info.

The confirmation page(bookings#new) has a nested form to include passengers in the booking object. To do this, I have first set the following models and associations:

class Passenger < ApplicationRecord

    belongs_to :booking

end
class Booking < ApplicationRecord

    belongs_to :flight
    has_many :passengers 

    accepts_nested_attributes_for :passengers 

    
end

And the relevant migrations that result in the following schema tables:


  create_table "bookings", force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.integer "flight_id"
    t.integer "passenger_id"
  end
  create_table "passengers", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.integer "booking_id"
    t.index ["booking_id"], name: "index_passengers_on_booking_id"
  end

From what I understand, the flow goes like this: User selects flight -> User submits flight, goes to Booking#new through the #new method on my controller:

class BookingsController < ApplicationController

    def new
        @booking = Booking.new
        @flight = Flight.find(params[:flight_id]) 
        params[:passengers_number].to_i.times do #params passed from select flight page
            @booking.passengers.build
            end
    end

Then, the form I built takes over on new.html.erb:

 <%= form_with model: @booking, url: bookings_path do |f| %>
            <%= f.hidden_field :flight_id, value: @flight.id %>

            <% @booking.passengers.each_with_index do |passenger, index| %>
                <%= f.fields_for passenger, index: index do |form| %>

                <h4><%= "Passenger #{index+1}"%> <br> </h4>

                    <%= form.label :name, "Full name:" %>
                    <%= form.text_field :name %>

                    <%= form.label :email, "Email:" %>
                    <%= form.email_field :email %>

             <% end %>
        <% end %>

            <%= f.submit "Confirm details"%>
        <% end %>

I fill it in with names and emails, click 'Confirm details' and I get this error on my terminal:

Unpermitted parameter: :passenger

Why? I have set accepts_nested_attributes_for :passengers on my Booking model, my booking_params method is:

 def booking_params
        params.require(:booking).permit(:flight_id,
            :passengers_attributes => [:name, :email, :passenger_id, :created_at, :updated_at])
      end

and my #create method is:

 def create
        @booking = Booking.new(booking_params)

        respond_to do |format|
            if @booking.save
              format.html { redirect_to @booking, notice: "booking was successfully created." }
              format.json { render :show, status: :created, location: @booking }
            else
                format.html { redirect_to root_path,  alert: "booking failed, #{@booking.errors.full_messages.first}" , status: :unprocessable_entity }
                format.json { render json: @booking.errors, status: :unprocessable_entity }
            end
          end
    end

Is there something I am not permitting properly? Note that if I set booking_params as params.require(:booking).permit! it gives me an unknown attribute 'passenger' for Booking error. But I have defined associations and database on passenger and booking, at least to my knowledge.

Thanks in advance

Edit: The server log that generates the error is:

Started POST "/bookings" for ::1 at 2021-08-27 17:52:17 +0300
Processing by BookingsController#create as HTML
  Parameters: {"authenticity_token"=>"[FILTERED]", "booking"=>{"flight_id"=>"5", "passenger"=>{"0"=>{"name"=>"Jason Smason", "email"=>"[email protected]"}, "1"=>{"name"=>"Joe Smith", "email"=>"[email protected]"}}}, "commit"=>"Confirm details"}
Unpermitted parameter: :passenger

Edit2: I followed PCurell's advice and changed my fields_for to <%= f.fields_for :passengers, passenger, index: index do |form| %> and my booking params to :passenger => [:name, :email, :passenger_id, :created_at, :updated_at])

That generated a different error: Unpermitted parameter: passengers_attributes'. So I changed my controller's booking_params` to:

def booking_params 
        params.require(:booking).permit(:flight_id, 
            :passengers_attributes => [:name, :email, :passenger_id, :created_at, :updated_at],
            :passenger => [:name, :email, :passenger_id, :created_at, :updated_at])
end

With that, I successfully managed to create a booking with 2 passengers. However, the passengers are blank; their name and email are nil. The log says:

Parameters: {"authenticity_token"=>"[FILTERED]", "booking"=>{"flight_id"=>"1", "passengers_attributes"=>{"0"=>{"0"=>{"name"=>"John Smith", "email"=>"[email protected]"}}, "1"=>{"1"=>{"name"=>"Burger King", "email"=>"[email protected]"}}}}, "commit"=>"Confirm details"}
Unpermitted parameter: :0
Unpermitted parameter: :1

I might be able to hardcode :0 and :1 to pass, but surely that's not the Rails way. Is there a way to dynamically let them in? Or am I doing the whole thing wrong?

Upvotes: 0

Views: 235

Answers (2)

3limin4t0r
3limin4t0r

Reputation: 21130

Your usage of fields_for is incorrect. When you look at the example in the guide you will notice that there is no need to wrap it with an .each if used for a collection.

10.2 Nested Forms

The following form allows a user to create a Person and its associated addresses.

<%= form_with model: @person do |form| %>
  Addresses:
  <ul>
    <%= form.fields_for :addresses do |addresses_form| %>
      <li>
        <%= addresses_form.label :kind %>
        <%= addresses_form.text_field :kind %>

        <%= addresses_form.label :street %>
        <%= addresses_form.text_field :street %>
        ...
      </li>
    <% end %>
  </ul>
<% end %>

When an association accepts nested attributes fields_for renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 2 sets of address fields being rendered on the new person form.

def new
  @person = Person.new
  2.times { @person.addresses.build }
end

When applying this to your code, removing the .each wrapper and changing passenger into :passengers should do the trick. You can access the index through the FormBuilder instance (form) passed to the fields_for block.

<%= form_with model: @booking, url: bookings_path do |f| %>
  <%= f.hidden_field :flight_id, value: @flight.id %>

  <%= f.fields_for :passengers do |form| %>
    <h4>Passenger <%= form.index + 1 %></h4>

    <%= form.label :name, "Full name:" %>
    <%= form.text_field :name %>

    <%= form.label :email, "Email:" %>
    <%= form.email_field :email %>
  <% end %>

  <%= f.submit "Confirm details"%>
<% end %>

Upvotes: 1

PCurell
PCurell

Reputation: 111

As @Rockwell Rice mentioned in his comment:

The problem here is that you are permitting the wrong param.

      def booking_params
        params.require(:booking).permit(:flight_id,
            :passenger => [:name, :email, :passenger_id, :created_at, :updated_at])
      end

Should work.

Although you might encounter another error.

I think that you are constructing your fields_for wrong.

This should be what you are looking for (and you will not have to change the booking_params)

<%= form_with model: @booking, url: bookings_path do |f| %>
            <%= f.hidden_field :flight_id, value: @flight.id %>

            <% @booking.passengers.each_with_index do |passenger, index| %>
                <%= f.fields_for :passengers, passenger, index: index do |form| %>

                <h4><%= "Passenger #{index+1}"%> <br> </h4>

                    <%= form.label :name, "Full name:" %>
                    <%= form.text_field :name %>

                    <%= form.label :email, "Email:" %>
                    <%= form.email_field :email %>

             <% end %>
        <% end %>

            <%= f.submit "Confirm details"%>
        <% end %>

The above should work.

If you don't care too much about the index this would work as well:

         <%= form_with model: @booking, url: bookings_path do |f| %>
            <%= f.hidden_field :flight_id, value: @flight.id %>
            <%= f.fields_for @booking.passengers do |form| %>

                <%= form.label :name, "Full name:" %>
                <%= form.text_field :name %>

                <%= form.label :email, "Email:" %>
                <%= form.email_field :email %>

            <% end %>

            <%= f.submit "Confirm details"%>
        <% end %>

Upvotes: 0

Related Questions