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

ITERATION A2: ADD A MISSING COLUMN

57

David Says. . .

Won’t We End Up Replacing All the Scaffolds?

Most of the time, yes. Scaffolding is not intended to be the shake ’n’ bake of application development. It’s there as support while you build out the application. As you’re designing how the list of products should work, you rely on the scaffold-generated create, update, and delete actions. Then you replace the generated creation functionality while relying on the remaining actions. And so on and so forth.

Sometimes scaffolding will be enough, though. If you’re merely interested in getting a quick interface to a model online as part of a backend interface, you may not care that the looks are bare. But this is the exception. Don’t expect scaffolding to replace the need for you as a programmer just yet (or ever).

means that we can modify the code produced in the scaffold. The scaffold is the starting point of an application, not a finished application in its own right. And we’re about to make use of that fact as we move on to the next iteration in our project.

6.2 Iteration A2: Add a Missing Column

So, we show our scaffold-based code to our customer, explaining that it’s still pretty rough-and-ready. She’s delighted to see something working so quickly. Once she plays with it for a while, she notices that something was missed in our initial discussions. Looking at the product information displayed in a browser window, it becomes apparent that we need to add an availability date column—the product will be offered to customers only once that date has passed.

This means we’ll need to add a column to the database table, and we’ll need to make sure that the various maintenance pages are updated to add support for this new column.

Some developers (and DBAs) would add the column by firing up a utility program and issuing the equivalent of the command

alter table products

add column date_available datetime;

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION A2: ADD A MISSING COLUMN

58

Instead, I tend to maintain the flat file containing the DDL I originally used to create the schema. That way I have a version-controlled history of the schema and a single file containing all the commands needed to re-create it. So let’s alter the file db/create.sql, adding the date_available column.

File 64

drop table if exists products;

 

 

create table products (

 

 

 

id

int

not

null auto_increment,

 

title

varchar(100)

not

null,

 

description

text

not

null,

 

image_url

varchar(200)

not

null,

 

price

decimal(10,2) not

null,

 

date_available

datetime

not

null,

primary key (id) );

When I first created this file, I added a drop table command at the top of it. This now allows us to create a new (empty) schema instance with the commands

depot> mysql depot_development <db/create.sql

Obviously, this approach only works if there isn’t important data already in the database table (as dropping the table wipes out the data it contains). That’s fine during development, but in production we’d need to step more carefully. Once an application is in production, I tend to produce versioncontrolled migration scripts to upgrade my database schemas.

Even in development, this can be a pain, as we’d need to reload our test data. I normally dump out the database contents (using mysqldump) when I have a set of data I can use for development, then reload this database each time I blow away the schema.

The schema has changed, so our scaffold code is now out-of-date. As we’ve made no changes to the code, it’s safe to regenerate it. Notice that the generate script prompts us when it’s about to overwrite a file. We type a to indicate that it can overwrite all files.

depot> ruby script/generate scaffold Product Admin

dependency

model

exists

app/models/

exists

test/unit/

exists

test/fixtures/

skip

app/models/product.rb

skip

test/unit/product_test.rb

skip

test/fixtures/products.yml

exists

app/controllers/

exists

app/helpers/

exists

app/views/admin

exists

test/functional/

overwrite app/controllers/admin_controller.rb? [Ynaq] a forcing scaffold

force app/controllers/admin_controller.rb

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION A2: ADD A MISSING COLUMN

59

Figure 6.5: New Product Page After Adding Date Column

force test/functional/admin_controller_test.rb force app/helpers/admin_helper.rb

force app/views/layouts/admin.rhtml force public/stylesheets/scaffold.css force app/views/admin/list.rhtml force app/views/admin/show.rhtml force app/views/admin/new.rhtml

force app/views/admin/edit.rhtml create app/views/admin/_form.rhtml

Refresh the browser, and create a new product, and you’ll see something like Figure 6.5 . (If it doesn’t look any different, perhaps the generator is still waiting for you to type a.) We now have our date field (and with no explicit coding). Imagine doing this with the client sitting next to you. That’s rapid feedback!

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION A3: VALIDATE!

60

 

6.3 Iteration A3: Validate!

 

While playing with the results of iteration two, our client noticed some-

 

thing. If she entered an invalid price, or forgot to set up a product descrip-

 

tion, the application happily accepted the form and added a line to the

 

database. While a missing description is embarrassing, a price of $0.00

 

actually costs her money, so she asked that we add validation to the appli-

 

cation. No product should be allowed in the database if it has an empty

 

text field, an invalid URL for the image, or an invalid price.

 

So, where do we put the validation?

 

The model layer is the gatekeeper between the world of code and the

 

database. Nothing to do with our application comes out of the database or

 

gets stored back into the database that doesn’t first go through the model.

 

This makes it an ideal place to put all validation; it doesn’t matter whether

 

the data comes from a form or from some programmatic manipulation in

 

our application. If the model checks it before writing to the database, then

 

the database will be protected from bad data.

 

Let’s look at the source code of the model class (in app/models/product.rb).

File 63

class Product < ActiveRecord::Base

 

end

 

Not much to it, is there? All of the heavy lifting (database mapping,

 

creating, updating, searching, and so on) is done in the parent class

 

(ActiveRecord::Base, a part of Rails). Because of the joys of inheritance,

 

our Product class gets all of that functionality automatically.

 

Adding our validation should be fairly clean. Let’s start by validating that

 

the text fields all contain something before a row is written to the database.

 

We do this by adding some code to the existing model.

File 65

class Product < ActiveRecord::Base

 

validates_presence_of :title, :description, :image_url

 

end

 

The validates_presence_of( ) method is a standard Rails validator. It checks

 

that a given field, or set of fields, is present and its contents are not empty.

 

Figure 6.6, on the following page, shows what happens if we try to submit

 

a new product with none of the fields filled in. It’s pretty impressive: the

 

fields with errors are highlighted, and the errors are summarized in a nice

 

list at the top of the form. Not bad for one line of code. You might also

 

have noticed that after editing the product.rb file you didn’t have to restart

 

the application to test your changes—in development mode, Rails notices

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION A3: VALIDATE!

61

Figure 6.6: Validating That Fields Are Present

 

that the files have been changed and reloads them into the application.

 

This is a tremendous productivity boost when developing.

 

Now we’d like to validate that the price is a valid, positive number. We’ll

 

attack this problem in two stages. First, we’ll use the delightfully named

 

validates_numericality_of( ) method to verify that the price is a valid number.

File 65

validates_numericality_of :price

 

Now, if we add a product with an invalid price, the appropriate message

 

will appear.6

 

 

 

 

6MySQL gives Rails enough metadata to know that price contains a number, so Rails

 

converts it to a floating-point value. With other databases, the value might come back as a

 

string, so you’d need to convert it using Float(price) before using it in a comparison

Prepared exclusively for Rida Al Barazi

Report erratum

ITERATION A3: VALIDATE!

62

 

Next we need to check that it is greater than zero. We do that by writing

 

a method named validate( ) in our model class. Rails automatically calls

 

this method before saving away instances of our product, so we can use it

 

to check the validity of fields. We make it a protected method, because it

 

shouldn’t be called from outside the context of the model.

File 65

protected

 

def validate

 

errors.add(:price, "should be positive") unless price.nil? || price > 0.0

 

end

protected

page 473

 

If the price is less than or equal to zero, the validation method uses

 

errors.add(...) to record the error. Doing this prevents Rails from writing

 

the row to the database. It also gives our forms a nice message to display

 

to the user. The first parameter to errors.add( ) is the name of the field, and

 

the second is the text of the message. Note that we only do the check if

 

the price has been set. Without that extra test we’ll compare nil against 0.0,

 

and that will raise an exception.

 

Two more things to validate. First, we want to make sure that each product

 

has a unique title. One more line in the Product model will do this. The

 

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

File 65

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 starts

 

with http: and ends with one of .gif, .jpg, or .png.7

regular expression

page 476

File 65

validates_format_of :image_url,

 

:with

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

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

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

Prepared exclusively for Rida Al Barazi

Report erratum