- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
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, |
|
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