Reputation: 31
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
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.
# 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
# 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
<%= 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
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
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