ruby-on-railsruby

Ruby on Rails - One form to create multiple objects and associations


I am trying to prepare an app where we can choose a flights and NUMBER OF PASSENGERS. Based on number of passengers(params[:_passengers] and chosen flight(params[:_chosen_flight_id]) I am preparing a view with form to gather Passengers data. enter image description here

It's creating POST request to bookings#create action where I am trying to create 1 booking(flight_id,passenger_id) and associated passenger using booking.build_passenger method:

Started POST "/bookings" for 127.0.0.1 at 2024-12-18 17:18:29 +0100
Processing by BookingsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "_passengers"=>"2", "_chosen_flight_id"=>"7", "booking"=>{"0"=>{"passenger_attributes"=>{"passport_no"=>"123123123", "name"=>"Marek", "surname"=>"Surname"}}, "1"=>{"passenger_attributes"=>{"passport_no"=>"123123121", "name"=>"Marek", "surname"=>"Example"}}}, "button"=>""}
  Flight Load (0.2ms)  SELECT "flights".* FROM "flights" WHERE "flights"."id" = $1 LIMIT $2  [["id", 7], ["LIMIT", 1]]
  ↳ app/controllers/bookings_controller.rb:14:in `create'
Unpermitted parameters: :0, :1. Context: { controller: BookingsController, action: create, request: #<ActionDispatch::Request:0x00007f49722664a8>, params: {"authenticity_token"=>"[FILTERED]", "_passengers"=>"2", "_chosen_flight_id"=>"7", "booking"=>{"0"=>{"passenger_attributes"=>{"passport_no"=>"123123123", "name"=>"Marek", "surname"=>"Surname"}}, "1"=>{"passenger_attributes"=>{"passport_no"=>"123123121", "name"=>"Marek", "surname"=>"Example"}}}, "button"=>"",  "controller"=>"bookings", "action"=>"create"} }
Completed 500 Internal Server Error in 2ms (ActiveRecord: 0.2ms (1 query, 0 cached) | GC: 0.0ms)



ActiveModel::ForbiddenAttributesError (ActiveModel::ForbiddenAttributesError):

Code below:

#bookings_controller
class BookingsController < ApplicationController
  def new
    @bookings = []
    params[:_passengers].to_i.times do
      @bookings << Booking.new
    end
    @bookings.each do |booking|
      booking.build_passenger
    end
    @flight = Flight.find(params[:_chosen_flight_id])
  end

  def create
    @flight = Flight.find(params[:_chosen_flight_id])

    params[:_passengers].to_i.times do
      i = 0
      @booking = Booking.new
      @booking.flight = @flight
      @booking.build_passenger(params[:booking]["#{i}"][:passenger_attributes])
      i = i + 1
      puts "Booking no#{i}:"
      puts @booking.inspect
    end
  end

  private
  def booking_params
    params.require(:booking).permit(:flight_id, passenger_attributes: [:passport_no, :name, :surname])
  end
end
#bookings/new.html.erb form
<%= form_tag bookings_path do %>
  <%= hidden_field_tag :_passengers, params[:_passengers] %>
  <%= hidden_field_tag :_chosen_flight_id, params[:_chosen_flight_id] %>
  <ul>
  <% @bookings.each_with_index do |booking, i| %>
    Passenger information:
    <%= fields_for "booking[#{i}]", booking do |booking_form| %>
      <%= booking_form.fields_for :passenger do |passenger_form| %>
        <li>
        <%= passenger_form.label :passport_no %>
        <%= passenger_form.text_field :passport_no %>
        <%= passenger_form.label :name %>
        <%= passenger_form.text_field :name %>
        <%= passenger_form.label :surname %>
        <%= passenger_form.text_field :surname %>
        </li>
      <% end %>
    <% end %>
  <% end %>
  <%= button_tag "SUBMIT" %>
  </ul>
<% end %>

Any ideas how to iterate over my params and create booking-passenger pairs?


Solution

  • Quite simply, you need to use booking_params and not access the params object directly. In the example below I changed passenger_attributes to passengers_attributes and the relationship to has_many passengers, then I changed the form to fields_for booking and fields_for :passengers, booking.passengers.build, you also need to accept_nested_attributes_for passengers.

    class BookingsController < ApplicationController
      def new
        @booking = Booking.new
        (params[:_passengers].to_i || 0).times { @booking.passengers.build }
        # @flight = Flight.find(params[:_chosen_flight_id])
      end
    
      def create
        # @flight = Flight.find(params[:_chosen_flight_id])
        @booking = Booking.new(booking_params)
        if @booking.save
          redirect_to @booking
        else
          respond_with @booking
        end
      end
    
      private
      def booking_params
        params.require(:booking).permit(:flight_id, passengers_attributes: [:passport_no, :name, :surname])
      end
    end
    
    class Booking < ApplicationRecord
      has_many :passengers
      has_one :flight
      accepts_nested_attributes_for :passengers
    end
    
    <%= hidden_field_tag :_passengers, params[:_passengers] %>
    <%= hidden_field_tag :_chosen_flight_id, params[:_chosen_flight_id] %>
    <ul>
      <%= form_with model: @booking do |booking_form| %>
        <%= booking_form.fields_for :passengers do |passenger_form| %>
          Passenger information:
          <li>
            <%= passenger_form.label :passport_no %>
            <%= passenger_form.text_field :passport_no %>
            <%= passenger_form.label :name %>
            <%= passenger_form.text_field :name %>
            <%= passenger_form.label :surname %>
            <%= passenger_form.text_field :surname %>
          </li>
        <% end %>
        <%= booking_form.submit %>
      <% end %>
    </ul>
    
    tarted POST "/bookings" for 127.0.0.1 at 2024-12-19 21:27:08 -0500
    Processing by BookingsController#create as TURBO_STREAM
      Parameters: {"authenticity_token" => "[FILTERED]", "booking" => {"passengers_attributes" => {"0" => {"passport_no" => "1", "name" => "2", "surname" => "3"}, "1" => {"passport_no" => "3", "name" => "4", "surname" => "5"}, "2" => {"passport_no" => "5", "name" => "4", "surname" => "4"}}}, "commit" => "Create Booking"}
      TRANSACTION (0.4ms)  BEGIN immediate TRANSACTION /*action='create',application='Bookings',controller='bookings'*/
      ↳ app/controllers/bookings_controller.rb:10:in 'BookingsController#create'
      Booking Create (2.1ms)  INSERT INTO "bookings" ("created_at", "updated_at") VALUES ('2024-12-20 02:27:08.595012', '2024-12-20 02:27:08.595012') RETURNING "id" /*action='create',application='Bookings',controller='bookings'*/
      ↳ app/controllers/bookings_controller.rb:10:in 'BookingsController#create'
      Passenger Create (0.2ms)  INSERT INTO "passengers" ("created_at", "updated_at", "booking_id", "passport_no", "name", "surname") VALUES ('2024-12-20 02:27:08.608990', '2024-12-20 02:27:08.608990', 18, '1', '2', '3') RETURNING "id" /*action='create',application='Bookings',controller='bookings'*/
      ↳ app/controllers/bookings_controller.rb:10:in 'BookingsController#create'
      Passenger Create (0.1ms)  INSERT INTO "passengers" ("created_at", "updated_at", "booking_id", "passport_no", "name", "surname") VALUES ('2024-12-20 02:27:08.614035', '2024-12-20 02:27:08.614035', 18, '3', '4', '5') RETURNING "id" /*action='create',application='Bookings',controller='bookings'*/
      ↳ app/controllers/bookings_controller.rb:10:in 'BookingsController#create'
      Passenger Create (0.1ms)  INSERT INTO "passengers" ("created_at", "updated_at", "booking_id", "passport_no", "name", "surname") VALUES ('2024-12-20 02:27:08.620005', '2024-12-20 02:27:08.620005', 18, '5', '4', '4') RETURNING "id" /*action='create',application='Bookings',controller='bookings'*/
      ↳ app/controllers/bookings_controller.rb:10:in 'BookingsController#create'
      TRANSACTION (13.3ms)  COMMIT TRANSACTION /*action='create',application='Bookings',controller='bookings'*/
      ↳ app/controllers/bookings_controller.rb:10:in 'BookingsController#create'
    Redirected to http://localhost:3000/bookings/18
    Completed 302 Found in 75ms (ActiveRecord: 15.9ms (4 queries, 0 cached) | GC: 0.0ms)
    
    ❯ rails c
    Loading development environment (Rails 8.0.1)
    bookings(dev)> Booking.last.passengers
      Booking Load (0.1ms)  SELECT "bookings".* FROM "bookings" ORDER BY "bookings"."id" DESC LIMIT 1 /*application='Bookings'*/
      Passenger Load (0.1ms)  SELECT "passengers".* FROM "passengers" WHERE "passengers"."booking_id" = 18 /* loading for pp */ LIMIT 11 /*application='Bookings'*/
    => 
    [#<Passenger:0x00007d3f2b33ee50
      id: 7,
      created_at: "2024-12-20 02:27:08.608990000 +0000",
      updated_at: "2024-12-20 02:27:08.608990000 +0000",
      booking_id: 18,
      passport_no: "1",
      name: "2",
      surname: "3">,
     #<Passenger:0x00007d3f2b31bc20
      id: 8,
      created_at: "2024-12-20 02:27:08.614035000 +0000",
      updated_at: "2024-12-20 02:27:08.614035000 +0000",
      booking_id: 18,
      passport_no: "3",
      name: "4",
      surname: "5">,
     #<Passenger:0x00007d3f48c27bd0
      id: 9,
      created_at: "2024-12-20 02:27:08.620005000 +0000",
      updated_at: "2024-12-20 02:27:08.620005000 +0000",
      booking_id: 18,
      passport_no: "5",
      name: "4",
      surname: "4">]
    

    Edits: Per comment, I changed to use form_with and build the passengers in the controller. You could refactor further but this fixes the bug.