Just finished chapter 10 of Agile Web Development with Rails. I’m still missing something very fundamental about Rails development. I can follow the book, but whenever I try to go off on a tangent, I get lost beyond repair.
I don’t really like the idea of this shopping cart object with no database backend. I’m certainly not averse to objects without a table behind them, but this doesn’t seem like a good candidate for that. While it may be suitable for the scope of the book, it seems that whatever I’d like to do next with a shopping cart would be more easily done if it was stored. On the other hand, I’m a noob at Rails and I’m pretty much talking out of my ass.
I decided to put the cart in a table just to see if I could do it – and maybe learn something too. I wanted to document it somewhere, and this is where it starts. I’m going to use the Orders table as my cart and the line_items table as my cart items. When, and if, my cart turns into an order, I’ll just change a boolean flag. First, I need to change a couple of things in the database, so I run this migration:
007_add_cart_flag_to_orders.rb
class AddCartFlagToOrders < ActiveRecord::Migration
def self.up
add_column
rders, :iscart, :boolean
rename_column :line_items, :total_price, :item_price
end
def self.down
remove_column
rders, :iscart
rename_column :line_items, :item_price, :total_price
end
end
In orders, I add an IsCart boolean field. This will be set to true when the user creates a cart and set to false at checkout. Also, I can’t think of one good reason to store total_price in a table. I don’t like storing calculated fields in table, so I’m changing it to store item_price and I’ll calculate total price when I need it.
MVC, MVC, MVC. I’ve got to keep saying that to myself.
Okay, so the user clicks on an Add to Cart button and the controller handles all requests. So I think I’ll start with the add_to_cart method in the store_controller.
def add_to_cart
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index("Invalid product")
else
@cart = Order.find(find_cart)
puts "orderid", @cart.id
@current_line = @cart.add_product(product)
# redirect_to_index unless request.xhr?
end
end
I started, obviously, with the code I had at the end of chapter 10. Only the “else” part has changed. First, I need to find an existing cart. If I can’t, then I need to create a new one. Before, I stored a Cart object in the session (I think), but now that I have a table, that doesn’t seem right. So I’m going to store the order_id in the session. I change find_cart thusly:
def find_cart
unless session[:order]
@order = Order.new
@order.save
session[:order] = @order.id
end
session[:order]
end
If
rder in the session already has a value, return it. Otherwise create a new Order, save it so it generates and order.id, and store that order.id in the session. Back in add_to_car, I make a @Cart instance variable to hold the Order object. I also output some stuff to the console because nothing worked right, but you can ignore the “puts” line. Then I create a @current_item instance variable to hold the line_item object that will get created via add_product.
Over in order.rb, I make an add_product method that will return a line item.
def add_product(product)
li = self.line_items.find_by_product_id(product)
puts li.nil?
if li
li.quantity += 1
li.save
else
li = self.line_items.new
li.order_id = self.id
li.product = product
li.quantity = 1
li.item_price = product.price
li.save
end
li
end
I use the find_by_product_id dynamic find to set the li variable. “If li” will return true if it exists and I’ll up the quantity by one and save it. If it doesn’t exist, I create a new line, add all the salient data and save it. Finally I return li back to the store controller.
MVC, MVC, MVC. The user made a request handled by the controller (store_controller.rb). The controller got the necessary info from the model (order.rb). Now it should pass that info to the view. At the end of the chapter, everything was all AJAXy, which I like. But a couple of times the book said to do it non-AJAX first, then AJAXify it later. I renamed add_to_cart.rjs so it wouldn’t get called. When the add_to_cart method in store_controller.rb finishes, it will try to show a view called add_to_cart.rhtml. So I made that file.
<h1>Your Cart</h1>
<table>
<%= render(:partial => "cart_item", :collection => @cart.line_items) %>
<tr class="total-line">
<td colspan="2">Total</td>
<td class="total-cell"><%= number_to_currency(@cart.total_price) %></td>
</tr>
</table>
Not a lot of changes here. I’ll start from the bottom. I needed a total price at the bottom of my cart, so implemented a method in order.rb
def total_price
self.line_items.inject(0) {|a, li| a += (li.quantity * li.item_price)}
#@all_items = self.line_items.find :all
#@all_items.inject(0) { |sum, li| sum + (li.quantity * li.item_price)}
end<
I couldn’t get this working for the life of me. The final product above is via some helpful folks at ruby-forum.com. I left an alternative method commented out so I can remember. Reading up on inject is on my todo list.
Above that, I call the cart_item partial. But I have to pass it @cart.line_items.
<% if cart_item == @current_line %>
<tr id="current_item">
<% else %>
<tr>
<% end %>
<td><%= cart_item.quantity %>×</td>
<td><%= h(cart_item.product.title) %></td>
<td class="item-price" ><%= number_to_currency(cart_item.total_price) %></td>
</tr>
Since I created @current_item in store_controller.rb, it’s available to any view or partial that’s called. The total_price method in line_item.rb is
def total_price
self.quantity * self.item_price
end
And the result.
J