6. PROPUESTA DE INTERVENCIÓN
6.1 Acciones de mejora de cada uno de los puntos analizados.
Observant readers (yes, that’s all of you) will have noticed that our catalog
listing view already includes anAdd to Cart link in each product listing.
File 79 <%= link_to 'Add to Cart',
{:action => 'add_to_cart', :id => product }, :class => 'addtocart' %><br/>
This link points back to anadd_to_cart( ) action in the store controller and
Report erratum
ITERATIONC1: CREATING ACAR T 79
will pass in the product id as a form parameter.3 Here’s where we start
to see how important theid field is in our models. Rails identifies model
objects (and the corresponding database rows) by theiridfields. If we pass
an id toadd_to_cart( ), we’re uniquely identifying the product to add.
Let’s implement the add_to_cart( ) method now. It needs to find the shop-
ping cart for the current session (creating one if there isn’t one there already), add the selected product to that cart, and display the cart con- tents. So, rather than worry too much about the details, let’s just write
the code at this level of abstraction. We’ll create anadd_to_cart( ) method
in app/controllers/store_controller.rb. It uses the params object to get theid parameter from the request and then finds the corresponding product, and
it uses thefind_cart( ) method we created earlier to find the cart in the ses-
sion and add the product to it. Be careful when you add theadd_to_cart( )
method to the controller. Because it is called as an action, it must be pub-
lic and so must be added above the private directive we put in to hide the
find_cart( ) method.
File 75 def add_to_cart
product = Product.find(params[:id]) @cart = find_cart
@cart.add_product(product)
redirect_to(:action => 'display_cart')
end
Clearly, this code isn’t going to run yet: we haven’t yet created aCartclass,
and we don’t have any implementation of thedisplay_cart( ) functionality.
Let’s start off with theCartclass and itsadd_product( ) method. As it stores
application data, it is logically part of our model, so we’ll create the file cart.rb in the directory app/models. However, it isn’t tied to a database
table, so it’s not a subclass ofActiveRecord::Base.
File 76 class Cart
attr_reader :items attr_reader :total_price def initialize @items = [] @total_price = 0.0 end def add_product(product) << →page474 @items << LineItem.for_product(product) @total_price += product.price end end
3Saying:id => productis idiomatic shorthand for:id => product.id. Both pass the product’s id
back to the controller.
Report erratum
ITERATIONC1: CREATING ACAR T 80
That was pretty straightforward. We create a new line item based on the product and add it to the list. Of course, we haven’t yet got a method that creates a line item based on information in a product, so let’s rectify
that now. We’ll open up app/models/line_item.rb and add a class method
for_product( ). Creating these class-level helper methods is a neat trick for keeping your code readable.
File 77 class LineItem < ActiveRecord::Base
belongs_to :product
def self.for_product(product) self.new
→page479 item = self.new
item.quantity = 1 item.product = product item.unit_price = product.price item end end
So, let’s see where we are. We created a Cartclass to hold our line items,
and we implemented the add_to_cart( ) method in the controller. That in
turn calls the newfind_cart( ) method, which makes sure that we keep the
cart object in the session.
We still need to implement thedisplay_cart( ) method and the corresponding
view. At the same time, we’ve written a whole lot of code without trying it out, so let’s just slap in a couple of stubs and see what happens. In the store controller, we’ll implement an action method to handle the incoming request.
File 75 def display_cart
@cart = find_cart @items = @cart.items
end
Over in theapp/views/storedirectory we’ll create a stub for the correspond-
ing view,display_cart.rhtml.
File 78 <h1>Display Cart</h1>
<p>
Your cart contains <%= @items.size %> items.
</p>
So, with everything plumbed together, let’s have a look at our store in a
browser. Navigating tohttp://localhost:3000/storebrings up our catalog page.
Click on theAdd to Cart links for one of the products.4 We expect to see
the stub cart display, but instead we are faced with a somewhat brutal
4If you don’t see a list of products, you’ll need to go back to the administration section of
the application and add some.
Report erratum
ITERATIONC1: CREATING ACAR T 81
page. (Although not in Rails 0.13.1 or later, where the steps on this page are no longer necessary.)
At first, we might be tempted to think that we’d mispelled the name of the action method or the name of the view, but that’s not the case. This isn’t a Rails error message—it comes straight from WEBrick. To find out what’s going on, we need to look at the WEBrick console output. Find the window where WEBrick is running, and you’ll see it is full of logging and trace messages. The trace indicates that something has gone wrong in the application. (Technically, this is a stack backtrace, which shows the chain of method calls that got us to the point where the application choked.) The easy way to find out what went wrong is to scroll back through the trace. Just before it starts, you’ll see an error message.
#<ActionController::SessionRestoreError: Session contained
objects where the class definition wasn't available. Remember to require classes for all objects kept in the session. The session has been deleted.>
When Rails tried to load the session information from the cookie that came from the browser, it came across some classes that it didn’t know about.
We’ll have to tell Rails about ourCartandLineItemclasses. (The sidebar on
page83explains why.) Over inapp/controllersyou’ll find a file calledappli-
cation.rb. This file is used to establish a context for the entire application.
By default, it contains an empty definition of class ApplicationController.
We’ll need to add two lines to this class to declare our new model files.
File 74 class ApplicationController < ActionController::Base
model :cart model :line_item
end
Now if we hit Refresh in our browser, we should see our stub view displayed
(see Figure8.2, on the next page). If we use the Back button to return to
the catalog display and add another product to the cart, you’ll see the
Report erratum
ITERATIONC1: CREATING ACAR T 82
Figure 8.2: Our Cart Now Has an Item in It
count update when the display cart page is displayed. It looks like we have sessions working.
Phew! That was probably the hardest thing we’ve done so far. It was definitely the longest time we’ve spent without having anything to show our customer (or ourselves). But now that we have everything linked together correctly, let’s quickly implement a simple cart display so we can get some
customer feedback. We’ll replace the stub code in display_cart.rhtml with
code that displays the items in the cart.
File 82 <h1>Display Cart</h1>
<table> <%
for item in @items product = item.product -%>
<tr>
<td><%= item.quantity %></td>
<td><%= h(product.title) %></td>
<td align="right"><%= item.unit_price %></td>
<td align="right"><%= item.unit_price * item.quantity %></td>
</tr> <% end -%>
</table>
This template illustrates one more feature of ERb. If we end a piece of
embedded Ruby with-%>(note the extra minus sign), ERb will suppress the
newline that follows. That means embedded Ruby that doesn’t generate any output won’t end up leaving extraneous blank lines in the output.
Report erratum
ITERATIONC1: CREATING ACAR T 83
Session Information, Serialization, and Classes
The session construct stores the objects that you want to keep around between browser requests. To make this work, Rails has to take these objects and store them at the end of one request, loading them back in when a subsequent request comes in from the same browser. To store objects outside the running application, Rails uses Ruby’s serialization mechanism, which converts objects to data that can be loaded back in. However, that loading works only if Ruby knows about the classes of the objects in the serialized file. When we store a cart in a session, we’re storing an object of classCart. But when it comes time to load that data back in, there’s no guarantee that Rails will have loaded up theCartmodel at that point (because Rails loads things only when it thinks it needs them). Using themodeldeclaration forces Rails to load the user model class early, so Ruby knows what to do with it when it loads the serialized session.
Hit Refresh in our browser, and (assuming we picked a product from the catalog) we’ll see it displayed.
1 Pragmatic Project Automation 29.95 29.95
Hit the Back button (we’ll work on navigation shortly), and add another.
1 Pragmatic Project Automation 29.95 29.95 1 Pragmatic Version Control 29.95 29.95
Looking good. Go back and add a second copy of the original product.
1 Pragmatic Project Automation 29.95 29.95 1 Pragmatic Version Control 29.95 29.95 1 Pragmatic Project Automation 29.95 29.95
That’s not so good. Although the cart is logically correct, it’s not what our users would expect. Instead, we should probably have merged both of those automation books into a single line item with a quantity of 2.
Fortunately, this is a fairly straightforward change to the add_product( )
method in theCartmodel. When adding a new product, we’ll look to see if
that product is already in the cart. If so, we’ll just increment its quantity rather than adding a new line item. Remember that the cart is not a database object—this is just straight Ruby code.
File 81 def add_product(product)
item = @items.find {|i| i.product_id == product.id}
if item
item.quantity += 1
Report erratum
ITERATIONC1: CREATING ACAR T 84 else item = LineItem.for_product(product) @items << item end @total_price += product.price end
The problem we now face is that we already have a session with duplicate products in the cart, and this session is associated with a cookie stored in
our browser. It won’t go away unless we delete that cookie.5 Fortunately,
there’s an alternative to punching buttons in a browser window when we want to try out our code. The Rails way is to write tests. But that’s such a
big topic, we’re putting it in its own chapter, Chapter12,Task T: Testing,
on page132.
Tidying Up the Cart
Before we declare this iteration complete and we show our customer our application so far, let’s tidy up the cart display page. Rather than simply dumping out the products, let’s add some formatting. At the same time,
we can add the much-neededContinue shopping link so we don’t have to
keep hitting the Back button. While we’re adding links, let’s anticipate a little and add links to empty the cart and to check out.
Our newdisplay_cart.rhtml file looks like this.
File 25 <div id="cartmenu">
<ul>
<li><%= link_to 'Continue shopping', :action => "index" %></li>
<li><%= link_to 'Empty cart', :action => "empty_cart" %></li>
<li><%= link_to 'Checkout', :action => "checkout" %></li>
</ul>
</div>
<table cellpadding="10" cellspacing="0">
<tr class="carttitle"> <td rowspan="2">Qty</td> <td rowspan="2">Description</td> <td colspan="2">Price</td> </tr> <tr class="carttitle"> <td>Each</td> <td>Total</td> </tr> <%
for item in @items product = item.product -%>
<tr>
<td><%= item.quantity %></td>
5Which you can do if you want. You can delete the cookie file (on a Unix box it will be in
a file whose name startsruby_sessin your/tmpdirectory. Alternatively, look in your browser for a cookie fromlocalhostcalled_session_id, and delete it.
Report erratum
ITERATIONC1: CREATING ACAR T 85
Figure 8.3: On Our Way to a Presentable Cart
<td><%= h(product.title) %></td>
<td align="right"><%= item.unit_price %></td>
<td align="right"><%= item.unit_price * item.quantity %></td>
</tr> <% end %>
<tr>
<td colspan="3" align="right"><strong>Total:</strong></td>
<td id="totalcell"><%= @cart.total_price %></td>
</tr>
</table>
After a bit of CSS magic, our cart looks like Figure8.3.
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 and security flaws. She noticed that the link to add an item to
our cart looks likestore/add_to_cart/nnn, wherennnis 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 Figure8.4, on the following page. So it looks as if our
next iteration will be spent making the application more resilient.
Report erratum
ITERATIONC2: HANDLINGERRORS 86
Figure 8.4: Our Application Spills Its Guts