Iteration E3: Finishing the Cart

We know by now that to implement the empty-cart function, we have to add a link to the cart and modify the destroy method in the carts controller to clean up the session.

David says:
David says:
Battle of the Routes: product_path vs. product_url

It can seem hard in the beginning to know when to use product_path and when to use product_url when you want to link or redirect to a given route. In reality, it’s simple.

When you use product_url, you’ll get the full enchilada with protocol and domain name, like http://example.com/products/1. That’s the thing to use when you’re doing redirect_to, because the HTTP spec requires a fully qualified URL when doing 302 Redirect and friends. You also need the full URL if you’re redirecting from one domain to another, like product_url(domain: "example2.com", product: product).

The rest of the time, you can happily use product_path. This will generate only the /products/1 part, and that’s all you need when doing links or pointing forms, like link_to "My lovely product", product_path(product).

The confusing part is that oftentimes the two are interchangeable because of lenient browsers. You can do a redirect_to with a product_path and it’ll probably work, but it won’t be valid according to spec. And you can link_to a product_url, but then you’re littering up your HTML with needless characters, which is a bad idea too.

Start with the template and use the button_to method to add a button:

 <%​ ​if​ notice ​%>
  <aside id=​"notice"​>​<%=​ notice ​%>​</aside>
 <%​ ​end​ ​%>
 
 <h2>Your Pragmatic Cart</h2>
 <ul>
 <%​ @cart.​line_items​.​each​ ​do​ |item| ​%>
  <li>​<%=​ item.​quantity​ ​%>​ &times; ​<%=​ item.​product​.​title​ ​%>​</li>
 <%​ ​end​ ​%>
 </ul>
 
»<%=​ button_to ​'Empty cart'​, @cart, ​method: :delete​,
»data: ​{ ​confirm: ​​'Are you sure?'​ } ​%>

In the controller, let’s modify the destroy method to ensure that the user is deleting his or her own cart (think about it!) and to remove the cart from the session before redirecting to the index page with a notification message:

 def​ ​destroy
» @cart.​destroy​ ​if​ @cart.​id​ == session[​:cart_id​]
» session[​:cart_id​] = ​nil
  respond_to ​do​ |format|
» format.​html​ { redirect_to store_index_url,
»notice: ​​'Your cart is currently empty'​ }
  format.​json​ { head ​:no_content​ }
 end
 end

And we update the corresponding test in test/controllers/carts_controller_test.rb:

  test ​"should destroy cart"​ ​do
» post line_items_url, ​params: ​{ ​product_id: ​products(​:ruby​).​id​ }
» @cart = Cart.​find​(session[​:cart_id​])
»
  assert_difference(​'Cart.count'​, -1) ​do
  delete cart_url(@cart)
 end
 
» assert_redirected_to store_index_url
 end

Now when we view our cart and click the “Empty cart” button, we are taken back to the catalog page and see the message shown in the screenshot.

images/g_5_empty_cart.png

 

We can remove the flash message that’s autogenerated when a line item is added:

 def​ ​create
  product = Product.​find​(params[​:product_id​])
  @line_item = @cart.​add_product​(product)
 
  respond_to ​do​ |format|
 if​ @line_item.​save
» format.​html​ { redirect_to @line_item.​cart​ }
  format.​json​ { render ​:show​,
 status: :created​, ​location: ​@line_item }
 else
  format.​html​ { render ​:new​ }
  format.​json​ { render ​json: ​@line_item.​errors​,
 status: :unprocessable_entity​ }
 end
 end
 end

Finally, we get around to tidying up the cart display. The <li>-based approach makes it hard to style. A table-based layout would be easier. Replace app/views/carts/show.html.erb with the following:

 <article>
 <%​ ​if​ notice ​%>
  <aside id=​"notice"​>​<%=​ notice ​%>​</aside>
 <%​ ​end​ ​%>
 
  <h2>Your Cart</h2>
  <table>
 <%​ @cart.​line_items​.​each​ ​do​ |line_item| ​%>
  <tr>
  <td class=​"quantity"​>​<%=​ line_item.​quantity​ ​%>​</td>
  <td>​<%=​ line_item.​product​.​title​ ​%>​</td>
  <td class=​"price"​>​<%=​ number_to_currency(line_item.​total_price​) ​%>​</td>
  </tr>
 <%​ ​end​ ​%>
  <tfoot>
  <tr>
  <th colspan=​"2"​>Total:</th>
  <td class=​"price"​>​<%=​ number_to_currency(@cart.​total_price​) ​%>​</td>
  </tr>
  </tfoot>
  </table>
 <%=​ button_to ​'Empty cart'​, @cart,
 method: :delete​,
 data: ​{ ​confirm: ​​'Are you sure?'​ } ​%>
 
 </article>

To make this work, we need to add a method to both the LineItem and Cart models that returns the total price for the individual line item and entire cart, respectively. Here is the line item, which involves only simple multiplication:

 def​ ​total_price
  product.​price​ * quantity
 end

We implement the Cart method using the nifty Array::sum method to sum the prices of each item in the collection:

 def​ ​total_price
  line_items.​to_a​.​sum​ { |item| item.​total_price​ }
 end

With this in place, we’ll style the cart to look a bit nicer. This all gets inserted into app/assets/stylesheet/carts.css.

 // Place all the styles related to the Carts controller here.
 // They will automatically be included in application.css.
 // You can use Sass (SCSS) here: https://sass-lang.com/
 
 .carts {
  table {
  border-collapse: collapse;
  }
  td {
  padding: 0.5em;
  }
  td.quantity {
  white-space: nowrap;
  }
  td​.​quantity::after {
  content: ​" ×"​;
  }
  td.price {
  font-weight: bold;
  text-align: right;
  }
  tfoot {
  th, th.price {
  font-weight: bold;
  padding-top: 1em;
  }
  th {
  text-align: right;
  }
  th.price {
  border-top: solid thin;
  }
  }
  input[type=​"submit"​] {
  background-color: #881;
  border-radius: 0.354em;
  border: solid thin #441;
  color: white;
  font-size: 1em;
  padding: 0.354em 1em;
  }
  input[type=​"submit"​]:hover {
  background-color: #992;
  }
 }

The following screenshot shows a nicer-looking cart.

images/h_1_cart_2_styled.png

Finally, we update our test cases to match the current output:

  test ​"should create line_item"​ ​do
  assert_difference(​'LineItem.count'​) ​do
  post line_items_url, ​params: ​{ ​product_id: ​products(​:ruby​).​id​ }
 end
 
  follow_redirect!
 
» assert_select ​'h2'​, ​'Your Cart'
» assert_select ​'td'​, ​"Programming Ruby 1.9"
 end

What We Just Did

Our shopping cart is now something the client is happy with. Along the way, we covered the following:

  • Adding a column to an existing table, with a default value
  • Migrating existing data into the new table format
  • Providing a flash notice of an error that was detected
  • Using the logger to log events
  • Removing a parameter from the permitted list
  • Deleting a record
  • Adjusting the way a table is rendered, using CSS

But, just as we think we’ve wrapped up this functionality, our customer wanders over with a copy of Information Technology and Golf Weekly. Apparently, it has an article about the Ajax style of browser interface, where stuff gets updated on the fly. Hmmm…let’s look at that tomorrow.

Playtime

Here’s some stuff to try on your own:

  • Create a migration that copies the product price into the line item, and change the add_product method in the Cart model to capture the price whenever a new line item is created.

  • Write unit tests that add both unique products and duplicate products to a cart. Assert how many products should be in the cart in each instance. Note that you’ll need to modify the fixture to refer to products and carts by name—for example, product: ruby.

  • Check products and line items for other places where a user-friendly error message would be in order.

  • Add the ability to delete individual line items from the cart. This will require buttons on each line, and such buttons will need to be linked to the destroy action in the LineItemsController.

  • We prevented accessing other users’ carts in the LineItemsController, but you can still see other carts by navigating directly to a URL like http://localhost/carts/3. See if you can prevent accessing any cart other than than one currently stored in the session.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.94.152