Agile Web Development with Rails, Edition 4

10.2 Iteration E2: Handling Errors 9.4 Playtime

10.1 Iteration E1: Creating a Smarter Cart

RuntimeError: Edit app/controllers/line_items_controller.rb failed at makedepot.rb:1077

Traceback:
  

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_items quantity:integer
      invoke  active_record
      create    db/migrate/20141215144107_add_quantity_to_line_items.rb

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

edit db/migrate/20141215144107_add_quantity_to_line_items.rb
class AddQuantityToLineItems < ActiveRecord::Migration
  def change
    add_column :line_items, :quantity, :integer, default: 1
  end
end

Apply the migration

rake db:migrate
mv 20141215144107_add_quantity_to_line_items.rb 20141215000004_add_quantity_to_line_items.rb
== 20141215000004 AddQuantityToLineItems: migrating ===========================
-- add_column(:line_items, :quantity, :integer, {:default=>1})
   -> 0.0014s
== 20141215000004 AddQuantityToLineItems: migrated (0.0015s) ==================
 

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.find_by(product_id: product_id)
    if current_item
      current_item.quantity += 1
    else
      current_item = line_items.build(product_id: product_id)
    end
    current_item
  end

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

edit app/controllers/line_items_controller.rb
#<IndexError: regexp not matched>
  /home/rubys/git/gorp/lib/gorp/edit.rb:91:in `edit'
  makedepot.rb:1077:in `block (3 levels) in <main>'
  /home/rubys/git/gorp/lib/gorp/edit.rb:111:in `instance_exec'
  /home/rubys/git/gorp/lib/gorp/edit.rb:111:in `block in dcl'
  /home/rubys/git/gorp/lib/gorp/edit.rb:109:in `sub!'
  /home/rubys/git/gorp/lib/gorp/edit.rb:109:in `dcl'
  makedepot.rb:1076:in `block (2 levels) in <main>'
  /home/rubys/git/gorp/lib/gorp/edit.rb:173:in `instance_exec'
  /home/rubys/git/gorp/lib/gorp/edit.rb:173:in `edit'
  makedepot.rb:1074:in `block in <main>'
  /home/rubys/git/gorp/lib/gorp/output.rb:59:in `call'
  /home/rubys/git/gorp/lib/gorp/output.rb:59:in `block (4 levels) in <top (required)>'
  /home/rubys/git/gorp/lib/gorp/output.rb:49:in `each'
  /home/rubys/git/gorp/lib/gorp/output.rb:49:in `block (3 levels) in <top (required)>'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:175:in `call'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:175:in `_nested_structures'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:68:in `tag!'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:93:in `method_missing'
  /home/rubys/git/gorp/lib/gorp/output.rb:22:in `block (2 levels) in <top (required)>'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:175:in `call'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:175:in `_nested_structures'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:68:in `tag!'
  /home/rubys/.rvm/gems/ruby-2.1.5/gems/builder-3.2.2/lib/builder/xmlbase.rb:93:in `method_missing'
  /home/rubys/git/gorp/lib/gorp/output.rb:11:in `block in <top (required)>'
    
class LineItemsController < ApplicationController
  include CurrentCart
  before_action :set_cart, only: [:create]
  before_action :set_line_item, only: [:show, :edit, :update, :destroy]
 
  # GET /line_items
  def index
    @line_items = LineItem.all
  end
 
  # GET /line_items/1
  def show
  end
 
  # GET /line_items/new
  def new
    @line_item = LineItem.new
  end
 
  # GET /line_items/1/edit
  def edit
  end
 
  # POST /line_items
  def create
    @line_item = LineItem.new(line_item_params)
 
    if @line_item.save
      redirect_to @line_item, notice: 'Line item was successfully created.'
    else
      render :new
    end
  end
 
  # PATCH/PUT /line_items/1
  def update
    if @line_item.update(line_item_params)
      redirect_to @line_item, notice: 'Line item was successfully updated.'
    else
      render :edit
    end
  end
 
  # DELETE /line_items/1
  def destroy
    @line_item.destroy
    redirect_to line_items_url, notice: 'Line item was successfully destroyed.'
  end
 
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_line_item
      @line_item = LineItem.find(params[:id])
    end
 
    # Only allow a trusted parameter "white list" through.
    def line_item_params
      params.require(:line_item).permit(:product_id, :cart_id)
    end
  #...
end

Update the view to show both columns.

edit app/views/carts/show.html.erb
<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
 
<h2>Your Pragmatic Cart</h2>
<ul>    
  <% @cart.line_items.each do |item| %>
    <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

Generate a migration to combine/separate items in carts.

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

Fill in the self.up method

edit db/migrate/20141215144110_combine_items_in_cart.rb
  def 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
          item = cart.line_items.build(product_id: product_id)
          item.quantity = quantity
          item.save!
        end
      end
    end
  end

Combine entries

rake db:migrate
mv 20141215144110_combine_items_in_cart.rb 20141215000005_combine_items_in_cart.rb
== 20141215000005 CombineItemsInCart: migrating ===============================
== 20141215000005 CombineItemsInCart: migrated (0.0150s) ======================
 

Verify that the entries have been combined.

get /carts/1

Your Pragmatic Cart

Fill in the self.down method

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

Separate out individual items.

rake db:rollback
== 20141215000005 CombineItemsInCart: reverting ===============================
== 20141215000005 CombineItemsInCart: reverted (0.0028s) ======================
 
rake db:migrate:status
 
database: /home/rubys/git/awdwr/edition4/work/depot/db/development.sqlite3
 
 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20141215000001  Create products
   up     20141215000002  Create carts
   up     20141215000003  Create line items
   up     20141215000004  Add quantity to line items
  down    20141215000005  Combine items in cart
 
mv db/migrate/20141215000005_combine_items_in_cart.rb db/migrate/20141215000005_combine_items_in_cart.bak

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

get /carts/1

Your Pragmatic Cart

Recombine the item data.

mv db/migrate/20141215000005_combine_items_in_cart.bak db/migrate/20141215000005_combine_items_in_cart.rb
rake db:migrate
== 20141215000005 CombineItemsInCart: migrating ===============================
== 20141215000005 CombineItemsInCart: migrated (0.0149s) ======================
 

Add a few products to the order.

post /line_items?product_id=2

ActionController::ParameterMissing in LineItemsController#create

param is missing or the value is empty: line_item

Extracted source (around line #62):
60
61
62
63
64
65
                
# Only allow a trusted parameter "white list" through.
def line_item_params
params.require(:line_item).permit(:product_id, :cart_id)
end
#START:current_cart
#...

Rails.root: /home/rubys/git/awdwr/edition4/work/depot

Application Trace | Framework Trace | Full Trace

Request

Parameters:

{"product_id"=>"2",
 "authenticity_token"=>"cIgq60JrwgEeCqnlA5CWO7lqap19IAflgiDOwZRWh5nwTwpiXHMNPOLWN5K1DiYZEczDl39FMVRQR83WfoezmA=="}

Response

Headers:

None
post /line_items?product_id=3

ActionController::ParameterMissing in LineItemsController#create

param is missing or the value is empty: line_item

Extracted source (around line #62):
60
61
62
63
64
65
                
# Only allow a trusted parameter "white list" through.
def line_item_params
params.require(:line_item).permit(:product_id, :cart_id)
end
#START:current_cart
#...

Rails.root: /home/rubys/git/awdwr/edition4/work/depot

Application Trace | Framework Trace | Full Trace

Request

Parameters:

{"product_id"=>"3",
 "authenticity_token"=>"gMUPmDaBfqexhMe3iz2NlrKoNSoUjalePgY+IeiE7u8AAi8RKJmxmk1YWcA9oz20Gg6cIBbon+/sYT02AlXa7g=="}

Response

Headers:

None

Try something malicious.

get /carts/wibble

ActiveRecord::RecordNotFound in CartsController#show

Couldn't find Cart with 'id'=wibble

Extracted source (around line #51):
49
50
51
52
53
54
                
# Use callbacks to share common setup or constraints between actions.
def set_cart
@cart = Cart.find(params[:id])
end
# Only allow a trusted parameter "white list" through.

Rails.root: /home/rubys/git/awdwr/edition4/work/depot

Application Trace | Framework Trace | Full Trace

Request

Parameters:

{"id"=>"wibble"}

Response

Headers:

None

10.2 Iteration E2: Handling Errors 9.4 Playtime