Ana
Ana

Reputation: 31

RoR How to access multiple model params posted through the same form

I am building a simple form with Ruby on Rails to submit an order.

My form needs to submit information from 3 different models: the user, the catalog_item and the order itself.

Here's my order model:

class Order < ApplicationRecord
    after_initialize :default_values
    validates :quantity, presence: true

    belongs_to :user
    belongs_to :catalog_item
    validates :user_id, presence: true
    validates :catalog_item_id, presence: true

    accepts_nested_attributes_for :user
    validates_associated :user
end

Here's my user model:

class User < ApplicationRecord
    has_many :orders
end

Here's my controller:

class OrdersController < ApplicationController

def checkout
    @order = Order.new(catalog_item: CatalogItem.find(params[:catalog_item_id]), user: User.new)
end

def create
    @order = Order.new(order_params)
    if @order.save
        # redirect_to confirmation_path
    else
        # redirect_to error_path
    end
end

private
    def user_params
    [:name, :email, :phone_number]
  end

    def order_params
        params.require(:order).permit(:id, :catalog_item_id, user_attributes: user_params)
    end
end

And here is my view form:

<%= form_for @order do |order_form| %>
    <%= order_form.hidden_field :catalog_item_id %>
    <%= order_form.fields_for :user do |user_fields| %>
        <%= user_fields.label :name %>
    <%= user_fields.text_field :name %>
    <%= user_fields.label :email %>
    <%= user_fields.text_field :email %>
    <%= user_fields.label :phone_number %>
    <%= user_fields.text_field :phone_number %>
    <% end %>
    <%= order_form.submit %>
<% end %>

This if the form HTML:

<form class="new_order" id="new_order" action="/orders" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓"><input type="hidden" name="authenticity_token" value="+z8JfieTzJrNgsr99C4jwBtXqIrpNtiEGPdVi73qJrpiGPpjYzbLwUng+e+yp8nIS/TLODWVFQtZqS/45SUoJQ==">
    <input type="hidden" value="1" name="order[catalog_item_id]" id="order_catalog_item_id">
    <label for="order_user_attributes_name">Name</label>
    <input type="text" name="order[user_attributes][name]" id="order_user_attributes_name">
    <label for="order_user_attributes_email">Email</label>
    <input type="text" name="order[user_attributes][email]" id="order_user_attributes_email">
    <label for="order_user_attributes_phone_number">Phone number</label>
    <input type="text" name="order[user_attributes][phone_number]" id="order_user_attributes_phone_number">
    <input type="submit" name="commit" value="Create Order" data-disable-with="Create Order">

Here are my routes:

get 'checkout/:catalog_item_id', to: 'orders#checkout', as: 'checkout'
post 'orders', to: 'orders#create'

When I try to save the @order inside the action create I get this error:

#<ActiveModel::Errors:0x007fe95d58b698 @base=#<Order id: nil, quantity: 1, created_at: nil, updated_at: nil, user_id: nil, catalog_item_id: 1>, @messages={:user_id=>["can't be blank"]}, @details={:user_id=>[{:error=>:blank}]}>

However it does work if I do this:

@catalog_item = CatalogItem.find(order_params[:catalog_item_id])
@user = User.new(order_params[:user_attributes])
@user.save
@order = Order.new(catalog_item: @catalog_item, user: @user)

This is what is being sent in the HTTP request when I post the form:

order[catalog_item_id]:1
order[user_attributes][name]:Ana
order[user_attributes][email]:[email protected]
order[user_attributes][phone_number]:123123123
commit:Create Order

I am new to RoR and I don't understand why order_params doesn't have the user but it does have the catalog_item_id.

Any help will be truly appreciated. Thank you!

Upvotes: 1

Views: 437

Answers (3)

Jay-Ar Polidario
Jay-Ar Polidario

Reputation: 6603

Assuming that your Order model belongs_to :user, My "suggested-rails-best-practice" solution is as follows:

See Rails Nested Attributes for more info. Basically what Nested Attributes does is it allows you to "create" also an associated record (in your example, the associated User) in just one command:

# example code:
Order.create(
  catalog_item_id: 1,
  user_attributes: {
    name: 'Foo',
    email: '[email protected]'
  }
)

# above will create two records (i.e.):
# 1) <Order id: 1 catalog_item_id: 1>
# 2) <User id: 1, order_id: 1, name: 'Foo', email: '[email protected]'>

Now that you can also pass in user_attributes as part of the hash when creating an order, it's easy enough to just treat user_attributes as also part of the request params, see controller below.

Model:

# app/models/order.rb
belongs_to :user

accepts_nested_attributes_for :user

# from our discussion, the validation needs to be updated into:
validates :user, presence: true
validates :category_item, presence: true

Controller:

# app/controllers/orders_controller.rb

def create
  @order = Order.new(order_params)

  if @order.save
    # DO SOMETHING WHEN SAVED SUCCESSFULLY
  else
    # DO SOMETHING WHEN SAVING FAILED (i.e. when validation errors)
    render :checkout
  end
end

private

# "Rails Strong Params" see for more info: http://api.rubyonrails.org/classes/ActionController/StrongParameters.html
def order_params
  params.require(:order).permit(:id, :catalog_item_id, user_attributes: [:name, :email, :phone_number])
end

View;

<%= form_for @order do |order_form| %>
  <!-- YOU NEED TO PASS IN catalog_item_id as a hidden field so that when the form is submitted the :catalog_item_id having the value pre-set on your `checkout` action, will be also submitted as part of the request -->
  <%= order_form.hidden_field :catalog_item_id %>

  <%= order_form.fields_for :user do |user_form| %>
    <%= user_form.label :name %>
    <%= user_form.text_field :name %>
    <%= user_form.label :email %>
    <%= user_form.text_field :email %>
    <%= user_form.label :phone_number %>
    <%= user_form.text_field :phone_number %>
    <% end %>
  <%= order_form.submit %>
<% end %>

Upvotes: 2

max
max

Reputation: 102045

If you want the user to be able to create orders from the show view for an item you can setup a nested route instead:

resources :catalog_items do 
  resources :orders, only: [:create]
end

Make sure you have

class Order < ApplicationRecord
  belongs_to :user
  belongs_to :catalog_item_id
  accepts_nested_attributes_for :user
  validates_associated :user # triggers user validations
end

class CatalogItem
  has_many :orders
end

Then you can do:

# /app/views/orders/_form.html.erb
<%= form_for [@catalog_item, @order || @catalog_item.orders.new] do |order_form| %>
  <%= order_form.fields_for :user do |user_fields| %>
    <%= user_fields.label :name %>
    <%= user_fields.text_field :name %>
    <%= user_fields.label :email %>
    <%= user_fields.text_field :email %>
    <%= user_fields.label :phone_number %>
    <%= user_fields.text_field :phone_number %>
  <% end %>
  <%= order_form.submit %>
<% end %>

# /app/views/catalog_items/show 
<%= render partial: 'orders/form' %>

This will set the form url to /catalog_items/:catalog_item_id/orders. which means that we pass catalog_item_id through the URL and not the form params - - this is a better practice as it makes the route descriptive and RESTful.

Then setup the controller:

class OrderController
  # POST /catalog_items/:catalog_item_id/orders
  def create
    @catalog_item = CatalogItem.find(params[:catalog_item_id])
    @order = @catalog_item.orders.new(order_params)
    # Uncomment the next line if you have some sort of authentication like Devise 
    # @order.user = current_user if user_signed_in?
    if @order.save
      redirect_to @catalog_item, success: 'Thank you for your order'
    else
      render 'catalog_items/show' # render show view with errors.
    end
  end

  # ...

  private

  def user_params
    [:name, :email, :phone_number]
  end

  def order_params
    params.require(:order)
          .permit(:id, user_attributes: user_params)
  end
end

Upvotes: 0

Puhlze
Puhlze

Reputation: 2614

User is a class so fields_for :user creates fields for a new user object.

Try calling order_form.fields_for instead of fields_for to scope the fields_for to your order object.

Upvotes: 0

Related Questions