- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
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