Activity Description

Create a migration that copies the product price into the line item, and add_product method in the Cart model to capture the price whenever a new line item is created.

Author’s Solutions

Solution – search for “Add price to line item”

Readers’ Solutions

Marius says

The migration first: rails generate migration add_product_price_to_line_item price:decimal

class AddProductPriceToLineItem < ActiveRecord::Migration
  def self.up
    add_column :line_items, :price, :decimal, :precision => 8, :scale => 2

    say_with_time "Updating prices..." do
      LineItem.find(:all).each do |lineitem|
        lineitem.update_attribute :price, lineitem.product.price
      end
    end
  end

  def self.down
    remove_column :line_items, :price
  end
end
In app/models/cart.rb

def add_product(product_id, product_price)
  current_item = line_items.where(:product_id => product_id).first
  if current_item
    current_item.quantity += 1
  else
    current_item = LineItem.new(:product_id => product_id, :price => product_price)
    line_items << current_item
  end
  current_item
end
In app/controllers/line_items_controller.rb pass product.price as an argument in the Create action:

def create
  @cart = current_cart
  product = Product.find(params[:product_id])
  @line_item = @cart.add_product(product.id, product.price)
  session[:counter] = 0
  respond_to do |format|
    if @line_item.save
      format.html { redirect_to(@line_item.cart) }
      format.xml  { render :xml => @line_item, :status => :created, :location => @line_item }
    else
      format.html { render :action => "new" }
      format.xml  { render :xml => @line_item.errors, :status => :unprocessable_entity }
    end
  end
end

Brent says

The migration should update existing line items price with the product price. The add_product method will only take care of adding new products (after the migration).

Marius says

Thanks. I read over that.

Ken says

Would it also make sense to update app/models/line_item.rb to use the line_item’s price instead of the product’s price? This would make sure you are using the line_item price in the display:


class LineItem < ActiveRecord::Base
  belongs_to :product
  belongs_to :cart

  def total_price
    price * quantity
  end
end

Doc says

Through unit testing, I found that the price and quantity are not actually updated in the table for data added after the migration. I changed the add_product method to include a parameter for the current price (defaults to nil to use the catalog price if current_price is not provided) and used the update_attributes method to update the table with the new price and quantity. I chose to update the price any time the line item is changed.

File: app/models/cart.rb

...
  def add_product(product_id, current_price=nil)
    current_item = line_items.find_by_product_id product_id
    current_price = Product.find(product_id).price if current_price.nil?

    if current_item
      current_item.update_attributes \
        :quantity => current_item.quantity + 1,
        :price => current_price
    else
      current_item = LineItem.new \
        :product_id => product_id,
        :price => current_price
      line_items << current_item
    end

    current_item
  end
...

Elijah says

This will keep you from having to pass in the price:


def add_product(product_id)
    @product = Product.find(product_id)
    current_item = line_items.where(:product_id => @product.id).first
    if current_item
      current_item.quantity += 1
    else
      current_item=line_items.build(:product_id => @product.id,:price => @product.price)
      line_items << current_item
    end
    current_item
  end

Will says

In the examples given above, why is the “current_item” added to “line_items”? This code is not included in the book?

File: app/models/cart.rb

def add_product(product_id)
    ..
      line_items << current_item

Am I correct that the save method in “line_items_controller” already covers this?

File: app/controllers/line_items_controller.rb

def create
    ..
      if @line_item.save

Greg says

Am I missing something, or you overcomplicate the code of the Cart model?

I added one line to the add_product method:

File: app/models/cart.rb

      current_item.price = current_item.product.price

Karen says

This last proposed solution won’t work if current_item does not exist yet (the current item is not the same book as an item already in the cart.) You get a method not found error for price on the current item.

You do need to go back using the product_id to get the matching product and price if current_item does not exist…

else
 product = Product.find(product_id) 
      current_item = line_items.build(:product_id => product_id, :price => product.price)
end

for cases where line item refers to the same product as an item already in the cart, you stick to incrementing quantity alone….

Besides what I note above, I left everything else intact….

I like the Elijah solution the best thus far…..

Dean says

To add a product you must also consider the price, same product_id but a price change for a certain product makes a new line_item.


def add_product(product_id)
    @product = Product.find(product_id)
    current_item = line_items.where(:product_id => @product.id, :price => product.price).first
    if current_item
      current_item.update_attributes :quantity => current_item.quantity + 1
    else
      current_item = line_items.build(:product_id => product_id, :price => product.price)
      line_items << current_item
    end
    current_item
  end

Dave Buenconsejo says

The migration first: rails generate migration add_product_price_to_line_item price:decimal

class AddProductPriceToLineItem < ActiveRecord::Migration
  def self.up
    add_column :line_items, :price, :decimal, :precision => 8, :scale => 2
    LineItem.all.each do |line|
      line.update_attributes :price => line.product.price
    end
  end

  def self.down
    remove_column :line_items, :price
  end
end
In app/models/cart.rb

  def add_product(product_id)
    current_item = line_items.find_by_product_id(product_id)
    if current_item
      current_item.quantity += 1
    else
      current_item = line_items.build(:product_id => product_id)
      current_item.price = current_item.product.price
    end
    current_item
  end
In app/controllers/line_items_controller.rb pass product.price as an argument in the Create action:

def create
  @cart = current_cart
  product = Product.find(params[:product_id])
  @line_item = @cart.add_product(product.id)
  session[:counter] = 0
  respond_to do |format|
    if @line_item.save
      format.html { redirect_to(@line_item.cart) }
      format.xml  { render :xml => @line_item, :status => :created, :location => @line_item }
    else
      format.html { render :action => "new" }
      format.xml  { render :xml => @line_item.errors, :status => :unprocessable_entity }
    end
  end
end

Duccio Armenise says

The previous solutions didn’t work for me as the new column was created but the migration failed in copying the prices in.

I fixed it calling LineItem.reset_column_information right after adding the price attribute to the model:

rails generate migration add_price_to_line_item price:decimal

class AddPriceToLineItem < ActiveRecord::Migration
  def self.up

    add_column :line_items, :price, :decimal
    LineItem.reset_column_information

    LineItem.all.each do |li|
      li.update_attribute :price, li.product.price
    end

  end

  def self.down
    remove_column :line_items, :price
  end
end

I think this should be a best practice.

H4. Steven says…

Why does Ken’s solution above not work for me? It creates an error…


Started GET "/carts/28" for 127.0.0.1 at 2011-10-26 11:45:30 -0500
  Processing by CartsController#show as HTML
  Parameters: {"id"=>"28"}
  Cart Load (0.1ms)  SELECT "carts".* FROM "carts" WHERE "carts"."id" = ? LIMIT 1  [["id", "28"]]
  LineItem Load (0.1ms)  SELECT "line_items".* FROM "line_items" WHERE "line_items"."cart_id" = 28
  Product Load (0.1ms)  SELECT "products".* FROM "products" WHERE "products"."id" = 23 LIMIT 1
  Product Load (0.1ms)  SELECT "products".* FROM "products" WHERE "products"."id" = 24 LIMIT 1
Rendered line_items/_line_item.html.erb (19.2ms)
Rendered carts/_cart.html.erb (46.8ms)
Rendered carts/show.html.erb within layouts/application (49.4ms)
Completed 500 Internal Server Error in 54ms

ActionView::Template::Error (You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.*):
    1: <tr>
    2:   <td><%= line_item.quantity %>&times;</td>
    3:   <td><%= line_item.product.title %></td>
    4:   <td class="item_price"><%= number_to_currency(line_item.total_price) %></td>
    5: </tr>
  app/models/line_item.rb:6:in `total_price'
  app/views/line_items/_line_item.html.erb:4:in `_app_views_line_items__line_item_html_erb___2702596415403854200_70149284115780'
  app/views/carts/_cart.html.erb:3:in `_app_views_carts__cart_html_erb___4150741666978282053_70149281547220'
  app/views/carts/show.html.erb:5:in `_app_views_carts_show_html_erb___3334241725704639237_70149282347060'
  app/controllers/carts_controller.rb:22:in `show'