- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •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 A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
ITERATION C2: A SMAR TER CAR T 110
our dismay, she makes that tsk-tsk sound that customers make just before telling you that you clearly don’t get something.
Real shopping carts, she explains, don’t show separate lines for two of the same product. Instead, they show the product line once with a quantity of 2. Looks like we’re lined up for our next iteration.
8.3Iteration C2: A Smarter Cart
It looks like we have to find a way to associate a count with each product in our cart. Let’s create a new model class, CartItem, which contains both a reference to a product and a quantity.
Download depot_g/app/models/cart_item.rb
class CartItem
attr_reader :product, :quantity
def initialize(product) @product = product @quantity = 1
end
def increment_quantity @quantity += 1
end
def title @product.title
end
def price
@product.price * @quantity end
end
We’ll now use this from within the add_product method in our Cart. We see whether our list of items already includes the product we’re adding; if it does, we bump the quantity, and otherwise we add a new CartItem.
Download depot_g/app/models/cart.rb
def add_product(product)
current_item = @items.find {|item| item.product == product} if current_item
current_item.increment_quantity else
@items << CartItem.new(product) end
end
Report erratum
ITERATION C2: A SMAR TER CAR T 111
We’ll also make a quick change to the add_to_cart view to use this new information.
Download depot_g/app/views/store/add_to_cart.rhtml
<h1>Your Pragmatic Cart</h1>
<ul>
<% for cart_item in @cart.items %>
<li><%= cart_item.quantity %> × <%= h(cart_item.title) %></li> <% end %>
</ul>
By now we’re pretty confident in our Rails-fu, so we confidently go to the store page and hit the Add to Cart button for a product. And, of course, there’s nothing like a little hubris to trigger a reality check. Rather than seeing our new cart, we’re faced with a somewhat brutal error screen, shown here.
At first, we might be tempted to think that we’d misspelled something in cart.rb, but a quick check shows that it’s OK. But then, we look at the error message more closely. It says “undefined method ‘product’ for #<Product:...>.” That means that it thinks the items in our cart are products, not cart items. It’s almost as if Rails hasn’t spotted the changes we’ve made.
But, looking at the source, the only time we reference a product method, we’re calling it on a CartItem object. So, why does it think the @items array contains products when our code clearly populates it with cart items?
To answer this, we have to ask where the cart that we’re adding to comes from. That’s right. It’s in the session. And the cart in the session is the old version, the one where we just blindly appended products to the @items array. So, when Rails pulls the cart out of the session, it’s getting a cart full of product objects, not cart items. And that’s our problem.
The easiest way to confirm this is to delete the old session, removing all traces of the original cart implementation. Because we’re using database-backed sessions, we can use a handy Rake task to clobber the session table.
depot> rake db:sessions:clear
Report erratum
ITERATION C2: A SMAR TER CAR T 112
Now hit Refresh, and you’ll see the application is running the new cart and the new add_to_cart view.
The Moral of the Tale
Our problem was caused by the session storing the old version of the cart object, which wasn’t compatible with our new source file. We fixed that by blowing away the old session data. Because we’re storing full objects in the session data, whenever we change our application’s source code, we potentially become incompatible with this data, and that can lead to errors at runtime. This isn’t just a problem during development.
Say we rolled out version one of our Depot application, using the old version of the cart. We have thousands of customers busily shopping. We then decide to roll out the new, improved cart model. The code goes into production, and suddenly all the customers who are in the middle of a shopping spree find they’re getting errors when adding stuff to the cart. Our only fix is to delete the session data, which loses our customers’ carts.
This tells us that it’s generally a really bad idea to store application-level objects in session data. Any change to the application could potentially require us to lose existing sessions when we next update the application in production.
Instead, the recommended practice is to store only simple data in the session: strings, numbers, and so on. Keep your application objects in the database, and then reference them using their primary keys from the session data. If we were rolling the Depot application into production, we’d be wise to make the Cart class an Active Record object and store cart data in the database.3 The session would then store the cart object’s id. When a request comes in, we’d extract this id from the session and then load the cart from the database.4 Although this won’t automatically catch all problems when you update your application, it gives you a fighting chance of dealing with migration issues.
Anyway, we’ve now got a cart that maintains a count for each of the products that it holds, and we have a view that displays that count. Figure 8.1, on the following page shows what this looks like.
Happy that we have something presentable, we call our customer over and show her the result of our morning’s work. She’s pleased—she can see the site starting to come together. However, she’s also troubled, having just read an article in the trade press on the way e-commerce sites are being attacked and compromised daily. She read that one kind of attack involves feeding requests with bad parameters into web applications, hoping to expose bugs
3.But we won’t for this demonstration application, because we wanted to illustrate the problems.
4.In fact, we can abstract this functionality into something called a filter and have it happen
automatically. We’ll cover filters starting on page 447.
Report erratum
ITERATION C3: HANDLING ERRORS 113
Figure 8.1: A Cart with Quantities
and security flaws. She noticed that the link to add an item to our cart looks like store/add_to_cart/nnn, where nnn is our internal product id. Feeling malicious, she manually types this request into a browser, giving it a product id of “wibble.” She’s not impressed when our application displays the page in Figure 8.2, on the next page. This reveals way too much information about our application. It also seems fairly unprofessional. So it looks as if our next iteration will be spent making the application more resilient.
8.4Iteration C3: Handling Errors
Looking at the page displayed in Figure 8.2, it’s apparent that our application threw an exception at line 16 of the store controller.5 That turns out to be the line
product = Product.find(params[:id])
If the product cannot be found, Active Record throws a RecordNotFound exception,6 which we clearly need to handle. The question arises—how?
We could just silently ignore it. From a security standpoint, this is probably the best move, because it gives no information to a potential attacker. However, it also means that should we ever have a bug in our code that gener-
5.Your line number might be different. We have some book-related formatting stuff in our source files.
6.This is the error thrown when running with MySQL. Other databases might cause a different
error to be raised. If you use PostgreSQL, for example, it will refuse to accept wibble as a valid value for the primary key column and raise a StatementInvalid exception instead. You’ll need to adjust your error handling accordingly.
Report erratum
ITERATION C3: HANDLING ERRORS 114
Figure 8.2: Our Application Spills Its Guts
ates bad product ids, our application will appear to the outside world to be unresponsive—no one will know there has been an error.
Instead, we’ll take three actions when an exception is thrown. First, we’ll log the fact to an internal log file using Rails’ logger facility (described on page 243). Second, we’ll output a short message to the user (something along the lines of “Invalid product”). And third, we’ll redisplay the catalog page so they can continue to use our site.
The Flash!
As you may have guessed, Rails has a convenient way of dealing with errors and error reporting. It defines a structure called a flash. A flash is a bucket (actually closer to a Hash) in which you can store stuff as you process a request. The contents of the flash are available to the next request in this session before being deleted automatically. Typically the flash is used to collect error messages. For example, when our add_to_cart action detects that it was passed an invalid product id, it can store that error message in the flash area and redirect to the index action to redisplay the catalog. The view for the index action can extract the error and display it at the top of the catalog page. The flash information is accessible within the views by using the flash accessor method.
Why couldn’t we just store the error in any old instance variable? Remember that after a redirect is sent by our application to the browser, the browser sends a new request back to our application. By the time we receive that
Report erratum
ITERATION C3: HANDLING ERRORS 115
request, our application has moved on—all the instance variables from previous requests are long gone. The flash data is stored in the session in order to make it available between requests.
Armed with all this background about flash data, we can now change our add_to_cart method to intercept bad product ids and report on the problem.
Download depot_h/app/controllers/store_controller.rb
def add_to_cart begin
product = Product.find(params[:id]) rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}") flash[:notice] = "Invalid product"
redirect_to :action => :index else
@cart = find_cart @cart.add_product(product)
end end
The rescue clause intercepts the exception thrown by Product.find. In the handler we
•Use the Rails logger to record the error. Every controller has a logger attribute. Here we use it to record a message at the error logging level.
•Create a flash notice with an explanation. Just as with sessions, you access the flash as if it were a hash. Here we used the key :notice to store our message.
•Redirect to the catalog display using the redirect_to method. This takes a wide range of parameters (similar to the link_to method we encountered in the templates). In this case, it instructs the browser to immediately request the URL that will invoke the current controller’s index action. Why redirect, rather than just display the catalog here? If we redirect, the user’s browser will end up displaying a URL of http://.../store/index, rather than http://.../store/add_to_cart/wibble. We expose less of the application this way. We also prevent the user from retriggering the error by hitting the Reload button.
This code uses a little-known feature of Ruby’s exception handling. The else clause invokes the code that follows only if no exception is thrown. It allows us to specify one path through the action if the exception is thrown and another if it isn’t.
With this code in place, we can rerun our customer’s problematic query. This time, when we enter the URL
http://localhost:3000/store/add_to_cart/wibble
Report erratum
ITERATION C3: HANDLING ERRORS 116
we don’t see a bunch of errors in the browser. Instead, the catalog page is displayed. If we look at the end of the log file (development.log in the log directory), we’ll see our message.7
Parameters: {"action"=>"add_to_cart", "id"=>"wibble", "controller"=>"store"}
Product Load (0.000427) SELECT * FROM products WHERE (products.id = 'wibble') LIMIT 1
Attempt to access invalid product wibble
Redirected to http://localhost:3000/store/index Completed in 0.00522 (191 reqs/sec) . . .
Processing StoreController#index ...
::
Rendering within layouts/store
Rendering store/index
So, the logging worked. But the flash message didn’t appear on the user’s browser. That’s because we didn’t display it. We’ll need to add something to the layout to tell it to display flash messages if they exist. The following rhtml code checks for a notice-level flash message and creates a new <div> containing it if necessary.
<% if flash[:notice] -%>
<div id="notice"><%= flash[:notice] %></div> <% end -%>
So, where do we put this code? We could put it at the top of the catalog display template—the code in index.rhtml. After all, that’s where we’d like it to appear right now. But as we continue to develop the application, it would be nice if all pages had a standardized way of displaying errors. We’re already using a Rails layout to give all the store pages a consistent look, so let’s add the flashhandling code into that layout. That way if our customer suddenly decides that errors would look better in the sidebar, we can make just one change and all our store pages will be updated. So, our new store layout code now looks as follows.
Download depot_h/app/views/layouts/store.rhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot", :media => "all" %>
</head>
7. On Unix machines, we’d probably use a command such as tail or less to view this file. On Windows, you could use your favorite editor. It’s often a good idea to keep a window open showing new lines as they are added to this file. In Unix you’d use tail -f. You can download a tail command for Windows from http://gnuwin32.sourceforge.net/packages/coreutils.htm or get a GUI-based tool from http://tailforwin32.sourceforge.net/. Finally, some OS X users find Console.app (in Applications → Utilities) a convenient way to track log files. Use the open command, passing it the name of the log file.
Report erratum
ITERATION C3: HANDLING ERRORS 117
<body id="store"> <div id="banner">
<img src="/images/logo.png" />
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns"> <div id="side">
<a href="http://www....">Home</a><br />
<a href="http://www..../faq">Questions</a><br /> <a href="http://www..../news" >News</a><br />
<a href="http://www..../contact">Contact</a><br />
</div>
<div id="main">
<% if flash[:notice] -%>
<div id="notice"><%= flash[:notice] %></div> <% end -%>
<%= yield :layout %>
</div>
</div>
</body>
</html>
We’ll also need a new CSS styling for the notice box.
Download depot_h/public/stylesheets/depot.css
#notice {
border: 2px solid red; padding: 1em; margin-bottom: 2em; background-color: #f0f0f0;
font: bold smaller sans-serif;
}
This time, when we manually enter the invalid product code, we see the error reported at the top of the catalog page.
Sensing the end of an iteration, we call our customer over and show her that the error is now properly handled. She’s delighted and continues to play with
Report erratum