Web Application Development Practical: Ruby on Rails - Working with Database (II)

This practical deals with multiple tables in a web application. Before doing this practical, you should have finished the previous practical.

Before we start this practical, did you notice the URLs as you were playing around with your new cookbook? Rails tries very hard to present the user with pretty URLs. Rails URLs are simple and straightforward, not long and cryptic.

Creating Actions and Views

Firstly, we improve the web page that shows the list of all recipes. We take over the handling of the list action from the scaffolding. Edit recipes_controller.rb (under cookbook/app/controllers) and add a list method similar to Figure 1.

def list
end



Figure 1. A new list method

You have to enable in your routes file any extra methods you add to the controller as well: Edit routes.rb (under cookbook/config) like Figure 2, by adding the following:

resources :recipes do
    get 'list', :on => :collection
end

 

Figure 2. Add new method to routes file


Browse to http://127.0.0.1:3000/recipes/list and you should see something like Figure 3.



Figure 3. The results of the new list method


Template is missing -- because we haven't created any view templates yet. Let's create one that only shows the title and date of each recipe. The template file is named
list.html.erb for which we should put under cookbook\app\views\recipes. It is simply an html file with Ruby code embedded within ERB tags.

<html> 
  <head>
    <title>All Recipes</title>
  </head>
  <body>
  
  <h1>Online Cookbook - All Recipes</h1>
  <table border="1">
  <tr>
  <td width="80%"><p align="center"><i><b>Recipe</b></i></td> 
   <td width="20%"><p align="center"><i><b>Date</b></i></td>
  </tr>
   
  <% @recipes.each do |recipe| %>
  <tr>
  <td><%= link_to recipe.title, :action => "show", :id => recipe.id %></td>
  <td><%= recipe.date %></td>
  </tr>
  <% end %> 
  </table>
  <p><%= link_to "Create new recipe", :action => "new" %></p>
 </body> 
</html>
      

Edit recipes_controller.rb and add the single line of code to the list method.

def list
   
@recipes = Recipe.all
end


Refresh your browser and you should see something like Figure 4.



Figure 4. A nicer recipe list


Now this definitely looks better! How does it work?

When a user browses to http://127.0.0.1:3000/recipes/list, Rails will call the list method we just created. The single line of code in the method asks the Recipe class for all recipes from the database, assigning all the recipes to the variable @recipes. Next, Rails will look for a template to render and return to the browser. Most of our list template is standard HTML. The real action is in this section of the template:

<% @recipes.each do |recipe| %>
   <td><%= link_to recipe.title, :action => "show", :id => recipe.id %></td>
   <td><%= recipe.date %></td>  
</tr>
<% end %>

This embedded Ruby code iterates through the collection of recipes retrieved in the controller. The first cell of the table row creates a link to the recipe's show page. Notice the attributes used on the recipe object (title, id, and date). These came directly from the column names in the recipes table.

Adding Categories to the Cookbook

We want to be able to assign a recipe to a category (like "Food") and be able to list only those recipes that are in a particular category. To do this, enter this command in your terminal (make sure you are in the cookbook directory):

rails generate scaffold Category name:string

Then, use a rake command to run the migration:

rake db:migrate

Browse to http://127.0.0.1:3000/categories/new and create two categories named Food and Beverage.

 

Assigning a Category to Each Recipe

The cookbook application now has recipes and categories, but we still need to tie them together. We want to be able to assign a category to a recipe. To do this we need to add a column to our recipes table to hold the category id for each recipe, and we'll have to write an edit action for recipes that provides a drop-down list of categories.


First, add a category_id field to the recipe table as an Integer to match the key of the category table. As mentioned in Section 3.3 of our course material, we should not edit the database directly by database editing tools because this may cause problems. Instead, we use database migrations. To add a field into the recipe table, type the following command in the terminal:

rails generate migration AddCategoryIDToRecipes category_id:integer

A file 2012XXXXXXXX_add_category_id_to_recipes.rb has been generated in the cookbook/db/migrate folder. Take a look at this file and understand the meaning.


Then run the database migration as usual:

rake db:migrate


We can check whether the recipes table has been successfully updated by using the rails console with sandbox:

rails console --sandbox

In the sandbox mode, all operations we did to the database will be rolled back. When we enter the console, type the following command:

Recipe.find(1)

The above command will return the first record in the recipes table, take a look at the returned record and check whether it contains the field category_id. Type “exit” to quit the console mode.

Notice if there is anything wrong in the migration, we can always roll back the last migration by using:

rake db:rollback

After the category_id has been added to the recipes table, now tell the Recipe model class about this too.
Edit
cookbook/app/models/recipe.rb and cookbook/app/models/category.rb to add a single line to each model class, as shown in Figures 5 and 6. Notice as we add a new field category_id in the table recipes, we should make this field accessible.

 



Figure 5. Setting relationships in the Recipe model

class Recipe < ActiveRecord::Base
   attr_accessible :date, :description, :instructions, :title, :category_id
  
belongs_to :category
end

 


Figure 6. Setting relationships in the Category model

class Category < ActiveRecord::Base
    attr_accessible :name
  
has_many :recipe
end


It should be pretty obvious that this tells Rails that a recipe belongs to a single category and that a category can have many recipes. These declarations actually generate methods to navigate these data relationships in Ruby code.
For example, if I have a recipe object in @recipe, I can find its category name with the code
@recipe.category.name. Similarly, if I have a category object in @category, I can fetch a collection of all recipes in that category using the code @category.recipe.


Now it's time to take over the edit recipe action and template from the scaffolding so that we can assign categories. Edit
recipes_controller.rb (under cookbook\app\controllers) and add @categories = Category.find(:all ) under an edit method like in Figure 7.



Figure 7. The Recipe controller's new edit method

 

def edit
  @recipe = Recipe.find(params[:id])
  
@categories = Category.find(:all)
end


This creates two instance variables that the template will use to render the "edit recipe" page. @recipe is the recipe that we want to edit (the id parameter comes from the web request). @categories is a collection of all the categories in the database. The template will use it to create a drop-down list of category choices.


Edit _form
.html.erb (under cookbook/app/views/recipe) as in Figure 8. The name of this file begins with “_”, which indicates that it is a partial form to be included in other templates (in this example, it is the edit.html.erb).

The following block is inserted:



Figure 8. Editing the _form.html.erb

<div class="field">
    <%= f.label :category %><br />
    <%= f.collection_select :category_id, @categories, :id, :name, :prompt => true %>
</div>

You can see the @recipe and @categories variables being used. You may found this discussion about the method collection_select useful. The method collection_select can automatically load all values of @categories, and it can also pre-select the value of category according to the current recipe. Study this template and then try it out.

Browse to http://127.0.0.1:3000/recipes/list and edit the recipe for "Tap water for everyone" Change its category to "Beverage," as shown in Figure 9.



Figure 9. Changing the category for a recipe

Continue to modify the rest of the application to make it consistent with the newly added field. For example,

1. you should show the category option when you create a new recipe or show the details of a recipe.

2. You could add a function that can list all recipes given a category name or ID (consider using @category.recipe to list all recipes having the same category)

Exercise

Continued from the previous exercise, you are required to create a category table for your to-do list application. And this table should contain at least three entries such as business, personal and family. Then link the category table with the to-do list table from the previous exercise and assign a category to every entry in the to-do list table.