1

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]:ana@gmail.com
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!

Ana
  • 31
  • 1
  • 5
  • What view have you posted? Log the params hash and you'll see what the form is actually passing. Any data that you need for the method that is not collected by the form, you can pass in a hidden field. – margo May 22 '17 at 20:58
  • @margo When I log the params this is what I get `{"utf8"=>"✓", "authenticity_token"=>"9tXNW6m8fDha53/HGL1i+yTzDEDDsHbcx87KpqugI0dv8j5G7Rl7Y96FTNVeNIjzdFBv8h8Tu1OGkLDV828t2A==", "user"=>"John", "email"=>"john@example.com", "phone_number"=>"123123123"} permitted: false>, "commit"=>"Create Order", "controller"=>"orders", "action"=>"create"} permitted: false>` I posted the form from the checkout view `get 'checkout/:catalog_item_id', to: 'orders#checkout', as: 'checkout'`. Hope that makes sense. – Ana May 22 '17 at 21:07
  • I'm still a bit confused as to the actual scenario - can an order only ever have one item? – max May 22 '17 at 21:34
  • @max Yes that's correct. The idea is that the app will allow people to order workshops (catalog_items), but it's a simple workflow in that the user comes into the website, chooses the workshop and checksout. So only one catalog_item per order. No need to login either. – Ana May 22 '17 at 22:06
  • Yeah then definatly make it a nested route. – max May 22 '17 at 22:14

3 Answers3

2

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: 'foo@bar.com'
  }
)

# 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: 'foo@bar.com'>

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 %>
Jay-Ar Polidario
  • 6,463
  • 14
  • 28
  • Hi Jay, thanks for your help! This is making a lot of sense to me. :-) I have made the changes in my application as suggested, however when I try to save the order it gets rolled back due to `#`. I checked the order_params which is missing the user for some reason `"1"} permitted: true>`. In my Order model I have `belongs_to`, `accepts_nested_attributes_for` and `validates_associated` for `:user`. I also have `validates :user_id, presence: true`. Would this be causing a problem? Ta! – Ana May 22 '17 at 22:02
  • @Ana hi Ana, did you implement also my "Strong-Params" above? `params.require(:order).permit(:id, :catalog_item_id, user_attributes: [:name, :email, :phone_number])` ? Also, did you change yours into this specifically `<%= order_form.fields_for :user do |user_form| %>` from above? That is important as well, because this line nests the fields from `order` --to--> `user`. – Jay-Ar Polidario May 23 '17 at 08:32
  • your current code is `<%= fields_for :user, @order.user do |user_fields| %>` that will generate request params like `user: {name: 'Foo', email: 'foo@bar.com'}`. However, you do not want this. Try changing it into `<%= order_form.fields_for :user do |user_form| %>`. You'll notice that the request params becomes something like `order: { user_attributes: { name: 'Foo', email: 'foo@bar.com' }}`. See the difference? The other one is nested, while the other is not. If you have read my answer above,you'll immediately realize why this nested one will work when the params are passed into `Order.create` – Jay-Ar Polidario May 23 '17 at 08:41
  • @Ana if this still doesn't work, can you update your question, and add the HTML of the form on the page? If you don't know how to do this and you're using Chrome, see [this](https://developers.google.com/web/tools/chrome-devtools/inspect-styles/). Furthermore, can you also add to your question details about the request? [See this](https://stackoverflow.com/questions/4423061/view-http-headers-in-google-chrome) Basically, what I'd like to see is the request params. When you already have your Chrome Dev Tools opened, submit your form, then copy the request params. – Jay-Ar Polidario May 23 '17 at 08:53
  • Hi Jay, I haven't had a chance to do this yet, but I will do it later today and post the outcome. Thanks so much for your help! – Ana May 23 '17 at 10:31
  • Hi Jay, I checked my form and I had changed the code to `<%= order_form.fields_for :user do |user_fields| %> to make sure it is nested. I still get an empty user. I updated my code above. Thanks so much for your help! – Ana May 23 '17 at 18:59
  • Did you add this line? `accepts_nested_attributes_for :user` in the `Order` model as I have written above? – Jay-Ar Polidario May 23 '17 at 21:11
  • Hi Jay, yes I have. I have added the info you requested above (HTTP request, HTML form) and also included the Order controller. I am sure it's a small thing that I am not seeing. – Ana May 23 '17 at 21:15
  • Hmm that's strange... I wonder if your `after_initialize :default_values` overrides the `User` value. Can you also show the code for `default_values`? – Jay-Ar Polidario May 23 '17 at 21:18
  • The code is `def default_values self.quantity ||= 1 end` – Ana May 23 '17 at 21:20
  • Your request params looks ok, and everything seems to be ok. Can you run this in the action method? `order = Order.new(catalog_item_id: 99, user_attributes: {name: 'Foo', email: 'foo@example.com'}) \n puts order.user`? And let me know the results? \n means newline by the way – Jay-Ar Polidario May 23 '17 at 21:25
  • Hi Jay, I did that and after `puts @order.user` I logged `@order` and got `#`. Then I tried to save the `@order` and the transaction got rolled back. It seems like saving the `@order` is not propagating saving the `@user`. Do I have to indicate that somehow in the User model? – Ana May 23 '17 at 22:01
  • What does `puts @order.user` show in the Rails server? Or you can just log it actually so `logger.debug(@order.user)` instead of `puts @order.user` if you're unfamiliar with `puts` – Jay-Ar Polidario May 23 '17 at 22:05
  • Sure. This is what it logs `#`. I got all my code on github if that's not too much trouble. – Ana May 23 '17 at 22:10
  • @Ana seems like the `user` is associated to the new `order` just fine as you can see above – Jay-Ar Polidario May 23 '17 at 22:12
  • Yes correct. Same when I do this `@catalog_item = CatalogItem.find(order_params[:catalog_item_id]) \n @user = User.new(order_params[:user_attributes]) \n @user.save \n @order = Order.new(catalog_item: @catalog_item, user: @user) \n @order.save`. It just doesn't work with `@order = Order.new(order_params) \n @order.save` which seems like the cleaner way to do it. – Ana May 23 '17 at 22:17
  • Oh I think it's the validation. Can you update your code into `validates :user, presence: true` and `validates :category_item, presence: true`? You usually (except in some cases) validate association this way, instead of validating the actual `_id` attribute. – Jay-Ar Polidario May 23 '17 at 22:17
  • Although, I doubt this would have an effect as yours was validated without errors even if the user_id is still blank at the start. But just in case this, can you try changing with the above code. – Jay-Ar Polidario May 23 '17 at 22:19
  • Sure will do. What's the `:category`? – Ana May 23 '17 at 22:19
  • If this still doesn't work. I can try checking out your github repo, and debug on this myself as I am curious as well why it doesn't work :) – Jay-Ar Polidario May 23 '17 at 22:23
  • Sure, it is okay to post github repo in here? – Ana May 23 '17 at 22:26
  • I don't have any other ideas haha :) Can you share me your github repo? I'll try to debug. – Jay-Ar Polidario May 23 '17 at 22:26
  • Yes, it's fine :) – Jay-Ar Polidario May 23 '17 at 22:26
  • Okay it's this one https://github.com/anahenneberke/worklife. This app is just for learning so the code is not that clean (yet!). :-) – Ana May 23 '17 at 22:28
  • hey no worries! Give me a few minutes. I'll be back. – Jay-Ar Polidario May 23 '17 at 22:28
  • Seems like it works when I changed it into `validates :user, presence: true` and `validates :category_item, presence: true` Can you try again? – Jay-Ar Polidario May 23 '17 at 22:39
  • actually maybe I'll submit a pull request then, so you could pull my updated code. – Jay-Ar Polidario May 23 '17 at 22:40
  • Do you mean `:catalog_item`? If I use `:category_item` I get `undefined method 'category_item'`. I'll wait for the pull request, probably be easier that way. – Ana May 23 '17 at 22:43
  • The problem was the `validates :catalog_item_id, presence: true` should have been `validates :catalog_item, presence: true` as you said. I cannot thank you enough! Should I post the solution as an answer in this page? – Ana May 23 '17 at 22:47
  • I just submitted a pull request. Glad it worked! :) I'll update my answer, so you won't need to write the solution. – Jay-Ar Polidario May 23 '17 at 22:52
  • Thank you so SO much Jay-Ar! I have learned a lot during this Q&A, including debugging the application. :-) – Ana May 23 '17 at 22:56
  • No worries! :) By the way, another cool trick on debugging is to make use of `byebug`. Just insert that on aaaaaany part of a ruby file. That will pause the execution, and once paused, you'll be able to inspect variables, update variables, create new variables, etc... as if you are running the code in that specific line where you insert `byebug`. Press `c` and press Enter to unpause. This is very helpful!!! You'll appreciate it surely! :) – Jay-Ar Polidario May 23 '17 at 22:59
  • Great I will do. So glad I will be able to move on now and keep building this app. Thanks for the tip. – Ana May 23 '17 at 23:02
  • Good luck further ahead. Let me know if you get stuck :) Hopefully I could help – Jay-Ar Polidario May 23 '17 at 23:03
0

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.

Puhlze
  • 2,634
  • 18
  • 16
0

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
max
  • 96,212
  • 14
  • 104
  • 165
  • You're making another very common noob misstake - always call `save` and `update` with an `if` statement and respond to success and failure. – max May 22 '17 at 21:27
  • You should also setup the routes with `resources :orders` – max May 22 '17 at 21:32
  • Thanks @max I am going to give this a go. I was hoping to make the catalog_item_id field hidden. – Ana May 22 '17 at 21:42
  • Edited. Making the route nested might be what you actually are looking for. – max May 22 '17 at 21:54
  • I think I am getting closer. I am sending the catalog_id as a hidden field but I will change it to the nested route you purposed later. I managed to get what I need in the params `"1", "user_attributes"=>"Ana Henneberke", "email"=>"ana.henneberke4@gmail.com", "phone_number"=>"07501782993"} permitted: true>} permitted: true>` but when I try to save the order `@order = Order.new(order_params) @order.save` I get this error `#`. – Ana May 22 '17 at 22:34
  • Add a breakpoint and check `@order.user.errors` the user is not being saved for some reason and I'm to tired to figure it out. – max May 22 '17 at 22:38
  • It works when I save the user first and then the order `@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) @order.save` (not catching success or failure just to illustrate) so there's something missing somewhere. If I find a solution, I'll post it here. Thanks for your help and patience. :-) – Ana May 22 '17 at 23:20
  • hmm, then its something with `accepts_nested_attributes_for` - I can't really see anything wrong though - but you might want read through the docs: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html – max May 22 '17 at 23:26