Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

ITERATION A4: PRETTIER LISTINGS

85

validation will perform a simple check to ensure that no other row in the products table has the same title as the row we’re about to save.

validates_uniqueness_of :title

Lastly, we need to validate that the URL entered for the image is valid. We’ll do this using the validates_format_of method, which matches a field against a regular expression. For now we’ll just check that the URL ends with one of .gif,

.jpg, or .png.9

validates_format_of :image_url,

:with

=> %r{\.(gif|jpg|png)$}i,

:message => "must be a URL for a GIF, JPG, or PNG image"

So, in a couple of minutes we’ve added validations that check

The field’s title, description, and image URL are not empty.

The price is a valid number not less than $0.01.

The title is unique among all products.

The image URL looks reasonable.

This is the full listing of the updated Product model.

Download depot_b/app/models/product.rb

class Product < ActiveRecord::Base

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 a GIF, JPG, or PNG image"

regular expression

֒page 639

protected def validate

errors.add(:price, "should be at least 0.01") if price.nil? || price < 0.01 end

end

Nearing the end of this cycle, we ask our customer to play with the application, and she’s a lot happier. It took only a few minutes, but the simple act of adding validation has made the product maintenance pages seem a lot more solid.

6.4Iteration A4: Prettier Listings

Our customer has one last request (customers always seem to have one last request). The listing of all the products is ugly. Can we “pretty it up” a bit?

9. Later on, we’d probably want to change this form to let the user select from a list of available images, but we’d still want to keep the validation to prevent malicious folks from submitting bad data directly.

Report erratum

ITERATION A4: PRETTIER LISTINGS

86

And, while we’re in there, can we also display the product image along with the image URL?

We’re faced with a dilemma here. As developers, we’re trained to respond to these kinds of requests with a sharp intake of breath, a knowing shake of the head, and a murmured “you want what?” At the same time, we also like to show off a bit. In the end, the fact that it’s fun to make these kinds of changes using Rails wins out, and we fire up our trusty editor.

But then we’re faced with a second dilemma. So far, the only code we’ve written that has anything to do with displaying the product list is

scaffold :product

There’s not much scope for customizing the view there! We used a dynamic scaffold, which configures itself each time a request comes in. If we want to see the actual view code in the scaffold, we’ll need to get Rails to generate it explicitly, creating a static scaffold. The scaffold generator takes two parameters: the names of the model and the controller.

depot> ruby script/generate scaffold product admin

exists

app/controllers/

exists

app/helpers/

exists

app/views/admin

exists

test/functional/

dependency

model

exists

app/models/

exists

test/unit/

exists

test/fixtures/

skip

app/models/product.rb

identical

test/unit/product_test.rb

identical

test/fixtures/products.yml

create

app/views/admin/_form.rhtml

create

app/views/admin/list.rhtml

create

app/views/admin/show.rhtml

create

app/views/admin/new.rhtml

create

app/views/admin/edit.rhtml

overwrite app/controllers/admin_controller.rb? [Ynaqd] y

force

app/controllers/admin_controller.rb

overwrite test/functional/admin_controller_test.rb? [Ynaqd] y

force

test/functional/admin_controller_test.rb

identical

app/helpers/admin_helper.rb

create

app/views/layouts/admin.rhtml

create

public/stylesheets/scaffold.css

Wow! That’s a lot of action. Basically, though, it’s fairly simple. It checks to make sure we have a model file and then creates all the view files needed to display the maintenance screens. However, when it gets to the controller, it stops. It notices that we have edited the file admin_controller and asks for our permission before overwriting it with its new version. The only change we made to this file was adding the scaffold :product line, which we no longer need, so

Report erratum

ITERATION A4: PRETTIER LISTINGS

87

we say Y. It also asks permission before overwriting the controller’s functional test, and again we agree.

If you refresh your browser, you should see no difference in the page that’s displayed: the code added by the static scaffold is identical to that generated on the fly by the dynamic scaffold. However, as we now have code, we can edit it.

The Rails view in the file app/views/admin/list.rhtml produces the current list of products. The source code, which was produced by the scaffold generator, looks something like the following.

<h1>Listing products</h1>

<table>

<tr>

<% for column in Product.content_columns %> <th><%= column.human_name %></th>

<% end %>

</tr>

<% for product in @products %>

<tr>

<% for column in Product.content_columns %> <td><%=h product.send(column.name) %></td>

<% end %>

<td><%= link_to 'Show', :action => 'show', :id => product %></td> <td><%= link_to 'Edit', :action => 'edit', :id => product %></td> <td><%= link_to 'Destroy', { :action => 'destroy', :id => product },

:confirm => 'Are you sure?', :method => :post %></td>

</tr>

<% end %>

</table>

<%= link_to 'Previous page',

{:page => @product_pages.current.previous } if @product_pages.current.previous %> <%= link_to 'Next page',

{:page => @product_pages.current.next } if @product_pages.current.next %>

<br />

<%= link_to 'New product', :action => 'new' %>

The view uses ERb to iterate over the columns in the Product model. It creates a table row for each product in the @products array. (This array is set up by the list action method in the controller.) The row contains an entry for each column in the result set.

The dynamic nature of this code is neat, because it means that the display will automatically update to accommodate new columns. However, it also makes the display somewhat generic. So, let’s take this code and modify it to produce nicer-looking output.

ERb

֒page 50

Report erratum

ITERATION A4: PRETTIER LISTINGS

88

Before we get too far, though, it would be nice if we had a consistent set of test data to work with. We could use our scaffold-generated interface and type data in from the browser. However, if we did this, future developers working on our codebase would have to do the same. And, if we were working as part of a team on this project, each member of the team would have to enter their own data. It would be nice if we could load the data into our table in a more controlled way. It turns out that we can. Migrations to the rescue!

Let’s create a data-only migration. The up method adds three rows containing typical data to our products table. The down method empties the table out. The migration is created just like any other.

depot> ruby script/generate migration add_test_data exists db/migrate

create db/migrate/003_add_test_data.rb

We then add the code to populate the products table. This uses the create method of the Product model. The following is an extract from that file. (Rather than type the migration in by hand, you might want to copy the file from the sample code available online.10 Copy it to the db/migrate directory in your application. While you’re there, copy the images11 and the file depot.css12 into corresponding places (public/images and public/stylesheets in your application).

Download depot_c/db/migrate/003_add_test_data.rb

class AddTestData < ActiveRecord::Migration def self.up

Product.create(:title => 'Pragmatic Version Control', :description =>

%{<p>

This book is a recipe-based approach to using Subversion that will get you up and running quickly--and correctly. All projects need version control: it's a foundational piece of any project's infrastructure. Yet half of all project teams in the U.S. don't use any version control at all. Many others don't use it well, and end up experiencing time-consuming problems.

</p>},

:image_url => '/images/svn.jpg' , :price => 28.50)

# . . .

end

def self.down Product.delete_all

end end

10. http://media.pragprog.com/titles/rails2/code/depot_c/db/migrate/003_add_test_data.rb

11.http://media.pragprog.com/titles/rails2/code/depot_c/public/images

12.http://media.pragprog.com/titles/rails2/code/depot_c/public/stylesheets/depot.css

Report erratum

ITERATION A4: PRETTIER LISTINGS

89

(Note that this code uses %{...}. This is an alternative syntax for double-quoted string literals, convenient for use with long strings.)

Running the migration will populate your products table with test data.

depot> rake db:migrate

Now let’s get the product listing tidied up. There are two pieces to this. Eventually we’ll be writing some HTML that uses CSS to style the presentation. But for this to work, we’ll need to tell the browser to fetch the stylesheet.

We need somewhere to put our CSS style definitions. All scaffold-generated applications use the stylesheet scaffold.css in the directory public/stylesheets. Rather than alter this file, we created a new application stylesheet, depot.css, and put it in the same directory. A full listing of this stylesheet starts on page 678.

Finally, we need to link these stylesheets into our HTML page. If you look at the .rhtml files we’ve created so far, you won’t find any reference to stylesheets. You won’t even find the HTML <head> section where such references would normally live. Instead, Rails keeps a separate file that is used to create a standard page environment for all admin pages. This file, called admin.rhtml, is a Rails layout and lives in the layouts directory.

Download depot_b/app/views/layouts/admin.rhtml

<html>

<head>

<title>Admin: <%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold' %>

</head>

<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield :layout %>

</body>

</html>

The fourth line loads the stylesheet. It uses stylesheet_link_tag to create an HTML <link> tag, which loads the standard scaffold stylesheet. We’ll simply add our depot.css file here (dropping the .css extension). Don’t worry about the rest of the file: we’ll look at that later.

<%= stylesheet_link_tag 'scaffold', 'depot' %>

While we’re in there, we’ll add a <!DOCTYPE... directive to the top of the file. Without this line, Internet Explorer operates in quirks mode, which is incompatible with web standards. The top of our layout now looks like this.

Report erratum

ITERATION A4: PRETTIER LISTINGS

90

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >

<html>

<head>

<title>Admin: <%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold', 'depot' %>

</head>

Now that we have the stylesheet all in place, we’ll use a simple table-based template, editing the file list.rhtml in app/views/admin, replacing the dynamic column display.

Download depot_c/app/views/admin/list.rhtml

<div id="product-list" >

<h1>Product Listing</h1>

<table cellpadding="5" cellspacing="0"> <% for product in @products %>

<tr valign="top" class="<%= cycle('list-line-odd', 'list-line-even') %>">

<td>

<img class="list-image" src="<%= product.image_url %>"/>

</td>

<td width="60%">

<span class="list-title"><%= h(product.title) %></span><br /> <%= h(truncate(product.description, 80)) %>

</td>

<td class="list-actions">

<%= link_to 'Show', :action => 'show', :id => product %><br/> <%= link_to 'Edit', :action => 'edit', :id => product %><br/>

<%= link_to 'Destroy', { :action => 'destroy', :id => product }, :confirm => "Are you sure?",

:method => :post %>

</td>

</tr>

<% end %>

</table>

</div>

<%= if @product_pages.current.previous

link_to("Previous page", { :page => @product_pages.current.previous }) end

%>

<%= if @product_pages.current.next

link_to("Next page", { :page => @product_pages.current.next }) end

%>

<br />

<%= link_to 'New product', :action => 'new' %>

Report erratum

ITERATION A4: PRETTIER LISTINGS

91

What’s with :method => :post?

You may have noticed that the scaffold-generated “Destroy” link includes the parameter :method => :post. This parameter was added to Rails 1.2, and it gives us a glimpse into the future of Rails.

Browsers use HTTP to talk with servers. HTTP defines a set of verbs that browsers can employ and defines when each can be used. A regular hyperlink, for example, uses an HTTP GET request. A GET request is defined by HTTP to be used to retrieve data: it isn’t supposed to have any side effects. The technical term is idempotent —you should be able to issue the same GET request many times and get the same result each time.

But if we use a GET request as a link to a Rails action that deletes a product, it’s no longer idempotent: it’ll work the first time but fail on subsequent clicks. So, the Rails team changed the scaffold code generator to force the link to issue an HTTP POST. These POST requests are permitted to have side effects and so are more suitable for deleting resources.

Over time, expect to see Rails become more and more strict about the correct use of HTTP.

Even this simple template uses a number of built-in Rails features.

The rows in the listing have alternating background colors. This is done by setting the CSS class of each row to either list-line-even or list-line-odd. The Rails helper method called cycle does this, automatically toggling between the two style names on successive lines.

The h method is used to escape the HTML in the product title and description. That’s why you can see the markup in the descriptions: it’s being escaped and displayed, rather than being interpreted.

We also used the truncate helper to display just the first 80 characters of the description.

Look at the link_to ’Destroy’ line. See how it has the parameter :confirm => "Are you sure?". If you click this link, Rails arranges for your browser to pop up a dialog box asking for confirmation before following the link and deleting the product. (Also, see the sidebar on this page for some scoop on this action.)

So, we’ve loaded some test data into the database, we rewrote the list.rhtml file that displays the listing of products, we created a depot.css stylesheet, and we linked that stylesheet into our page by editing the layout admin.rhtml. Bring

Report erratum

ITERATION A4: PRETTIER LISTINGS

92

up a browser, point to localhost:3000/admin/list, and the resulting product listing might look something like the following.

A static Rails scaffold provides real source code, files that we can modify and immediately see results. The combination of dynamic and static scaffolds gives us the flexibility we need to develop in an agile way. We can customize a particular source file and leave the rest alone—changes are both possible and localized.

So, we proudly show our customer her new product listing, and she’s pleased. End of task. Time for lunch.

What We Just Did

In this chapter we laid the groundwork for our store application.

We created a development database and configured our Rails application to access it.

We used migrations to create and modify the schema in our development database and to load test data.

We created the products table and used the scaffold generator to write an application to maintain it.

We augmented that generated code with validation.

We rewrote the generic view code with something prettier.

One topic we didn’t cover was the pagination of the product listing. The scaffold generator automatically used Rails’ built-in pagination helper. This breaks the lists of products into pages of 10 entries each and automatically handles navigation between pages. We discuss this in more depth starting on page 478.

Report erratum

ITERATION A4: PRETTIER LISTINGS

93

Playtime

Here’s some stuff to try on your own.

The method validates_length_of (described on page 369) checks the length of a model attribute. Add validation to the product model to check that the title is at least 10 characters long.

Change the error message associated with one of your validations.

Add the product price to the output of the list action.

(You’ll find hints at http://wiki.pragprog.com/cgi-bin/wiki.cgi/RailsPlayTime)

Report erratum