Agile Web Development with Rails, Edition 4

Agile Web Development with Rails, Edition 4

10.2 Iteration E2: Handling Errors 9.4 Playtime

10.1 Iteration E1: Creating a Smarter Cart

Change the cart to track the quantity of each product.

Add a quantity column to the line_item table in the database.

rails generate migration add_quantity_to_line_item quantity:integer
      invoke  active_record
      create    db/migrate/20100606143458_add_quantity_to_line_item.rb

Modify the migration to add a default value for the new column

edit db/migrate/20100606143458_add_quantity_to_line_item.rb
class AddQuantityToLineItem < ActiveRecord::Migration
  def self.up
    add_column :line_items, :quantity, :integer, :default => 1
  end
 
  def self.down
    remove_column :line_items, :quantity
  end
end

Apply the migration

rake db:migrate
mv 20100606143458_add_quantity_to_line_item.rb 20100301000004_add_quantity_to_line_item.rb
(in /home/rubys/svn/rails4/Book/util/work-193/depot)
==  AddQuantityToLineItem: migrating ==========================================
-- add_column(:line_items, :quantity, :integer, {:default=>1})
   -> 0.0114s
==  AddQuantityToLineItem: migrated (0.0115s) =================================
 

Create a method to add a product to the cart by either incrementing the quantity of an existing line item, or creating a new line item.

edit app/models/cart.rb
  def add_product(product_id)
    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)
      line_items << current_item
    end
    current_item
  end

Replace the call to LineItem.new with a call to the new method.

edit app/controllers/line_items_controller.rb
  def create
    @cart = find_or_create_cart
    product = Product.find(params[:product_id])
    @line_item = @cart.add_product(product.id)
 
    respond_to do |format|
      if @line_item.save
        format.html { redirect_to(@line_item.cart,
          :notice => 'Line item was successfully created.') }
        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

Update the view to show both columns.

edit app/views/carts/show.html.erb
<h2>Your Pragmatic Cart</h2>
<ul>    
  <% for item in @cart.line_items %>
    <li><%= item.quantity %> &times; <%= item.product.title %></li>
  <% end %>
</ul>

Look at the cart, and see that's not exactly what we intended

get /carts/1

Your Pragmatic Cart

  • 1 × Programming Ruby 1.9
  • 1 × Programming Ruby 1.9

Generate a migration to combine/separate items in carts.

rails generate migration combine_items_in_cart
      invoke  active_record
      create    db/migrate/20100606143509_combine_items_in_cart.rb

Fill in the self.up method

edit db/migrate/20100606143509_combine_items_in_cart.rb
  def self.up
    # replace multiple items for a single product in a cart with a single item
    Cart.all.each do |cart|
      # count the number of each product in the cart
      sums = cart.line_items.group(:product_id).sum(:quantity)
 
      sums.each do |product_id, quantity|
        if quantity > 1
          # remove individual items
          cart.line_items.where(:product_id=>product_id).delete_all
 
          # replace with a single item
          cart.line_items.create(:product_id=>product_id, :quantity=>quantity)
        end
      end
    end
  end

Combine entries

rake db:migrate
mv 20100606143509_combine_items_in_cart.rb 20100301000005_combine_items_in_cart.rb
(in /home/rubys/svn/rails4/Book/util/work-193/depot)
==  CombineItemsInCart: migrating =============================================
==  CombineItemsInCart: migrated (0.1659s) ====================================
 

Verify that the entries have been combined.

get /carts/1

Your Pragmatic Cart

  • 2 × Programming Ruby 1.9

Fill in the self.down method

edit db/migrate/20100301000005_combine_items_in_cart.rb
  def self.down
    # split items with quantity>1 into multiple items
    LineItem.where("quantity>1").each do |lineitem|
      # add individual items
      lineitem.quantity.times do 
        LineItem.create :cart_id=>lineitem.cart_id,
          :product_id=>lineitem.product_id, :quantity=>1
      end
 
      # remove original item
      lineitem.destroy
    end
  end

Separate out individual items.

rake db:rollback
(in /home/rubys/svn/rails4/Book/util/work-193/depot)
==  CombineItemsInCart: reverting =============================================
==  CombineItemsInCart: reverted (0.1012s) ====================================
 

Every item should (once again) only have a quantity of one.

get /carts/1

Your Pragmatic Cart

  • 1 × Programming Ruby 1.9
  • 1 × Programming Ruby 1.9

Recombine the item data.

rake db:migrate
(in /home/rubys/svn/rails4/Book/util/work-193/depot)
==  CombineItemsInCart: migrating =============================================
==  CombineItemsInCart: migrated (0.1668s) ====================================
 

Add a few products to the order.

post /line_items?product_id=2
You are being redirected.
get http://localhost:3000/carts/1

Your Pragmatic Cart

  • 2 × Programming Ruby 1.9
  • 1 × Web Design for Developers
post /line_items?product_id=3
You are being redirected.
get http://localhost:3000/carts/1

Your Pragmatic Cart

  • 3 × Programming Ruby 1.9
  • 1 × Web Design for Developers
pub depot_g

Try something malicious.

get /carts/wibble

ActiveRecord::RecordNotFound in CartsController#show

Couldn't find Cart with ID=wibble

Rails.root: /home/rubys/svn/rails4/Book/util/work-193/depot

Application Trace | Framework Trace | Full Trace
app/controllers/carts_controller.rb:16:in `show'

Request

Parameters:

{"id"=>"wibble"}

Show session dump

Show env dump

Response

Headers:

None

10.2 Iteration E2: Handling Errors 9.4 Playtime