Agile Web Development with Rails, Edition 4

12.2 Iteration G2: Atom Feeds 11.6 Iteration F6: Testing AJAX changes

12.1 Iteration G1: Capturing an Order

Expected at least 1 element matching "input[type=submit][value="Place Order"]", found 0.
<0> expected to be
>=
<1>.

Traceback:
  /home/rubys/git/awdwr/edition4/checkdepot.rb:228:in `block in <class:DepotTest>'

Create a model to contain orders

rails generate scaffold Order name address:text email pay_type
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
      invoke  active_record
      create    db/migrate/20150330104552_create_orders.rb
      create    app/models/order.rb
      invoke    test_unit
      create      test/models/order_test.rb
      create      test/fixtures/orders.yml
      invoke  resource_route
       route    resources :orders
      invoke  scaffold_controller
      create    app/controllers/orders_controller.rb
      invoke    erb
      create      app/views/orders
      create      app/views/orders/index.html.erb
      create      app/views/orders/edit.html.erb
      create      app/views/orders/show.html.erb
      create      app/views/orders/new.html.erb
      create      app/views/orders/_form.html.erb
      invoke    test_unit
      create      test/controllers/orders_controller_test.rb
      invoke    helper
      create      app/helpers/orders_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/orders/index.json.jbuilder
      create      app/views/orders/show.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/orders.coffee
      invoke    scss
      create      app/assets/stylesheets/orders.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.scss

Create a migration to add an order_id column to line_items

rails generate migration add_order_id_to_line_item order_id:integer
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
      invoke  active_record
      create    db/migrate/20150330104553_add_order_id_to_line_item.rb

Apply both migrations

rake db:migrate
mv 20150330104552_create_orders.rb 20150330000007_create_orders.rb
mv 20150330104553_add_order_id_to_line_item.rb 20150330000008_add_order_id_to_line_item.rb
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
== 20150330000007 CreateOrders: migrating =====================================
-- create_table(:orders)
   -> 0.0031s
== 20150330000007 CreateOrders: migrated (0.0032s) ============================
 
== 20150330000008 AddOrderIdToLineItem: migrating =============================
-- add_column(:line_items, :order_id, :integer)
   -> 0.0005s
== 20150330000008 AddOrderIdToLineItem: migrated (0.0006s) ====================
 

Add a Checkout button to the cart

edit app/views/carts/_cart.html.erb
<h2>Your Cart</h2>
<table>
  <%= render(cart.line_items) %>
 
  <tr class="total_line">
    <td colspan="2">Total</td>
    <td class="total_cell"><%= number_to_currency(cart.total_price) %></td>
  </tr>
 
</table>
 
<%= button_to "Checkout", new_order_path, method: :get %>
<%= button_to 'Empty cart', cart, method: :delete,
    data: { confirm: 'Are you sure?' } %>

Return a notice when checking out an empty cart

edit app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  include CurrentCart
  before_action :set_cart, only: [:new, :create]
  before_action :set_order, only: [:show, :edit, :update, :destroy]
 
  # GET /orders
  #...
end
edit app/controllers/orders_controller.rb
  def new
    if @cart.line_items.empty?
      redirect_to store_url, notice: "Your cart is empty"
      return
    end
 
    @order = Order.new
  end

Modify tests to ensure that there is an item in the cart

edit test/controllers/orders_controller_test.rb
  test "requires item in cart" do
    get :new
    assert_redirected_to store_path
    assert_equal flash[:notice], 'Your cart is empty'
  end
 
  test "should get new" do
    item = LineItem.new
    item.build_cart
    item.product = products(:ruby)
    item.save!
    session[:cart_id] = item.cart.id
 
    get :new
    assert_response :success
  end

Modify the template for new orders

edit app/views/orders/new.html.erb
<div class="depot_form">
  <fieldset>
    <legend>Please Enter Your Details</legend>
    <%= render 'form' %>
  </fieldset>
</div>

Modify the partial used by the template

edit app/views/orders/_form.html.erb
<%= form_for(order) do |f| %>
  <% if order.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(order.errors.count, "error") %>
      prohibited this order from being saved:</h2>
 
      <ul>
      <% order.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
 
  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name, size: 40 %>
  </div>
  <div class="field">
    <%= f.label :address %><br>
    <%= f.text_area :address, rows: 3, cols: 40 %>
  </div>
  <div class="field">
    <%= f.label :email %><br>
    <%= f.email_field :email, size: 40 %>
  </div>
  <div class="field">
    <%= f.label :pay_type %><br>
    <%= f.select :pay_type, Order::PAYMENT_TYPES,
                  prompt: 'Select a payment method' %>
  </div>
  <div class="actions">
    <%= f.submit 'Place Order' %>
  </div>
<% end %>

Add payment types to the order

edit app/models/order.rb
class Order < ActiveRecord::Base
  PAYMENT_TYPES = [ "Check", "Credit card", "Purchase order" ]
end

Add some CSS

edit app/assets/stylesheets/application.css.scss
.depot_form {
  fieldset {
    background: #efe;
 
    legend {
      color: #dfd;
      background: #141;
      font-family: sans-serif;
      padding: 0.2em 1em;
    }
  }
 
  form {
    label {
      width: 5em;
      float: left;
      text-align: right;
      padding-top: 0.2em;
      margin-right: 0.1em;
      display: block;
    }
 
    select, textarea, input {
      margin-left: 0.5em;
    }
 
    .submit {
      margin-left: 4em;
    }
 
    br {
      display: none
    }
  }
}

Validate name and pay_type

edit app/models/order.rb
class Order < ActiveRecord::Base
  # ...
  validates :name, :address, :email, presence: true
  validates :pay_type, inclusion: PAYMENT_TYPES
end

Update the test data in the orders fixture

edit test/fixtures/orders.yml
# Read about fixtures at
# http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
 
one:
  name: Dave Thomas
  address: MyText
  email: dave@example.org
  pay_type: Check
 
two:
  name: MyString
  address: MyText
  email: MyString
  pay_type: MyString

Move a line item from a cart to an order

edit test/fixtures/line_items.yml
# Read about fixtures at
# http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
 
one:
  product: ruby
  order: one
 
two:
  product: ruby
  cart: one

Define a relationship from the line item to the order

edit app/models/line_item.rb
class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :product
  belongs_to :cart
 
  def total_price
    product.price * quantity
  end
end

Define a relationship from the order to the line item

edit app/models/order.rb
class Order < ActiveRecord::Base
  has_many :line_items, dependent: :destroy
  # ...
end

Add line item to order, destroy cart, and redisplay catalog page

edit app/controllers/orders_controller.rb
  def create
    @order = Order.new(order_params)
    @order.add_line_items_from_cart(@cart)
 
    respond_to do |format|
      if @order.save
        Cart.destroy(session[:cart_id])
        session[:cart_id] = nil
        format.html { redirect_to store_url, notice: 
          'Thank you for your order.' }
        format.json { render :show, status: :created,
          location: @order }
      else
        format.html { render :new }
        format.json { render json: @order.errors,
          status: :unprocessable_entity }
      end
    end
  end

Implement add_line_items_from_cart

edit app/models/order.rb
class Order < ActiveRecord::Base
  # ...
  def add_line_items_from_cart(cart)
    cart.line_items.each do |item|
      item.cart_id = nil
      line_items << item
    end
  end
end

Modify the test to reflect the new redirect

edit test/controllers/orders_controller_test.rb
  test "should create order" do
    assert_difference('Order.count') do
      post :create, params: { order: { address: @order.address, email: @order.email,
        name: @order.name, pay_type: @order.pay_type } }
    end
 
    assert_redirected_to store_path
  end

take a look at the validation errors

get /orders/new

NameError in Orders#new

Showing /home/rubys/git/awdwr/edition4/work-220/depot/app/views/orders/_form.html.erb where line #1 raised:

undefined local variable or method `order' for #<#<Class:0x000000022830f0>:0x0000000227b238>
Extracted source (around line #1):
1
2
3
4
5
6
              
<%= form_for(order) do |f| %>
<% if order.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(order.errors.count, "error") %>
prohibited this order from being saved:</h2>

Trace of template inclusion: app/views/orders/new.html.erb

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

Application Trace | Framework Trace | Full Trace

Request

Parameters:

None

Response

Headers:

None

process an order

get /orders/new

NameError in Orders#new

Showing /home/rubys/git/awdwr/edition4/work-220/depot/app/views/orders/_form.html.erb where line #1 raised:

undefined local variable or method `order' for #<#<Class:0x000000022830f0>:0x00000003cdf2c8>
Extracted source (around line #1):
1
2
3
4
5
6
              
<%= form_for(order) do |f| %>
<% if order.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(order.errors.count, "error") %>
prohibited this order from being saved:</h2>

Trace of template inclusion: app/views/orders/new.html.erb

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

Application Trace | Framework Trace | Full Trace

Request

Parameters:

None

Response

Headers:

None

look at the underlying database

sqlite3> select * from orders
sqlite3> select * from line_items
        id = 10
product_id = 2
   cart_id = 4
created_at = 2015-03-30 10:45:45.955225
updated_at = 2015-03-30 10:45:45.955225
  quantity = 1
     price = 36
  order_id = 

hide the notice when adding items to the cart

edit app/views/line_items/create.js.erb
$('#notice').hide();
 
if ($('#cart tr').length == 1) { $('#cart').show('blind', 1000); }
 
$('#cart').html("<%= escape_javascript render(@cart) %>");
 
$('#current_item').css({'background-color':'#88ff88'}).
  animate({'background-color':'#114411'}, 1000);

12.2 Iteration G2: Atom Feeds 11.6 Iteration F6: Testing AJAX changes