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

Chapter 10

Task E: Shipping

We’re now at the point where buyers can use our application to place orders. Our customer would like to see what it’s like to fulfill these orders.

Now, in a fully fledged store application, fulfillment would be a large, complex deal. We might need to integrate with various backend shipping agencies, we might need to generate feeds for customs information, and we’d probably need to link into some kind of accounting backend. We’re not going to do that here. But even though we’re going to keep it simple, we’ll still have the opportunity to experiment with partial templates, collections, and a slightly different interaction style to the one we’ve been using so far.

10.1 Iteration E1: Basic Shipping

We chat for a while with our customer about the shipping function. She says that she wants to see a list of the orders that haven’t yet been shipped. A shipping person will look through this list and fulfill one or more orders manually. Once the order had been shipped, the person would mark them as shipped in the system, and they’d no longer appear on the shipping page.

Our first task is to find some way of indicating whether an order has shipped. Clearly we need a new column in the orders table. We could make it a simple character column (perhaps with “Y” meaning shipped and “N” not shipped), but I prefer using timestamps for this kind of thing. If the column has a null value, the order has not been shipped. Otherwise, the value of the column is the date and time of the shipment. This way the column both tells us whether an order has shipped and, if so, when it shipped.

Prepared exclusively for Rida Al Barazi

ITERATION E1: BASIC SHIPPING 110

David Says. . .

Date and Timestamp Column Names

There’s a Rails column-naming convention that says datetime fields should end in _at and date fields should end in _on. This results in natural names for columns, such as last_edited_on and sent_at.

This is the convention that’s picked up by auto-timestamping, described on page 267, where columns with names such as created_at are automatically filled in by Rails.

So, let’s modify our create.sql file in the db directory, adding the shipped_at column to the orders table.

File 47

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,

 

shipped_at

datetime

null,

primary key (id) );

We load up the new schema.

depot> mysql depot_development <db/create.sql

To save myself having to enter product data through the administration pages each time I reload the schema, I also took this opportunity to write a simple set of SQL statements that loads up the product table. It could be something as simple as

lock tables products write; insert into products values(null,

'Pragmatic Project Automation', #title

'A really great read!',

#description

'/images/pic1.jpg',

#image_url

'29.95',

#price

'2004-12-25 05:00:00');

#date_available

insert into products values('',

 

'Pragmatic Version Control',

 

'A really controlled read!',

 

'/images/pic2.jpg',

 

'29.95',

 

'2004-12-25 05:00:00');

 

unlock tables;

 

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION E1: BASIC SHIPPING 111

File 43

File 44

File 46

Then load up the database.

depot> mysql depot_development <db/product_data.sql

We’re back working on the administration side of our application, so we’ll need to create a new action in the admin_controller.rb file. Let’s call it ship( ). We know its purpose is to get a list of orders awaiting shipping for the view to display, so let’s just code it that way and see what happens.

def ship

@pending_orders = Order.pending_shipping end

We now need to implement the pending_shipping( ) class method in the Order model. This returns all the orders with null in the shipped_at column.

def self.pending_shipping

find(:all, :conditions => "shipped_at is null") end

Finally, we need a view that will display these orders. The view has to contain a form, because there will be a checkbox associated with each order (the one the shipping person will set once that order has been dispatched). Inside that form we’ll have an entry for each order. We could include all the layout code for that entry within the view, but in the same way that we break complex code into methods, let’s split this view into two parts: the overall form and the part that renders the individual orders in that form. This is somewhat analogous to having a loop in code call a separate method to do some processing for each iteration.

We’ve already seen one way of handling these kinds of subroutines at the view level when we used components to show the cart contents on the checkout page. A lighter-weight way of doing the same thing is using a partial template. Unlike the component-based approach, a partial template has no corresponding action; it’s simply a chunk of template code that has been factored into a separate file.

Let’s create the overall ship.rhtml view in the directory app/views/admin.

Line 1 <h1>Orders To Be Shipped</h1>

-

- <%= form_tag(:action => "ship") %>

-

5 <table cellpadding="5" cellspacing="0">

-<%= render(:partial => "order_line", :collection => @pending_orders) %>

-</table>

-

-<br />

10 <input type="submit" value=" SHIP CHECKED ITEMS " />

-

-<%= end_form_tag %>

-<br />

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION E1: BASIC SHIPPING 112

Note the call to render( ) on line 6. The :collection parameter is the list of orders that we created in the action method. The :partial parameter performs double duty.

The first use of "order_line" is to identify the name of the partial template to render. This is a view, and so it goes into an .rhtml file just like other views. However, because partials are special, you have to name them with a leading underscore in the filename. In this case, Rails will look for the partial in the file app/views/admin/_order_line.rhtml.

The "order_line" parameter also tells Rails to set a local variable called order_line to the value of the order currently being rendered. This variable is available only inside the partial template. For each iteration over the collection of orders, order_line will be updated to reference the next order in the collection.

With all that explanation under our belts, we can now write the partial template, _order_line.rhtml.

File 45

<tr valign="top">

 

<td class="olnamebox">

 

<div class="olname"><%= h(order_line.name) %></div>

<div class="oladdress"><%= h(order_line.address) %></div>

</td>

<td class="olitembox">

<% order_line.line_items.each do |li| %>

<div class="olitem">

<span class="olitemqty"><%= li.quantity %></span> <span class="olitemtitle"><%= li.product.title %></span>

</div> <% end %>

</td>

<td>

<%= check_box("to_be_shipped", order_line.id, {}, "yes", "no") %>

</td> </tr>

So, using the store part of the application, create a couple of orders. Then switch across to localhost:3000/admin/ship. You’ll see something like Figure 10.1, on the following page. It worked, but it doesn’t look very pretty. On the store side of the application, we used a layout to frame all the pages and apply a common stylesheet. Before we go any further, let’s do the same here. In fact, Rails has already created the layout (when we first generated the admin scaffold). Let’s just make it prettier. Edit the file admin.rhtml in the app/views/layouts directory.

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION E1: BASIC SHIPPING 113

Figure 10.1: It’s a Shipping Page, But It’s Ugly

File 50

<html>

 

<head>

 

<title>ADMINISTER Pragprog Books Online Store</title>

 

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

 

</head>

 

<body>

 

<div id="banner">

 

<%= @page_title || "Administer Bookshelf" %>

 

</div>

 

<div id="columns">

 

<div id="side">

 

<%= link_to("Products", :action => "list") %>

 

<%= link_to("Shipping", :action => "ship") %>

 

</div>

 

<div id="main">

 

<% if @flash[:notice] -%>

 

<div id="notice"><%= @flash[:notice] %></div>

 

<% end -%>

 

<%= @content_for_layout %>

 

</div>

 

</div>

 

</body>

 

</html>

 

Here we’ve used the stylesheet_link_tag( ) helper method to create links to

 

scaffold.css, depot.css, and a new admin.css stylesheet. (I like to set different

 

color schemes in the administration side of a site so that it’s immediately

 

obvious that you’re working there.) And now we have a dedicated CSS file

 

for the administration side of the application, we’ll move the list-related

 

styles we added to scaffold.css back on page 65 into it. The admin.css file is

 

listed Section C.1, CSS Files, on page 508.

 

When we refresh our browser, we see the prettier display that follows.

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION E1: BASIC SHIPPING 114

Now we have to figure out how to mark orders in the database as shipped when the person doing the shipping checks the corresponding box on the form. Notice how we declared the checkbox in the partial template,

_order_line.rhtml.

<%= check_box("to_be_shipped", order_line.id, {}, "yes", "no") %>

The first parameter is the name to be used for this field. The second parameter is also used as part of the name, but in an interesting way. If you look at the HTML produced by the check_box( ) method, you’ll see something like

<input name="to_be_shipped[1]" type="checkbox" value="yes" />

In this example, the order id was 1, so Rails used the name to_be_shipped[1] for the checkbox.

The last three parameters to check_box( ) are an (empty) set of options, and the values to use for the checked and unchecked states.

When the user submits this form back to our application, Rails parses the form data and detects these fields with index-like names. It splits them out, so that the parameter to_be_shipped will point to a Hash, where the keys are the index values in the name and the value is the value of the corresponding form tag. (This process is explained in more detail on page 341.) In the case of our example, if just the single checkbox for

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION E1: BASIC SHIPPING 115

the order with an id of 1 was checked, the parameters returned to our controller would include

@params = { "to_be_shipped" => { "1" => "yes" } }

Because of this special handling of forms, we can iterate over all the checkboxes in the response from the browser and look for those that the shipping person has checked.

to_ship = params[:to_be_shipped] if to_ship

to_ship.each do |order_id, do_it| if do_it == "yes"

# mark order as shipped...

end end

end

We have to work out where to put this code. The answer depends on the workflow we want the shipping person to see, so we wander over and chat with our customer. She explains that there are multiple workflows when shipping. Sometimes you might run out of a particular item in the shipping area, so you’d like to skip them for a while until you get a chance to restock from the warehouse. Sometimes the shipper will try to ship things with the same style packaging and then move on to items with different packaging. So, our application shouldn’t enforce just one way of working.

After chatting for a while, we come up with a simple design for the shipping function. When a shipping person selects the shipping function, the function displays all orders that are pending shipping. The shipping person can work through the list any way they want, clicking the checkbox when they ship a particular order. When they eventually hit the Ship Checked Items button, the system will update the orders in the database and redisplay the items still remaining to be shipped. Obviously this scheme works only if shipping is handled by just one person at a time (because two people using the system concurrently could both choose to ship the same orders). Fortunately, our customer’s company has just one shipping person.

Given that information, we can now implement the complete ship( ) action in the admin_controller.rb controller. While we’re at it, we’ll keep track of how many orders get marked as shipped each time the form is submitted—this lets us write a nice flash notice.

Note that the ship( ) method does not redirect at the end—it simply redisplays the ship view, updated to reflect the items we just shipped. Because

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION E1: BASIC SHIPPING 116

 

of this, we use the flash in a new way. The flash.now facility adds a mes-

 

 

sage to the flash for just the current request. It will be available when we

 

 

render the ship template, but the message will not be stored in the session

 

 

and made available to the next request.

 

File 48

def ship

 

 

count = 0

 

 

if things_to_ship = params[:to_be_shipped]

 

 

count = do_shipping(things_to_ship)

 

 

if count > 0

 

 

 

count_text = pluralize(count, "order")

 

 

 

flash.now[:notice] = "#{count_text} marked as shipped"

 

 

end

 

 

end

 

 

 

@pending_orders = Order.pending_shipping

 

 

end

 

 

 

private

 

 

def do_shipping(things_to_ship)

 

 

count = 0

 

 

things_to_ship.each do |order_id, do_it|

 

 

if do_it == "yes"

 

 

 

order = Order.find(order_id)

 

 

 

order.mark_as_shipped

 

 

 

order.save

 

 

 

count += 1

 

 

end

 

 

end

 

 

 

count

 

 

end

 

 

 

def pluralize(count, noun)

 

 

case count

pluralize

 

when 0: "No #{noun.pluralize}"

page 186

 

when 1: "One #{noun}"

 

 

else

"#{count} #{noun.pluralize}"

 

 

end

 

 

 

end

 

 

 

We also need to add the mark_as_shipped( ) method to the Order model.

 

File 49

def mark_as_shipped

 

 

self.shipped_at = Time.now

 

 

end

 

 

Now when we mark something as shipped and click the button, we get the nice message shown in Figure 10.2, on the following page.

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION E1: BASIC SHIPPING 117

Figure 10.2: Status Messages During Shipping

What We Just Did

This was a fairly small task. We saw how to do the following.

We can use partial templates to render sections of a template and helpers such as render( ) with the :collection parameter to invoke a partial template for each member of a collection.

We can represent arrays of values on forms (although there’s more to learn on this subject).

We can cause an action to loop back to itself to generate the effect of a dynamically updating display.

Prepared exclusively for Rida Al Barazi

Report erratum