A Ruby on Rails Journey

June 4, 2008

Create Player Model

Filed under: TenthHole — Tags: , , , , — Dick @ 3:10 am

I have a Tenthhole project and an empty MySQL database. Now let’s start putting stuff in it. First, generate a model for players

This creates some stuff, like an empty model named player.rb and, most notably, a database migration

Here’s the migration.

Then, I make the magic happen

This update MySQL, which now looks like this

Next post I’ll set up the scaffolding.

Make TenthHole Database

Filed under: TenthHole — Tags: , , , , — Dick @ 2:46 am

I’ve created a new rails project and now I need a mysql database. I open up phpMyAdmin (right click on the Instant Rails icon, Configure, Database) and create a new database named Tenthhole_development. I know what to name it because I look in the config/database.yml file.

Here’s what it looks like in phpMyAdmin

After I hit create, I have a database with no tables in it

Next post, I’ll start creating models.

Create Tenthhole Project

Filed under: TenthHole — Tags: , , , , — Dick @ 2:30 am

I’m using RadRails and Aptana Studio Community Edition (free). I’m also using Instant Rails – yeah I’m a Windows guy. I have the 2.0 Instant Rails on one computer and whatever the previous version was on another computer. I think the one on this computer is pre-2.0, but i don’t know how to tell. I tried to follow a tutorial here that uses Rails 2.0 and I ran into some problems. It could be that I just don’t know what I’m doing, but I think it’s because I’m using pre-2.0.

I’m going to create a rails app for a golf league. The league consists of ten two-man teams playing nine holes once per week. There are additional players that can sub for any team. There are two complicated calculations that I have to worry about. The first is handicap. The second is score.

Handicap is calculated as the average of the best x scores of the last y scores, where x and y are

1 of 2

2 of 3

3 of 4

3 of 5

If the player only as one score, 80% of that differential (from par) is the handicap.

Scoring goes like this: The lower handicap player (LH) from team A plays the lower handicap player from team B. There are 21 total points available per week. The LH paring plays 9 hole match play for 1 point per hole (9 points). The higher handicap (HH) paring plays 9 hole match play for 1 point per hole (9 points). Each pairing also plays adjusted stroke play (points 19 and 20). Then the two teams play adjusted stroke play on their combined score for the final point.

Here’s my preliminary try at models:

Teams
-name

Players
-name
-email

Courses
-name
-hole1par
-hole1handicap
-hole2par
-hole2handicap

-hole9handicap

Matches
-Date
-Course
-Team
-Player
-Hole
-Score

In Aptana, File > New

February 26, 2008

Datestamp the Cart

Filed under: Agile Web Development with Rails — Dick @ 1:36 am

Since the cart is just an order with a flag set and it lives in the database, I’ll need some way to clear out old orders. Visitors will create carts and abandon them and they’d live there forever if I don’t do some maintenance. I don’t know how or when I’ll do that maintenance, but I know I’ll need a date in the cart to determine how old it is. I start by adding a field to the table. In a new migration called 008_add_last_access_date.rb:

class AddLastAccessDate < ActiveRecord::Migration
  def self.up
    add_column :o rders, :last_access, :datetime
  end

  def self.down
    remove_column :o rders, :last_access
  end
end

Every time I change the cart, I need to update this field. In order.rb, I created a new private method

 def update_last_access
   self.last_access = DateTime.now
   self.save
 end

And I called that in both the add_product and remove_product methods. I made the method private, but I’m not sure I needed to.

February 25, 2008

Remove from Cart in AJAX

For removing individual items from the cart, I went with a form in lieu of a link. The few things I’ve read say don’t put destructive stuff behind links, so I don’t. I start by adding a remove button next to each line item in the cart. In the _cart.item.rhtml partial:

    <td class="item-price" ><%= number_to_currency(cart_item.total_price) %></td>
	<td>
		<% form_remote_tag :url => { :action => :remove_from_cart, :id => cart_item.product } do %>
			<%= submit_tag "remove" %>
		<% end %>
	</td>
  </tr>

The form_remote_tag makes it AJAXy. Next, I change the remove_from_cart method in store_controller.rb to only redirect if javascript is disabled.

  def remove_from_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_or_make_cart)
      @current_line = @cart.remove_product(product)
      redirect_to_index unless request.xhr?
    end
  end

I create remove_from_cart.rjs that the above method will call by default.

page.replace_html("cart", :partial => "cart", :o bject => @cart)

page[:cart].visual_effect :blind_up if @cart.line_items.empty?

page[:current_item].visual_effect :highlight,
                                  :startcolor => "#88ff88",
                                  :endcolor => "#114411"

If the last line item is removed, the cart is built-up and hidden. In the model, order.rb, I define a remove_product method

  def remove_product(product)
    li = self.line_items.find_by_product_id(product)
    if li
      if li.quantity < 2
        li.destroy
      else
        li.quantity -= 1
        li.save
      end
      li
    end
  end

Finally, in the _cart.rhtml partial I surround the cart_items table in a div tag with an id of cart-item-list. This way I can style my remove buttons differently than other buttons. Here’s what I added to depot.css

#cart-item-list input {
 background: 		#9c9;
 color: 			#141;
 border:			0;
 padding:			0px 0px 0px 5px;
 font-size: smaller;
}

I can’t figure out how to make it look like a link, but this is good enough for me.

cart5.gif

The Empty Cart button works as it did in the book with only a couple of mods. I had to change find_cart to find_or_make_cart because I changed the name. Then I have to put back the “unless request.xhr?” line.

February 24, 2008

Add to Cart in AJAX

Now that I have all the shopping cart stuff working, it’s time to stick it back in the sidebar. I’m not creating a new cart for every visitor. A cart only gets created when the user adds something. This creates some differences in how the book handles certain things to how I have to handle them. The book creates an @cart variable in the index method, so it’s available all the time. Since I won’t be creating a cart, I had to change the index method in store_controller.rb

  def index

    @products = Product.find_products_for_sale

    @cart = get_existing_cart

  end

I created a get_existing_cart method that will return an order object if it exists, and nil if it doesn’t. I also renamed find_cart to find_or_make_cart so I wouldn’t forget just what that method did. In the private section, I have these two methods:

  def get_existing_cart

    if session[:order]

      Order.find(session[:order])

    end

  end  def find_or_make_cart

    unless session[:order]

      @order = Order.new

      @order.iscart = true

      @order.save

      session[:order] = @order.id

    end

    session[:order]

  end

The only other change in store_controller.rb is to add_to_cart. I need to put back the request.xhr? line from the book and also change the find_cart method call.

  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_or_make_cart)

      @current_line = @cart.add_product(product)

      redirect_to_index unless request.xhr?

    end

  end

That’s the controller bit. There’s a lot of work to do in the view bits, so I’ll tackle that next. First, I put cart div back in the sidebar in store.rhtml.

<div id=”side”>
<div id=”cart”>
<%= render(:partial => “cart”, :o bject => @cart) unless @cart.nil? %>
</div>

I no longer need the hidden div helper. If there is not cart (unless @cart.nil?), there will be no div and therefore nothing to hide. I delete add_to_cart.rhtml because I will using add_to_cart.rjs to update the sidebar. There are no changes to the cart or cart_item partials, so let’s get straight to the javascript. It’s straight out of the book except for one small change.

page.select("div#notice").each { |div| div.hide }

page.replace_html("cart", :partial => "cart", :o bject => @cart)

page[:cart].visual_effect :blind_down if @cart.total_quantity == 1

page[:current_item].visual_effect :highlight,
                                  :startcolor => "#88ff88",
                                  :endcolor => "#114411"

I check the total_quantity method to see if I should blind_down the cart. I’ll show that new method in the model portion. The last bit of view stuff is in index.rhtml. I have to change the add to cart button to make a js call.

		<% form_remote_tag :url => { :action => :add_to_cart, :id => product } do %>
			<%= submit_tag "Add to Cart" %>
		<% end %>

The last piece is the model. In order.rb I defined a total_quantity method.

  def total_quantity
    self.line_items.sum(:quantity)
  end

cart4.gif

That’s it. Store.rhtml shows the cart div if a cart exists. Index.rhtml has an ajax button that calls the add_to_cart method in store_controller.rb. That method sets a couple of variables and, by default, calls add_to_cart.rjs which renders the cart partial in the sidebar. The rjs file also makes a call to order.rb to check the quantity.

Next I’ll make the remove button ajax, fix up the empty cart button, and move on to the checkout process. I also need a way to blow away old carts, so I’ll work on that too.

February 23, 2008

Removing Items from Cart

Here’s how I implemented removing single items from the cart. First, I added a remove button to add_to_cart.rhtml.

<% 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>
	<td><%= button_to "Remove", :action => :remove_from_cart, :id => cart_item.product %></td>
  </tr>

Removefromcart

Next, I updated the remove_from_cart method in store_controller.rb

  def remove_from_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)
      @current_line = @cart.remove_product(product)
      redirect_to_index
    end
  end

It looks almost like the add_to_cart method, but with a few slight differences. I’m heard that I’m supposed to keep my controllers thin and my models fat. To me that means all my controllers do is call methods in my modesl. So the remove_product method in my order.rb model does all of the work. Removing something from the cart doesn’t really lend itself to a view, so I included the redirect. I don’t get any visual feedback that it worked, but I know I’m moving this to an AJAX something-or-other next, so as long as it works, I don’t care.

Finally, the remove_product method in order.rb

  def remove_product(product)
    li = self.line_items.find_by_product_id(product)
    if li
      if li.quantity < 2
        li.destroy
      else
        li.quantity -= 1
        li.save
      end
    end
  end

I use find_by_product, a so-called dynamic find, to find the line item with that product. I test to see if the line item was found. Logically, a user shouldn’t be able to delete a line item that doesn’t exist, but since I don’t quite grok the whole program-by-url thing, I’m just going to assume that a user could try to do it and crash the code. To prevent it, I’m taking precautions that seem superfluous.

Speaking of precautions, I check for a quantity less than two. I could, of course, check for a quantity equal to one, but I just know that quantity will end up negative somehow and it will be never-ending. To be on the safe side, I’ll stick with the less than two test. If there’s only 1 left, destroy the line item completely. Otherwise, decrement the quantity field and save. Oh yeah, don’t forget to save. I seem to like to forget that step, then I scratch my head wondering why my code doesn’t do what I want.

I about ready to put this baby back in the sidebar with some AJAX goodness.

February 21, 2008

Emptying the Cart

First, I need to fix up the find_cart method. I didn’t set the iscart field to true. I plan on using the Orders table as a cart and also to hold the orders.

  def find_cart
    unless session[:order]
      @order = Order.new
      @order.iscart = true
      @order.save
      session[:order] = @order.id
    end
    session[:order]
  end

So I added a line. Next I want to empty the cart. First, I add a button to add_to_cart.rhtml.

<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>

<%= button_to "Empty Cart", :action => :empty_cart %>

In store_controller.rb, I define empty_cart

  def empty_cart
    @cart = Order.find(find_cart)
    @cart.empty_cart
    @cart.destroy
    session[:order] = nil
    redirect_to_index("Your cart has been emptied") #unless request.xhr?
  end

This finds a cart, calls the empty_cart method is order.rb, destroys itself, clears the session information, and shows the index and a flash notice. More on the actual flow of objects later. In order.rb, I have an empty_cart method

  def empty_cart
    self.line_items.each { |li| li.destroy }
  end

Empty Cart Flash

Pretty simple here. Loop through all the items and destroy them. Am I using self correctly here? I assume because I don’t use self in the definition of the method, that this method only applies to the current instance. And that if I use self in an instance method, then the ’self’ reference will also only apply to the current instance. I’m not 100% sure about that, though.

I delete all the line items, then delete the cart, then clear the session. Once I’ve one all that, I show the main page, or index. The index calls find_cart, which creates a new cart. So here’s a typical flow. Visitor comes to website (cart created). Visitor adds a book (line item created). Visitor empties cart (cart deleted, new cart created). That doesn’t seem very efficient. I’m not sure why I want to create a cart right away. I should just create it if anything is added.

Old index

  def index
    @products = Product.find_products_for_sale
    @cart = Order.find(find_cart)
  end

New index

  def index
    @products = Product.find_products_for_sale
  end

Yep, that seems to work just fine. I still need to remove individual items from the cart, create a checkout procedure to turn it into an order, and the apply some AJAX. A lot of work left.

February 20, 2008

A Shopping Cart Table

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 :o rders, :iscart, :boolean
     rename_column :line_items, :total_price, :item_price
   end
  def self.down
     remove_column :o 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 :o 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.

Cart first iterationJ

Blog at WordPress.com.