This post is part of a series of reviews on the book Agile Web Development With Rails. Check out the Introduction post for a full table of contents along with some initial notes.

After learning how to build a cart that can keep track of orders, it’s time to make those orders happen. When we were dealing with our cart, we didn’t worry about creating database tables for the core thing the cart holds: cart items, so the time has come to finally create a model for that and for the orders taken (so they too will be stored at the database), and we’ll do it by using script generators at the command prompt:

ruby script/generate model order

ruby script/generate model line_item

A bunch of files should have been generated, including the migrations for these tables (remember migrations from Task A? Well, you should), so let’s edit each of ’em for our specific needs. Starting with the create_orders migration file:

class CreateOrders < ActiveRecord::Migration
  def self.up
    create_table :orders do |t|
      t.string :name
      t.text :address
      t.string :email
      t.string :pay_type, :limit => 10

      t.timestamps
    end
  end

  def self.down
    drop_table :orders
  end
end

We’re just telling it to create specific filds with the name and type of data we want. Timestamps are the created_at and updated_at fields that every Rails table has. Anyway, moving on to the create_line_items migration:

class CreateLineItems < ActiveRecord::Migration
  def self.up
    create_table :line_items do |t|
      t.integer :product_id, :null=>false
      t.integer :order_id, :null=>false
      t.integer :quantity, :null=>false
      t.decimal :total_price, :null=>false, :precision=>8, :scale=>2

      t.timestamps
    end
  end

  def self.down
    drop_table :line_items
  end
end

To make the migrations actually go into the database:

rake db:migrate

There’s a certain relation between products, orders and line items, an order includes line items, a product also includes line items, and line items are included by both. Of course we know this, but Rails doesn’t, and there’s a nice and easy to tell it. First, edit the order model file:

class Order<ActiveRecord::Base
  has_many:line_items
end

Now the product model file:

class Product<ActiveRecord::Base
  has_many:line_items
  #all the stuff we added in other tasks
end

And finally, the line item model file:

class LineItem<ActiveRecord::Base
  belongs_to:order
  belongs_to:product
end

Now that our tables are all set, let’s create a link on the cart partial to a checkout action:

<div class="cart-title">Your Cart</div>
<table>
  <%= render(:partial => "cart_item" , :collection => cart.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",    :action=>:checkout     %>
<%= button_to "Empty cart" , :action => :empty_cart %>

The cart is now linking to a checkout action, which we will now create (on the store controller):

def checkout
  @cart = find_cart
  if @cart.items.empty?
    redirect_to_index("Your cart is empty")
  else
    @order = Order.new
  end
end

After finding a cart (on the session), the action will check if the cart is empty (maybe some smart guy is trying to make an order with an empty cart) and redirect case it is, case not, a new order is created. The logic for the checkout action is now all set, time to make its view file (checkout.html.erb):

<div class="depot-form">
  <%= error_messages_for 'order' %>
  <% form_for :order, :url=>{:action => :save_order} do |form| %>
  <fieldset>
    <legend>Please enter your details</legend>
    <p>
      <%= label :order, :name, "Name:" %>
      <%= form.text_field :name, :size=>40 %>
    </p>
    <p>
      <%= label :order, :address, "Address:" %>
      <%= form.text_area :address, :rows=>3, :cols=>40 %>
    </p>
    <p>
      <%= label :order, :email, "E-mail:" %>
      <%= form.text_field :email, :size=>40 %>
    </p>
    <p>
      <%= label :order, :pay_type, "Pay with:" %>
      <%= form.select :pay_type, Order::PAYMENT_TYPES,
                      :prompt=>"Select a payment method" %>
    </p>
    <%= submit_tag "Place Order", :class=>"submit" %>
  </fieldset>
  <% end %>
</div>

The error_messages_for displays any validation error regarding the ‘order’ form, which we created in the line right below. The form_for helper takes a name (‘order’), a url to where the HTTP POST request will be sent to (in our case, a save_order action) and a block which will contain everything relating to the form, such as HTML tags and input fields. Notice the select method is using a constant which is in the Order class that we didn’t define yet, and that’s what we will be doing now along with adding some validation helpers:

class Order < ActiveRecord::Base

  PAYMENT_TYPES = [
    #Displayed        stored in DB
    ["Check",         "check"],
    ["Credit card",   "cc"],
    ["Purchase order","po"]
  ]

  validates_presence_of :name, :address, :email, :pay_type
  validates_inclusion_of :pay_type, :in => PAYMENT_TYPES.map {|disp,value| value}

  has_many :line_items
end

Try adding stuff to the cart and clicking the ‘Checkout’ button, a nice page should show asking for your information. All good, but the data still isn’t going into the database because we did’nt set the save_order method (which the form is pointing to), which we will be doing now by going through a few steps.
First we’ll be creating the save_order method on our store controller, which should capture an order, fill in an order model object with that order (which contains line items), validate the information case it’s wrong and finally redirect the user to the catalog page with a cheerful message. Getting down to the code:

def save_order
  @cart = find_cart
  @order = Order.new(params[:order])
  @order.add_line_items_from_cart(@cart)
  if @order.save
    session[:cart] = nil
    redirect_to_index("Thank you for your order")
  else
    render :action => :checkout
  end
end

We’re telling the code to find a cart, instace a new order based on the object sent by the form, add all items from the cart to that order and save it to the database, clearing the session and redirecting the user case that goes smoothly, or render back the checkout page case validation fails and the database isn’t able to save the data. To make this work, let’s define the add_line_items_from_cart method on the order model file:

def add_line_items_from_cart(cart)
  cart.items.each do |item|
    li=LineItem.from_cart_item(item)
    self.line_items<<li
  end
end

As you can see, it’s basically an iteration over the cart items that invokes a LineItem method and adds that result to the line_items array, which belongs to the order object itself. Now all we need to do is define that from_cart_item class method on the line item model file:

class LineItem < ActiveRecord::Base

  belongs_to :order
  belongs_to :product

  def self.from_cart_item(cart_item)
    li = self.new
    li.product = cart_item.product
    li.quantity = cart_item.quantity
    li.total_price = cart_item.price
    li
  end
end

All this is doing is associating a cart_item (which came from the cart object iteration in the add_line_items_from_cart method) with a new instance of LineItem that will later on be saved into the database when the method @order.save is called. And how is it possible to save an order and save all the line items that correspond to that order without calling the save method on each LineItem? Simple, remember the association we did at the beggining where an Order has_many :line_items and a LineItem belongs_to :order? We did that to tell Rails that a relation exists between them, so it would automatically associate everything into the database and save all the LineItems that corresponded to that specific Order. Try adding some items to the cart, checking out, filling in correct information and placing the order, you’ll see that everything is placed perfectly in the database and a nice message appears at the catalog page.

 

Lastly we’ll be adding a small AJAX feature that will make the “Thank you for your order” message disappear when a user is starting a new order after placing and order before that. This can be done with a simple line of code to our add_to_cart RJS template:

page.replace_html("cart",:partial => "cart", :object => @cart)

page[:cart].visual_effect :blind_down if @cart.total_items == 1

page[:current_item].visual_effect :highlight,
                                  :startcolor => "#88ff88",
                                  :endcolor => "#114411"
page.select("div#notice").each {|div|div.hide}

The page will now browse through all the divs with the ‘notice’ ID and hide them on any request made, so that the thanking message will disappear after any user interaction.

 

Before we finish things off, I’m gonna add some things that weren’t specified in the book. If you, like me, noticed that there isn’t an orders controller (I thought it was going to be added later, but it didn’t), you should also, like me, feel the urge to add it. With no controller, we cannot view the orders that were made, which is bad because how would our depot admin know where to ship the products to? Anyway, let’s get started on that, first thing we’ll do is generate our controller with an index, show and destroy methods:

ruby script/generate controller orders index show

As you’ll see, this is a pretty basic controller that could be easily generated by scaffold, and there’s nothing different to explain about it, so here’s the whole code:

class OrdersController < ApplicationController

  def index
    @orders = Order.all
    respond_to do |format|
      format.html
      format.xml {render :xml => @orders}
    end
  end

  def show
    @order = Order.find(params[:id])
    respond_to do |format|
      format.html
      format.xml {render :xml => @order}
    end
  end

  def destroy
    @order = Order.find(params[:id])
    @order.destroy

    respond_to do |format|
      format.html { redirect_to(orders_url) }
      format.xml  { head :ok }
    end
  end
  
end

We added the method destroy by hand so the generator wouldn’t create a view file for it. Moving on to the view files, let’s start with the index page:

<h1>Listing orders</h1>
<table>
  <tr><th>Buyer Name</th></tr>
  <% for order in @orders %>
  <tr>
    <td><%=  link_to order.name, order %></td>
  </tr>
  <% end %>
</table>
<br />

Basic iteration through all orders with a link to each of them. On with the show page:

<p>
  <b>Buyer Name:</b>
  <%= h @order.name %>
</p>

<p>
  <b>Address:</b>
  <%= h @order.address %>
</p>

<p>
  <b>E-mail:</b>
  <%= h @order.email %>
</p>

<p>
  <b>Payment type:</b>
  <%= h @order.pay_type %>
</p>

<p>
  <b>Line Items:</b><br/>
  <% for item in @order.line_items  %>
  <%= item.quantity %> &times; <%= link_to(item.product.title, product_url(item.product))  %> = <%= number_to_currency item.total_price %><br/>
  <% end %>
</p>

<p>
  <b>Total price:</b>
  <%= number_to_currency @order.total %>
</p>

<%= link_to 'Back', orders_path %>
<%= link_to 'Destroy', @order ,:confirm => 'Are you sure?',:method => :delete %>

As learned in this task, an order has many line items, so to show an order, we need to iterate through every line item it has, which we did here by adding the line item quantity, the a link to its product and the price of that line item. Lastly there’s a call to a total method which isn’t defined in our order model, so let’s add that:

def total
  self.line_items.sum(:total_price)
end

The sum method iterates through all the line items and sums their total_price, returning a total order price.

That’s pretty much it, we now have an application with a cool shopping cart that is now able to place orders.