views:

269

answers:

1

I have a form with a nested object (customer < order), and it works except that it keeps creating a new customer record.

I'd like to have it check to see if an existing customer is already present in the database (using the customer's email address to search), and if so, just attach the order to the existing record, but if not, continue creating a new customer record.

I think I have to do this by using an before_save call in my order model, but I don't know how to pass the form parameters (email address) in there. Is this the right approach, or is there some other way?


app/models/order.rb:

class Order < ActiveRecord::Base
  belongs_to :customer
  ...
  accepts_nested_attributes_for :customer
  ...
end

app/models/customer.rb:

class Customer < ActiveRecord::Base
  has_many :orders
  ...
end

app/controllers/orders_controller.rb:

def new
  @order = Order.new
  @order_number = "SL-#{Time.now.to_i}"
  @order.customer = Customer.new
  @services_available = Service.all
end

def create
  @order = Order.new params[:order]

  if @order.save
    flash[:notice] = 'Order was successfully created.'
    redirect_to @order
  else
    @order_number = "SL-#{Time.now.to_i}"
    render :action => "new"
  end
end

app/views/order/new.html.haml:

%h1 New order
- form_for @order do |order_form| 
  = error_messages_for :order
  %p
    = order_form.label :order_number
    %br
    = text_field_tag '', @order_number, :disabled => true, :id => 'order_number_display'
    = order_form.hidden_field :order_number, :value => @order_number
  - order_form.fields_for :customer do |customer_form| 
    %p
      = customer_form.label :first_name
      %br
      = customer_form.text_field :first_name
    %p
      = customer_form.label :last_name
      %br
      = customer_form.text_field :last_name
    %p
      = customer_form.label :phone
      %br
      = customer_form.text_field :phone
    %p
      = customer_form.label :email
      %br
      = customer_form.text_field :email
  %p= order_form.submit 'Create Order'
= link_to 'Back', orders_path
+2  A: 

With accepts_nested_attributes_for the attributes for the nest model are passed in the params has with the key :model(s)_attributes in your case: params[:order][:customer_attributes]

Because you have the accepts_nested_attributes_for works on the belongs_to side of a one-to-many relationship you're going to have to do some extra work. By overriding the customer_attributes attr_writter created when you call accept_nested_attributes_for.

In short this should work, or at least help you figure it out:

class Order < ActiveRecord::Base
  accepts_nested_attributes_for :customer
  belongs_to :customer


  def customer_attributes_with_existence_check=(attributes)
    self.customer = Customer.find_by_email(attributes[:email])
    self.customer_attributes_without_existence_check=(attributes) if customer.nil?
  end

  alias_method_chain 'customer_attributes=', :existence_check

end
EmFi
Okay, I get where you're going, but how do I get `customer_attributes` into the model? I tried running your code verbatim and I got an error `undefined local variable or method `customer_attributes'`. I tried to replace it with the params (`params[:order][:customer_attributes]`), but I got the same error, only this time for `params`. Where do I pass in these values so that the `identify_user` method can work?
neezer
I was assuming that the attributes were saved as an instance variable, with an attr_accessor, turns out I was wrong. After a little source diving I've come up with the right solution.
EmFi
Hmm... now I'm getting this error: `undefined method `customer_attributes_with_with_existence_check=' for class 'Order'` (using your edited code verbatim). I thought the extra "with" might be coming from `:with_existence_check`, so I changed that to `:existence_check`, with removed the extra "with", but still threw the same error. Any other thoughts?
neezer
Sorry about the back and forth, I didn't have time to test it. There were a couple of bugs with the last version. missing '=' in the method declaration, superfluous 'with_' in the alias\_method\_chain call. And I had an unless where there should have been an if. Everything has been fixed. I've tested it and it works.
EmFi
Maybe I'm missing something, then, but I made the changes you suggested and I'm still getting: `undefined method `customer_attributes_with_existence_check=' for class 'Order'`... ? Does the name of the method have to match the name of the method chain?
neezer
Looks like the alias_method_chain call must come after the definition of the new method.
EmFi
Awesome, that worked!! Thanks a ton! +1
neezer
Ei, I didn't thoroughly test this. Turns out the above only works if I already have a matching customer in the database. If not, I get the error `undefined method 'customer_attributes_without_existence_check'`. I tried defining the method above as `def customer_attributes_without_existence_check=(attributes) / self.customer = Customer.create!(attributes) / end` but no joy. Help??
neezer
No. Seems I forgot another '=' sign in the above code. The failure case in the new method wasn't actually calling the old method. Solution has been corrected.
EmFi
Okay; that got rid of the error, but the `Customer` isn't being created if it doesn't already exist. :(
neezer
I made a rookie mistake by forgetting that attribute writers require an explicit self when called. Solution has been updated and tested. Now it works as expected. Sorry for all the back and forth to get it right.
EmFi
Haha, so long as I get the problem fixed, I don't mind it at all! Thanks; that seemed to have done the trick (finally!). Thanks so much for your help!
neezer