Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 1st Edition (2005).pdf
Скачиваний:
34
Добавлен:
17.08.2013
Размер:
7.99 Mб
Скачать

Chapter 9

Task D: Checkout!

Let’s take stock. So far, we’ve put together a basic product administration system, we’ve implemented a catalog, and we have a pretty spiffy-looking shopping cart. So now we need to let the buyer actually purchase the contents of that cart. Let’s go ahead and implement the checkout function.

We’re not going to go overboard here. For now, all we’ll do is capture the customer’s contact details and payment option. Using these we’ll construct an order in the database. Along the way we’ll be looking a bit more at models, validation, form handling, and components.

Joe Asks. . .

Where’s the Credit-Card Processing?

At this point, our tutorial application is going to diverge slightly from reality. In the real world, we’d probably want our application to handle the commercial side of checkout. We might even want to integrate creditcard processing (possibly using the Payment module). However, integrating with backend payment processing systems requires a fair amount of paperwork and jumping through hoops. And this would distract from looking at Rails, so we’re going to punt on this particular detail.

http://rubyforge.org/projects/payment

Prepared exclusively for Rida Al Barazi

ITERATION D1: CAPTURING AN ORDER 96

9.1 Iteration D1: Capturing an Order

An order is a set of line items, along with details of the purchase transaction. We already have the line items—we defined them when we created the shopping cart in the previous chapter. We don’t yet have a table to contain orders. Based on the diagram on page 47, combined with a brief chat with our customer, we can create the orders table.

File 37

create table orders (

 

 

id

int

not null auto_increment,

 

name

varchar(100)

not null,

 

email

varchar(255)

not null,

 

address

text

not null,

 

pay_type

char(10)

not null,

primary key (id) );

We know that when we create a new order, it will be associated with one or more line items. In database terms, this means that we’ll need to add a foreign key reference from the line_items table to the orders table, so we’ll take this opportunity to update the DDL for line items too. (Have a look at the listing of create.sql on page 487 to see how the drop table statements should be added.)

File 37

create table

line_items (

 

 

id

int

not null auto_increment,

 

product_id

int

not null,

 

order_id

int

not null,

 

quantity

int

not null default 0,

 

unit_price

decimal(10,2)

not null,

 

constraint

fk_items_product

foreign key (product_id) references products(id),

 

constraint

fk_items_order

foreign key (order_id) references orders(id),

primary key (id) );

 

Remember to update the schema (which will empty your database of any

 

data it contains) and create the Order model using the Rails generator. We

 

don’t regenerate the model for line items, as the one that’s there is fine.

 

depot> mysql depot_development <db/create.sql

 

depot> ruby script/generate model Order

 

That told the database about the foreign keys. This is a good thing, as

 

many databases will check foreign key constraints, keeping our code hon-

 

est. But we also need to tell Rails that an order has many line items and

 

that a line item belongs to an order. First, we open up the newly created

 

order.rb file in app/models and add a call to has_many( ).

File 32

class Order < ActiveRecord::Base

 

has_many :line_items

 

end

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION D1: CAPTURING AN ORDER

97

 

Next, we’ll specify a link in the opposite direction, adding a call to the

 

method belongs_to( ) in the line_item.rb file. (Remember that a line item was

 

already declared to belong to a product when we set up the cart.)

File 31

class LineItem < ActiveRecord::Base

 

belongs_to :product

 

belongs_to :order

 

# . . .

 

We’ll need an action to orchestrate capturing order details. In the previous

 

chapter we set up a link in the cart view to an action called checkout, so

 

now we have to implement a checkout( ) method in the store controller.

File 30

def checkout

@cart = find_cart

 

@items = @cart.items

empty?

if @items.empty?

page 479

redirect_to_index("There's nothing in your cart!")

 

else

 

@order = Order.new

 

end

 

end

 

Notice how we first check to make sure that there’s something in the cart.

 

This prevents people from navigating directly to the checkout option and

 

creating empty orders.

 

Assuming we have a valid cart, we create a new Order object for the view to

 

fill in. Note that this order isn’t yet saved in the database—it’s just used

 

by the view to populate the checkout form.

 

The checkout view will be in the file checkout.rhtml in the app/views/store

 

directory. Let’s build something simple that will show us how to marry

 

form data to Rails model objects. Then we’ll add validation and error han-

 

dling. As always with Rails, it’s easiest to start with the basics and iterate

 

towards nirvana (the state of being, not the band).

 

Rails and Forms

Rails has great support for getting data out of relational databases and into Ruby objects. So you’d expect to find that it has corresponding support for transferring that data back and forth between those model objects and users on browsers.

We’ve already seen one example of this. When we created our product administration controller, we used the scaffold generator to create a form that captures all the data for a new product. If you look at the code for that view (in app/views/admin/new.rhtml), you’ll see the following.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

ITERATION D1: CAPTURING AN ORDER

98

File 34

<h1>New product</h1>

 

 

 

<%= start_form_tag :action => 'create' %>

 

 

<%= render_partial "form" %>

 

 

 

<%= submit_tag "Create" %>

 

 

 

<%= end_form_tag %>

 

 

 

<%= link_to 'Back', :action => 'list' %>

 

 

This references a subform using render_partial('form').1 That subform, in

 

 

the file _form.rhtml, captures the information about a product.

 

File 33

<%= error_messages_for 'product'

%>

 

 

<!--[form:product]-->

 

 

 

<p><label for="product_title">Title</label><br/>

 

 

<%= text_field 'product', 'title'

%></p>

 

 

<p><label for="product_description">Description</label><br/>

 

 

<%= text_area 'product', 'description', :rows => 5 %></p>

 

 

<p><label for="product_image_url">Image url</label><br/>

 

 

<%= text_field 'product', 'image_url' %></p>

 

 

<p><label for="product_price">Price</label><br/>

 

 

<%= text_field 'product', 'price'

%></p>

 

<p><label for="product_date_available">Date available</label><br/> <%= datetime_select 'product', 'date_available' %></p> <!--[eoform:product]-->

We could use the scaffold generator to create a form for the orders table too, but the Rails-generated form is not all that pretty. We’d like to produce something nicer. Let’s dig further into all those methods in the autogenerated form before creating the data entry form for ourselves.

Rails has model-aware helper methods for all the standard HTML input tags. For example, say we need to create an HTML <input> tag to allow the buyer to enter their name. In Rails, we could write something like the following in the view.

<%= text_field("order", "name", :size => 40 ) %>

Here, text_field( ) will create an HTML <input> tag with type="text". The neat thing is that it would populate that field with the contents of the name field in the @order model. What’s more, when the end user eventually submits the form, the model will be able to capture the new value of this field from the browser’s response and store it, ready to be written to the database as required.

There are a number of these form helper methods (we’ll look at them in more detail starting on page 332). In addition to text_field( ), we’ll be using

1render_partial( ) is a deprecated form of render(:partial=>...). The scaffold generators had not been updated to create code using the newer form at the time this book was written.

Prepared exclusively for Rida Al Barazi

Report erratum

 

ITERATION D1: CAPTURING AN ORDER

99

 

text_area( ) to capture the buyer’s address and select( ) to create a selection

 

 

list for the payment options.

 

 

Of course, for Rails to get a response from the browser, we need to link

 

 

the form to a Rails action. We could do that by specifying a link to our

 

 

application, controller, and action in the action= attribute of a <form> tag,

 

 

but it’s easier to use form_tag( ), another Rails helper method that does the

 

 

work for us.

 

 

So, with all that background out of the way, we’re ready to create the view

 

 

to capture the order information. Here’s our first attempt at the check-

 

 

out.rhtml file in app/views/store directory.

 

File 36

<% @page_title = "Checkout" -%>

 

 

<%= start_form_tag(:action => "save_order") %>

 

 

<table>

 

<tr> <td>Name:</td>

<td><%= text_field("order", "name", "size" => 40 ) %></td>

</tr> <tr>

<td>EMail:</td>

<td><%= text_field("order", "email", "size" => 40 ) %></td>

</tr>

<tr valign="top"> <td>Address:</td>

<td><%= text_area("order", "address", "cols" => 40, "rows" => 5) %></td>

</tr>

<tr>

<td>Pay using:</td> <td><%=

options = [["Select a payment option", ""]] + Order::PAYMENT_TYPES select("order", "pay_type", options)

%></td> </tr> <tr>

<td></td>

<td><%= submit_tag(" CHECKOUT ") %></td>

</tr> </table>

<%= end_form_tag %>

The only tricky thing in there is the code associated with the selection list. We’ve assumed that the list of available payment options is an attribute of the Order model—it will be an array of arrays in the model file. The first element of each subarray is the string to be displayed as option in the selection and the second value gets stored in the database.2 We’d better define the option array in the model order.rb before we forget.

2If we anticipate that other non-Rails applications will update the orders table, we might want to move the list of payment types into a separate lookup table and make the orders column a foreign key referencing that new table. Rails provides good support for generating selection lists in this context too.

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION D1: CAPTURING AN ORDER 100

Figure 9.1: Our First Checkout Page

File 32

PAYMENT_TYPES = [

 

 

 

[ "Check",

 

"check" ],

 

[

"Credit Card",

"cc"

],

 

[

"Purchase Order", "po"

]

 

].freeze

# freeze to make this array constant

If there’s no current selection in the model, we’d like to display some prompt text in the browser field. We do this by merging a new option at the start of the selection options returned by the model. This new option has an appropriate display string and a blank value.

So, with all that in place, we can fire up our trusty browser. Add some items to the cart, and click the checkout link. You’ll see a shiny new checkout page like the one in Figure 9.1 .

Looking good! Of course, if you click the Checkout button, you’ll be greeted with

Unknown action

No action responded to save_order

So let’s get on and implement the save_order( ) action in our store controller.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

ITERATION D1: CAPTURING AN ORDER

101

 

This method has to

 

 

1. Capture the values from the form to populate a new Order model

 

 

 

object.

 

 

2. Add the line items from our cart to that order.

 

 

3. Validate and save the order. If this fails, display the appropriate mes-

 

 

 

sages and let the user correct any problems.

 

 

4. Once the order is successfully saved, redisplay the catalog page,

 

 

 

including a message confirming that the order has been placed.

 

 

The method ends up looking something like this.

 

File 30

Line 1

def save_order

 

 

-

@cart = find_cart

 

 

-

@order = Order.new(params[:order])

 

 

-

@order.line_items << @cart.items

 

 

5

if @order.save

 

 

-

@cart.empty!

 

 

-

redirect_to_index('Thank you for your order.')

 

 

-

else

 

 

-

render(:action => 'checkout')

 

10 end

-end

On line 3, we create a new Order object and initialize it from the form data. In this case we want all the form data related to order objects, so we select the :order hash from the parameters (we’ll talk about how forms are linked to models on page 341). The next line adds into this order the line items that are already stored in the cart—the session data is still there throughout this latest action. Notice that we didn’t have to do anything special with the various foreign key fields, such as setting the order_id column in the line item rows to reference the newly created order row. Rails does that knitting for us using the has_many( ) and belongs_to( ) declarations we added to the Order and LineItem models.

Next, on line 5, we tell the order object to save itself (and its children, the line items) to the database. Along the way, the order object will perform validation (but we’ll get to that in a minute). If the save succeeds, we empty out the cart ready for the next order and redisplay the catalog, using our redirect_to_index( ) method to display a cheerful message. If instead the save fails, we redisplay the checkout form.

One last thing before we call our customer over. Remember when we showed her the first product maintenance page? She asked us to add validation. We should probably do that for our checkout page too. For now we’ll just check that each of the fields in the order has been given a

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION D1: CAPTURING AN ORDER 102

Joe Asks. . .

Aren’t You Creating Duplicate Orders?

Joe’s concerned to see our controller creating Order model objects in two actions, checkout and save_order. He’s wondering why this doesn’t lead to duplicate orders in the database.

The answer is simple: the checkout action creates an Order object in memory simply to give the template code something to work with. Once the response is sent to the browser, that particular object gets abandoned, and it will eventually be reaped by Ruby’s garbage collector. It never gets close to the database.

The save_order action also creates an Order object, populating it from the form fields. This object does get saved in the database.

So, model objects perform two roles: they map data into and out of the database, but they are also just regular objects that hold business data. They affect the database only when you tell them to, typically by calling save( ).

 

value. We know how to do this—we add a validates_presence_of( ) call to the

 

Order model.

File 32

validates_presence_of :name, :email, :address, :pay_type

 

So, as a first test of all of this, hit the Checkout button on the checkout

 

page without filling in any of the form fields. We expect to see the checkout

 

page redisplayed along with some error messages complaining about the

 

empty fields. Instead, we simply see the checkout page—no error mes-

 

sages. We forgot to tell Rails to write them out.3

 

Any errors associated with validating or saving a model are stored with that

 

model. There’s another helper method, error_messages_for( ), that extracts

 

and formats these in a view. We just need to add a single line to the start

 

of our checkout.rhtml file.

File 36

<%= error_messages_for("order") %>

 

 

 

 

3If you’re following along at home and you get the message No action responded to

 

save_order, it’s possible that you added the save_order( ) method after the private declaration

 

in the controller. Private methods cannot be called as actions.

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION D1: CAPTURING AN ORDER 103

Figure 9.2: Full House! Every Field Fails Validation

 

Just as with the administration validation, we need to add the scaffold.css

 

stylesheet to our store layout file to get decent formatting for these errors.

File 35

<%= stylesheet_link_tag "scaffold", "depot", :media => "all" %>

 

Once we do that, submitting an empty checkout page shows us a lot of

 

highlighted errors, as shown in Figure 9.2 .

 

If we fill in some data as shown at the top of Figure 9.3, on page 105, and

 

click Checkout , we should get taken back to the catalog, as shown at the

 

bottom of the figure. But did it work? Let’s look in the database.

Prepared exclusively for Rida Al Barazi

Report erratum