ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Rolling with Ruby on Rails Revisited, Part 2

by Bill Walton
01/05/2007

Editor's note: this is the second part of Rolling with Ruby on Rails Revisited, an update to Curt Hibbs's influential Rolling with Ruby on Rails and Rolling with Ruby on Rails Part 2.

Also check out Bill Walton's monthly series, Cookin' With Ruby on Rails.

NOTE TO READER: This tutorial is intended for use with Rails 1.2.6. It has not yet been updated to support Rails 2.x.

Paul: Good pizza. Thanks, Boss. Listen, I know you asked us to take the details off on a sidebar, but I'm dying to see the code that Rails generated to produce this. Would you mind if I asked CB to take a minute and show us?

Boss: Actually, Paul, I'm pretty interested in seeing that myself. CB, would you mind? But keep it short for now, OK?

CB: No problem. ("Easy now," CB reminds himself, grinning. "You don't want to pull the hook.") Let's take a quick look at the controller code for recipes (Figure 12). Remember that Rails stores controller code in the controllers directory in a file with the name it picks based on the arguments we supplied when we generated the scaffolding. In our case, we told it to generate a model and a controller; both named recipe. The controller gets named recipe_controller.

Rails-generated recipe controller
Figure 12. Our recipe controller

Boss: That's quite a bit of code, and I get most of it. I mean, you said Rails was going to generate code for create, read, update, and delete. I see a create method, and update and destroy methods. I guess the show and list methods are for the read, but what are the new and edit methods for? They don't seem to do much.

CB: That's one of the differences between a real web-based application and site that's just a bunch of static pages with some scripting, Boss. In a scripted site, we'd probably hand-code the HTML form the visitor uses to enter the data. Then we'd code the create method on the server-side to save the data when the form gets submitted. Remember, Rails generates the pages it serves, so it's going to create the HTML form for us. The new and edit methods just supply an object for the form to use. The create and update methods save the object to the database when the form gets submitted.

Boss: OK. I think I understand the basics now. Paul, you OK if we move on now?

Paul: Sure, Boss. I'll meet with CB later to get him to take me deeper.

CB: OK, then. Let's get back to it. You might have noticed that there's no way to assign a category on the New recipe page. That's because I told MySQL that each recipe will have a category, but I didn't tell Rails the same thing. All I need to do to fix this is to tell Rails how the two are related and then add a couple of lines to the view file for the New recipe page.

I tell Rails about the relationship between recipes and categories in their models. In the cookbook2\app\models\category.rb file (Figure 13), I tell Rails that each category will have many recipes assigned to it by adding one line.

has_many :recipes

adding relationship to category model
Figure 13. The category model file

Then I tell Rails that each recipe gets assigned to one category by adding one line to the recipe model (cookbook2\app\models\recipe.rb, Figure 14).

belongs_to :category

adding relationship to recipe model
Figure 14. The recipe model file

Now Rails knows about the relationship between the two models.

To give the visitor the ability to select a category, I need to add a couple of lines to the New recipe view. The view file for the new method in the recipe controller is in the recipe subdirectory under views. To fix the view, I'll open the cookbook2\app\views\recipe\_form.rhtml file (Figure 15).

original form file
Figure 15. The Rails-generated form partial

Paul: CB, I noticed there were a bunch of files in that directory, and one of them is new.html. How come you're not editing that file?

CB: Keen eye, Paul. Rails generates a view file for each method in the controller, but one of the guiding principles in Rails is "Don't Repeat Yourself." Rails views let us modularize our view code. Because the new method and the edit method both work on the same object, Rails creates a partial, names it _form.rhtml, and uses it in the new and edit views. A view can include as many partials as we need to keep our code DRY. We'll do more work on view files shortly.

CB: To add the category selection, I need to add two lines of code.

<p><label
for="recipe_category_id">Category</label><br/>
<%= select("recipe", "category_id", Category.find(:all).collect
{|c| [c.name, c.id] }) %></p>

The first line is straight HTML. The second is Ruby code. Rails sees the <% ... %> and knows that what's inside is embedded Ruby code that it needs to execute to generate the HTML to send back to the browser. Because this one has the = sign, Rails will generate the HTML we need for a select tag. If it didn't have the = sign, and we'll see some of that too, Rails would execute the code but wouldn't generate any HTML. You might have noticed that the New recipe page lets the visitor select a date, but the requirements say the system must assign the date. While I'm in here, I'm going to remove the lines that let the visitor furnish it. Now we have a form partial that looks like Figure 16.

updated recipe form file
Figure 16. Updated form partial

CB: Now we have to have the system assign a date whenever someone creates or updates a recipe. That needs to happen in the recipe controller, so I'll open cookbook2\app\controllers\recipe_controller.rb (Figure 17) and add a line to both the create and update methods.

@recipe.date = Time.now

updated recipe controller
Figure 17. Taking over the date assignment in the controller

Done!

CB: OK, Boss. We've added six lines of code. Time to check our work. I modified the model files so I need to restart our web server: mongrel. For performance reasons, even in development mode Rails only reads an application's model files at startup. Whenever we modify a Rails model, we need to restart our web server. To stop Mongrel, in the Command window, I just hit Ctrl-C. Then, to restart it, I enter...

mongrel_rails start

CB: OK, Boss. You ready to do the honors? Point our browser to http://localhost:3000/recipe/new, and ...

Voilá! Date's gone and there's a drop-down list for Category.

Paul: I noticed that in the controller, most of the references were to a variable named @recipe. Here in the form, though, it looks like you're referring to the same object using a different syntax.

CB: You're right, Paul. The text_field statements in the view use a different syntax, but refer to exactly the same object that the controller uses with an instance variable. It can be confusing at first. There's a much more accurate detailed explanation, but here's how I kept it straight when I was getting started. I use an instance variable to push stuff to the user. Any instance variable I create in my controller is automatically available to the view I use for generating the HTML file I'll send back to the visitor's browser. When I need to get information from the visitor, it will come back in something known as the params hash. That's a data structure that gets built in the visitor's browser and sent back when she starts a new request-response cycle. The syntax of text_field, for example, actually helped me remember how to get the data out of the form. If I have a text_field in a form like <%= text_field 'recipe,' 'description,' %> I can access that in the controller with @recipe.description = params[:recipe][:description], or I can present it back to the visitor in another cycle if I want by using @recipe = Recipe.find(the_one_I_want) in the controller, and then using <%= @recipe.description %> in the view. There's more to it than that, of course, but it helped me keep things straight in the beginning.

updated new recipe page
Figure 18. Updated recipe creation page

Go ahead and create a new recipe, Boss. You just fixed us a pizza, so that recipe's probably fresh in your mind ;-)

our first recipe!
Figure 19. Our first recipe!

CB: Starting to get impressed?

Boss: Well, sure. But it doesn't look anything like the original yet. Can we fix that up a little now?

CB: Piece of cake! I see several things that need attention. The recipe display needs a border. The Title column heading needs to change, and the table needs to have a column to display each recipe's category. The consultant's list of requirements says that clicking on a recipe's category will sort the list so that only recipes of that category are shown. That means we need some sort of link on each recipe's category. The screen shot also shows that the instructions should not appear in the listing. The requirements say that the Edit feature will be available from a noneditable view that we reach by clicking on the recipe name. Finally, the screen shot shows that the Show and Destroy links for the recipe need a little rearranging.

Boss: That sounds like a lot of work!

CB: A lot of work on some platforms. Not on Rails. Watch this. I open the cookbook2\app\views\recipes\list.rhtml file (Figure 20). Basically, what it's doing is reading the Recipe model and creating a table with a column for each attribute, putting a table header on each column with the name of the attribute, and then creating rows for each record in our Recipes table.

recipe list view file
Figure 20. Rails-generated recipe list view

CB: We're going to make some pretty major changes here. I need to make it look like this:

<table border="1">
  <tr>
    <th width="60%"><em>Recipe</em</th>
    <th width="20%"><em>Category</em></th>
    <th width="20%"><em>Date</em</th>
  </tr>
 
<% for recipe in @recipes %>
  <tr>
    <td><%= link_to %Q{#{recipe.title}}, :action => 'show', :id => recipe %>
        <%= link_to '(delete)', { :action => 'destroy', :id => recipe },
                               
  :confirm => 'Are you sure?', :post => true %></td>
    <td><%= link_to %Q{#{recipe.category.name}},
                   {:action => 'list', :category_id => recipe.category.id} %></td>
    <td><%=h recipe.date %></td>
  </tr>
<% end %>
</table>

CB: OK. By my count, we just added seven lines of code. Better check our work ;-) Boss, would you refresh the browser?

CB:Hey, Paul. Remember what I was saying about instance variables versus params? See how we use the instance variable @recipes in the for loop? Its value gets set in the controller, and now we're using it in the view to create the list of recipes to display in the page we're sending back to the visitor.

new listing view in browser
Figure 21. Making it pretty

Boss: It looks like we're getting close (Figure 21).

CB: We definitely are. But we need to remember to test to make sure our filtering works when we click on a recipe's category. We've only entered one Category so far, so we can't test that yet. While we're in the view code, though, let's finish that stuff off.

The requirements say we need a footer on every page that takes us to either the Recipes listing or the Categories listing. The window title and page heading also need to be the same on every page. Let's add all that now.

My cookbook2\app\views\layouts directory contains two files: category.rhtml and recipe.rhtml. I'll delete one (either one) and rename the other to application.rhtml. Now I'll open application.rhtml (Figure 22).

original application layout file
Figure 22. Working with the application layout file

CB: First I'll change the window title...

<title>Online Cookbook</title>

Then I'll add a heading for the page...

<h1>Online Cookbook</h1>

Rails applies what it finds here to all the pages that the application serves. So by adding just a few lines here I'll add the footer we need on every page.

<p><% if params[:controller] == 'recipe' %>
     <%= link_to "Create new recipe", :controller => "recipe", :action => "new" %>
   <% else %>
     <%= link_to "Create new category", :controller => "category", :action => "new" %>
   <% end %>
 
   <%= link_to "Show all recipes", :controller => "recipe", :action => "list" %>

   <%= link_to "Show all categories", :controller => "category", :action => "list" %></p>

Which gives us Figure 23

updated application layout file
Figure 23. Modified application layout file

We save that. OK, Boss. We changed the content of the <title> line and, by my generous approach (counting changed lines, too), we added seven lines of code. You ready to check our progress again?

Boss: Sure, CB. Refresh our browser and... (Figure 24).

browser shows new layout
Figure 24. Application-level results in the browser

Boss: Dammit man!

Paul: Earlier you were using views and partials to tweak the visuals, CB. Now you're using layouts. Could you explain a bit about them?

CB: Sure, Paul. Layouts are a mechanism Rails provides to make it easy to apply the DRY principle to our view code. When we generate the scaffolding for a model/controller pair, Rails assumes that we're probably going to want some commonality in the look and feel for the pages rendered by the methods in the controller. Layouts let us 'wrap' the views. By default, when Rails needs to generate a page, it looks for a view file with the same name as the method. Then it looks further instructions in a layout file with the same name as the controller. If that layout file doesn't exist, then Rails looks for a file called application.rhtml. Using the combination of partials, views, and layouts, Rails gives me an easy way to control what gets presented to the visitor at the method level, the controller level, and at the application level.

CB: Yep. But remember, we didn't get to test the filtering it's supposed to do. Now that we can get there from here, lets go add another category so we can do that. Click the Show all categories link for us (Figure 25).

categories listing
Figure 25. Initial category listing

CB: We have a little cleanup to do, the same stuff we already did for recipes. But before we do that, let's check the behavior. Let's see what happens when I click the link to add a new category. Either link will work fine. (Figure 26)

new category page
Figure 26. New category creation page

We need the same cleanup here, too. Let's stay on point, though, and go ahead and add a new category named "beverages" (Figure 27).

updated category listing
Figure 27. New category added to list

OK. Our new category feature works, so let's clean up the look. We don't have any screen shots of what the Category listing should look like, so I'm just going to assume it's similar to the Recipe listing. Let's go ahead and make that change.

I'll edit the cookbook2\app\views\category\list.rhtml file. It looks pretty much like the list view for recipes. I'm going to make essentially the same changes I made before.

Delete the heading line.

Add the border attribute to the table tag.

<table border="1">

This time I'll leave the table headings alone because there's only one and it seems OK. That means I just need to replace the code that generates the table rows.

<td><%= link_to %Q{#{category.send(column.name)}}, :action => 'show', :id => category %>
   
    <%= link_to '(delete)', { :action => 'destroy', :id => category },
                    :confirm => 'Are you sure?', :post => true %></td>

Then delete the lines below the </table> tag. Now that we're done with that... (Figure 28).

updated category view
Figure 28. Updated category list view file

I'll go ahead and save it.

Boss: I think I'm getting the hang of this, CB (he says, with a grin) We added two more lines. Better check our work again. Right?

CB: Looks like there's hope for you yet, Boss (grinning too). Why don't you do the honors?

Refresh our browser and... (Figure 29).

updated category view in browser
Figure 29. Updated category listing in browser

Boss: I'm really seeing what you meant when you said "One of the things that Rails really enables is an iterative and incremental approach to development." I can see where, in a normal project, you couldn't go very long before you'd need some Customer feedback. You'd run out of things to do pretty quick if they didn't stay close!

CB: You have it.

I just remembered the new.rhtml and edit.rhtml files under both the views\categories and views\recipes subdirectories need to have to have the <h1> heading lines removed from them, too. I'll go ahead and do that real quick.

Say, Boss. My fingers are really sore from all this typing ;-) Now that we have more than one category, we need to make sure the category selection works when we add or update a recipe. Think you could take care of that for us? Click the Show all recipes link. Then click the Create new recipe link. Enter a new recipe and assign it to the beverages category. We need at least one main course and one beverage (Figure 30).

listing all recipes
Figure 30. Listing recipes with different categories

CB: Looks like all we have left to do is the filtering when we click on a recipe's category. I set up the link to use filtering when I was fixing the recipe listing view, but I didn't put the behavior in place yet. It goes in the recipe controller, so I open the cookbook2\app\controllers\recipe_controller.rb file and replace the contents of the list method with this code:

if params[:category_id].nil?
  @recipes = Recipe.find(:all)
else
  @recipes = Recipe.find(:all,
                        :conditions => ["category_id = ?", params[:category_id]])
  params[:category_id] = nil
end

Once I'm done, I'll save the file. OK, Boss. We've added another six lines of code. You know what that means ;-)

Boss: I think I'm getting the rhythm ;-) Let's see. Refresh our browser, then click on the beverages link (Figure 31).

just beverage recipes
Figure 31. Recipe list filtered for a single category

CB: Et voil&aacutel;! Okay, Boss. Now click the Show all recipes link (Figure 32).

showing all recipes
Figure 32. We're almost done!

CB: Shucks, Boss!!! If I didn't know better, I'd think we were done!

Boss: It sure looks like it!

CB: Just to be sure, though, why don't you give 'er a thorough run through. HINT: Click the Show all categories link, then click the (delete) link on the Beverages category.

Boss: Hey! That's not right! (Figure 33)

Error message
Figure 33. A Rails error message

CB: Yep. Remember the first line of Rails code we entered? It was in category.rb, the category model file.

has_many :recipes

The error message is telling us we just tried to delete a record that has children. Rails stopped us from accidentally deleting a bunch of recipes. There are a couple of ways we could handle this but the requirements don't say anything about this situation or how to handle it. That's really a decision the Customer should make, you know. For now, I'll just fix it so the app doesn't crash. Then you can ask your boss tomorrow how he wants it to work. I'll add some code to ignore the request if there are child records. The code goes in the destroy method in the category controller (cookbook2\app\controllers\category_controller.rb) file. First I'll ask for all the recipes that have been assigned to the category I'm about to delete.

recipes = Recipe.find(:all, :conditions => ["category_id = ?", params[:id]])

Then I'll wrap the record deletion in an if statement that won't let the deletion happen if Rails found any records.

if recipes.empty?
  Category.find(params[:id]).destroy
end

And we're done. So I save the controller file... (Figure 34).

fixing the parent deletion problem
Figure 34. Modified destroy method to fix the error

Boss: And we added two more lines! Time to check our work again ;-) Refresh the browser. Go back to the recipes listing and delete the ice tea recipe. Now go back to the categories listing and delete the beverages category. (Figure 35).

the delete worked
Figure 35. Houston, we have a launch!

Boss: It's done!!!

CB: What?!?!?!? That can't be right! The pizza's not even cold yet ;-)

Seriously, though, Boss, you're right. We've just finished our first fully functional, web-based, database-driven web application using Rails. What did it take? Thirty or forty minutes? Keystrokes? By my count it's:

MySQL commands: 5
SQL script:     17 lines
Rails commands: 3
Rails code:     31 lines

31 lines of code? No configuration files? One SQL script and eight command-line entries? And we're done?

Boss: You're right. This is amazing. You say you need help with the Ops guys? I'm in. I'm going to tell my boss about it tomorrow morning, too. Do you have any words you want me to use?

CB: You bet. Tell him this.

Rails is the next level in web programming, and the developers who use it will make web applications faster than those who don't. Ruby on Rails makes software development simple. It makes it fast. It makes it fun. And best of all? Rails is available, for free, right now, under an open source MIT license.

Boss: I will, CB. And I'm gonna go make that call to Ops right now. The next thing I need from you is some info on how we learn more about Rails. And thanks again. This was definitely worth the price of a pizza!.

THE END (of our story, but the BEGINNING, we hope, of your Rails adventure!)

Click to continue your adventure as CB shows Paul how to use migrations to manage their Cookbook app's database schema.

Resources

Web sites

Mailing lists

Bill Walton is a software development/project management consultant/contractor.


Return to O'Reilly Ruby.

Copyright © 2009 O'Reilly Media, Inc.