This post is part of a series of reviews on the book Agile Web Development With Rails. Check out the Introduction post for a full table of contents along with some initial notes.

On the first task of the online store application building we learned to set up a quick way to manage products. But that was more to the owner-side of the application, so now let’s build something that will actually be visualized by clients.

First thing we’ll be doing is creating a catalog display: something pretty to show what we have to people. To do that we need a second controller, since now we will need something to deal with paying customers instead of administrators.

To the terminal we go:

ruby script generate controller store index

We just told the terminal to generate the controller file with a specific ‘index’ action (method) on it, which is the only one we will need for the moment. If you try to access that page (localhost:3000/store) it will just tell you where to go to fill it up with information, so let’s do that, but first we need to set up the controller so we have some information to work it. Open up the store controller file and code this in:

class StoreController < ApplicationController

  def index

    @products = Product.find_products_for_sale

  end

end

We’re using a Product class method we didn’t even define yet, so let’s define it on the product model file:

class Product < ActiveRecord::Base

 def self.find_products_for_sale
 find(:all, :order => "title" )
 end

 validates_presence_of :title, :description, :image_url
 validates_numericality_of :price
 validates_uniqueness_of :title

 validates_format_of :image_url,
 :with => %r{\.(gif|jpg|png)$}i,
 :message => 'must be a URL for GIF, JPG ' +
 'or PNG image.(gif|jpg|png)'

 validate :price_must_be_at_least_a_cent

 protected
 def price_must_be_at_least_a_cent
 errors.add(:price,'should be at least 0.01') if price.nil? || price<0.01
 end
end

We just defined a new class method (self before the function name), which in concept means it can be called anywhere just by Product.find_products_for_sale, it doesn’t need an instance to be called (much like static methods in Java), and it’s using a native Rails find method that’s returning an array back to the controller with all products ordered alphabetically (by title).

Now that we have the controlling method, its time to write the index view file:


<h1>Your Pragmatic Catalog</h1>
<% for product in @products -%>
<div>
<%= image_tag(product.image_url) %>
<h3><%=h product.title %></h3>
<%= product.description %>
<span><%= product.price %></span>
</div>
<% end %>

We have the page but it looks pretty lame, of course it does, we have no layout! Although that’s not hard to add, cause Rails makes it easy for us by having a folder named ‘layouts’ in the views folder, so all we need to do is create a new layout with the same name as the controller and we’re set. Just copy the products layout file, rename it to store.html.erb and edit it with this content:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot" , :media => "all" %>
</head>
<body id="store">
<div id="banner">
<%= image_tag("logo.png" ) %>
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns">
<div id="side">
<a href="http://www....">Home</a><br />
<a href="http://www..../faq">Questions</a><br />
<a href="http://www..../news">News</a><br />
<a href="http://www..../contact">Contact</a><br />
</div>
<div id="main">
<%= yield :layout %>
</div>
</div>

</body> </html>

Rails renders a page reading this file, then, when it gets to the yield command, it inserts all the data concerning the page’s specific view file. But having the page structure won’t make it pretty, and the depot.css file downloaded in Task A doesn’t have all the styling data needed for the rest of the application, so just replace it with this content (this is actually for the rest of the whole application):


/* Global styles */
#notice {
border: 2px solid red;
padding: 1em;
margin-bottom: 2em;
background-color: #f0f0f0 ;
font: bold smaller sans-serif;
}
/* Styles for products/index */
#product-list table {
border-collapse: collapse;
}
#product-list table tr td {
padding: 5px;
vertical-align: top;
}
#product-list .list-image {
width: 60px;
height: 70px;
}
#product-list .list-description {
width: 60%;
}
#product-list .list-description dl {
margin: 0;
}
#product-list .list-description dt {
color: #244 ;
font-weight: bold;
font-size: larger;
}
#product-list .list-description dd {
margin: 0;
}
#product-list .list-actions {
font-size: x-small;
text-align: right;
padding-left: 1em;
}
#product-list .list-line-even {
background: #e0f8f8 ;
}
#product-list .list-line-odd {
background: #f8b0f8 ;
}
/* Styles for main page */
#banner {
background: #9c9 ;
padding-top: 10px;
padding-bottom: 10px;
border-bottom: 2px solid;
font: small-caps 40px/40px "Times New Roman", serif;
color: #282 ;
text-align: center;
}
#banner img {
float: left;
}
#columns {
background: #141 ;
}
#main {
margin-left: 15em;
padding-top: 4ex;
padding-left: 2em;
background: white;
}
#side {
float: left;
padding-top: 1em;
padding-left: 1em;
padding-bottom: 1em;
width: 14em;
background: #141 ;
}
#side a {
color: #bfb ;
font-size: small;
}
h1 {
font: 150% sans-serif;
color: #226 ;
border-bottom: 3px dotted #77d ;
}
/* An entry in the store catalog */
#store .entry {
border-bottom: 1px dotted #77d ;
}
#store .title {
font-size: 120%;
font-family: sans-serif;
}
#store .entry img {
width: 75px;
float: left;
}
#store .entry h3 {
margin-bottom: 2px;
color: #227 ;
}
#store .entry p {
margin-top: 0px;
margin-bottom: 0.8em;
}
#store .entry .price-line {
}
#store .entry .add-to-cart {
position: relative;
}
#store .entry .price {
color: #44a;
font-weight: bold;
margin-right: 2em;
}
#store .entry form, #store .entry form div {
display: inline;
}
/* Styles for the cart in the main page and the sidebar */
.cart-title {
font: 120% bold;
}
.item-price, .total-line {
text-align: right;
}
.total-line .total-cell {
font-weight: bold;
border-top: 1px solid #595 ;
}
/* Styles for the cart in the sidebar */
#cart, #cart table {
font-size: smaller;
color: white;
}
#cart table {
border-top: 1px dotted #595 ;
border-bottom: 1px dotted #595 ;
margin-bottom: 10px;
}
/* Styles for order form */
.depot-form fieldset {
background: #efe;
}
.depot-form legend {
color: #dfd ;
background: #141 ;
font-family: sans-serif;
padding: 0.2em 1em;
}
.depot-form label {
width: 5em;
float: left;
text-align: right;
margin-right: 0.5em;
display: block;
}
.depot-form .submit {
margin-left: 5.5em;
}
/* The error box */
.fieldWithErrors {
padding: 2px;
background-color: red;
display: table;
}
#errorExplanation {
width: 400px;
border: 2px solid red;
padding: 7px;
padding-bottom: 12px;
margin-bottom: 20px;
background-color: #f0f0f0 ;
}
#errorExplanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px;
background-color: #c00 ;
color: #fff ;
}
#errorExplanation p {
color: #333 ;
margin-bottom: 0;
padding: 5px;
}

#errorExplanation ul li {
font-size: 12px;
list-style: square;
}

Now you can refresh the store index and enjoy a nice looking interface. But we still need 2 quick things, a price format and an ‘Add to Cart’ button (we will add functionality to it in the next task, here’s just about the looks). Open the store index view file and leave it like this:


<h1>Your Pragmatic Catalog</h1>
<% for product in @products -%>
<div>
<%= image_tag(product.image_url) %>
<h3><%=h product.title %></h3>
<%= product.description %>
<span><%= number_to_currency product.price %></span>
<%= button_to "Add to Cart" , :action => :add_to_cart, :id => product %>
</div>

<% end %>

Notice we used a number_to_currency helper to format the price (using helpers is the best way to do it, trust me) and another helper to create a button that links to a function called add_to_cart and passes the product ID as a paramater to that function.

……And we’re done! This task was quite simple because it was all about looks, the next one won’t be so simple :)