Reputation: 41
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
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
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