Task D: Cart Creation
9.3 Iteration D3: Adding a Button
Here we declare that a product has many line items, and define a hook method namedensure_not_referenced_by_any_line_item. A hook method is a method that Rails calls automatically at a given point in an object’s life. In this case, the method will be called before Rails attempts to destroy a row in the database.
If the hook method returns false, the row will not be destroyed.
We’ll have more to say about intermodel relationships starting on page299.
9.3 Iteration D3: Adding a Button
Now that that’s done, it is time to add an Add to Cart button for each product.
There is no need to create a new controller or even a new action. Taking a look at the actions provided by the scaffold generator, you find index,show,new, edit,create,updateanddestroy. The one that matches this operation iscreate. (newmay sound similar, but its use is to get a form that is used to solicit input for a subsequentcreateaction.)
Once this decision is made, the rest follows. What are we creating? Certainly not aCartor even aProduct. What we are creating is aLineItem. Looking at the comment associated with thecreatemethod inapp/controllers/line_items_controller.rb, you see that this choice also determines the URL to use (/line_items) and the HTTP method (POST).
This choice even suggests the proper UI control to use. When we added links before, we usedlink_to, but links default to using HTTP GET. We want to use POST, so we will add a button this time, this means that we will be using the button_tomethod.
We could connect the button to the line item by specifying the URL, but again we can let Rails take care of this for us by simply appending _path to the controller’s name. In this case, we will useline_items_path.
However, there’s a problem with this: how will theline_items_pathmethod know which product to add to our cart? We’ll need to pass it the id of the product corresponding to the button. That’s easy enough—all we need to do is add the :product_idoption to the line_items_path call. We can even pass in the product
ITERATIOND3: ADDING ABUTTON 133
instance itself—Rails knows to extract the id from the record in circumstances such as these.
In all, the one line that we need to add to ourindex.html.erblooks like this:
Download depot_f/app/views/store/index.html.erb
<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
<h1>Your Pragmatic Catalog</h1>
<% @products.each do |product| %>
<div class="entry">
<%= button_to 'Add to Cart', line_items_path(:product_id => product) %>
</div>
</div>
<% end %>
There’s one more formatting issue. button_to creates an HTML <form>, and that form contains an HTML<div>. Both of these are normally block elements, which will appear on the next line. We’d like to place them next to the price, so we need a little CSS magic to make them inline:
Download depot_f/public/stylesheets/depot.css
#store .entry form, #store .entry form div { display: inline;
}
Now our index page looks like Figure 9.1, on the following page. But before we push the button, we need to modify the create method in the line items controller to expect a product id as a form parameter. Here’s where we start to see how important theidfield is in our models. Rails identifies model objects (and the corresponding database rows) by their id fields. If we pass an id to create, we’re uniquely identifying the product to add.
Why the create method? The default HTTP method for a link is a get, and the default HTTP method for a button is a post, and rails uses these con-ventions to determine which method to call. See the comments inside the app/controller/line_items_controllerfile to see other conventions. We’ll be making extensive use of these conventions inside the Depot application.
Now let’s modify theLineItemsControllerto find the shopping cart for the current session (creating one if there isn’t one there already), add the selected product
Report erratum this copy is (B8.0 printing, September 9, 2010)
Prepared exclusively for Jared Rosoff
ITERATIOND3: ADDING ABUTTON 134
Figure 9.1: Now there’s an Add to Cart button.
to that cart, and display the cart contents. All we need to modify is a few lines of code in thecreatemethod inapp/controllers/line_items_controller.rb1:
Download depot_f/app/controllers/line_items_controller.rb
def create
@cart = current_cart
product = Product.find(params[:product_id])
@line_item = @cart.line_items.build(:product => product)
respond_to do |format|
if @line_item.save
format.html { redirect_to(@line_item.cart,
:notice => 'Line item was successfully created.' ) } format.xml { render :xml => @line_item,
:status => :created, :location => @line_item } else
format.html { render :action => "new" }
format.xml { render :xml => @line_item.errors, :status => :unprocessable_entity }
end end end
We use the current_cart method we implemented on page 129 to find (or cre-ate) a cart in the session. Next, we use theparamsobject to get the:product_id parameter from the request. Theparamsobject is important inside Rails
appli-1. Some lines have been wrapped to fit on the page
ITERATIOND3: ADDING ABUTTON 135
cations. It holds all of the parameters passed in a browser request. We store the result in a local variable as there is no need to make this available to the view.
We then pass that product we found in to@cart.line_items.build, which builds a new Line Item associated with both this @cart and the product specified. We save the result into a instance variable named@line_item.
The remainder of this method takes care of xml requests, which we will cover on page363, and handling errors which we will cover in more detail on page144.
But for now, we only want to modify one more thing: once the Line Item is cre-ated we want to redirect you to the cart instead of back to the line item itself.
Since the line item object knows how to find the cart object, all we need to do is add.cartto the method call.
As we changed the function of our controller, we know that we will need to update the corresponding functional test. We need to pass a product id on the call tocreate, and change what we expect for the target of the redirect. We do this by updatingtest/functional/line_items_controller_test.rb.
Download depot_g/test/functional/line_items_controller_test.rb
test "should create line_item" do assert_difference('LineItem.count' ) do
post :create, :product_id => products(:ruby).id end
assert_redirected_to cart_path(assigns(:line_item).cart) end
We rerun the functional tests.
depot> rake test:functionals
Confident that the code works as intended, we try the Add to Cart buttons in our browser. And here is what we see:
This is a bit underwhelming. While we have scaffolding for the Cart, when we created it we didn’t provide any attributes, so the view doesn’t have anything to show. For now, lets write a trivial template (we’ll tart it up in a minute):
Download depot_f/app/views/carts/show.html.erb
<h2>Your Pragmatic Cart</h2>
<ul>
<% for item in @cart.line_items %>
Report erratum this copy is (B8.0 printing, September 9, 2010)
Prepared exclusively for Jared Rosoff
ITERATIOND3: ADDING ABUTTON 136
<li><%= item.product.title %></li>
<% end %>
</ul>
So, with everything plumbed together, let’s hit Refresh in our browser, and see our simple view displayed:
Go back to http://localhost:3000/, the main catalog page, and add a different product to the cart. You’ll see the original two entries plus our new item in your cart. It looks like we’ve got sessions working. It’s time to show our customer, so we call her over and proudly display our handsome new cart. Somewhat to 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.
What We Just Did
It has been a busy, productive day so far. We’ve added a shopping cart to our store, and along the way we’ve dipped our toes into some neat Rails features:
• We created a Cart object in one request and were able to successfully locate the same Cart in subsequent requests using a session object,
• we added a private method in the base class for all of our controllers, making it accessible to all of our controllers,
• we created relationships between Carts and Line Items, relationships between Line Items and Products, and were able to navigate using these relationships, and
• we added a button which caused a product to be POSTed to a cart, caus-ing a new LineItem to be created.
ITERATIOND3: ADDING ABUTTON 137
Playtime
Here’s some stuff to try on your own:
• Change the application so that clicking a book’s image will also invoke thecreateaction. Hint: the first parameter tolink_tois placed in the gener-ated<a>tag, and the Rails helperimage_tagconstructs an HTML<img>
tag. Include a call to it as the first parameter to alink_tocall. Be sure to include:method => :postin yourhtml_optionson your call tolink_to.
• Add a new variable to the session to record how many times the user has accessed the store controller’s indexaction. The first time through, your count won’t be in the session. You can test for this with code like this:
if session[:counter].nil?
...
If the session variable isn’t there, you’ll need to initialize it. Then you’ll be able to increment it.
• Pass this counter to your template, and display it at the top of the catalog page. Hint: the pluralize helper (described on page 374) might be useful when forming the message you display.
• Reset the counter to zero whenever the user adds something to the cart.
• Change the template to display the counter only if it is greater than five.
(You’ll find hints athttp://pragprog.com/wikis/wiki/RailsPlayTime.)
Report erratum this copy is (B8.0 printing, September 9, 2010)
Prepared exclusively for Jared Rosoff
ITERATIOND3: ADDING ABUTTON 138
In this chapter, we’ll see
• modifying the schema and existing data,
• error diagnosis and handling,
• the flash, and
• logging.