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

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