Contents 

Ruby on Rails:
Table of Contents
Preface
Zero to Sixty: Introducing Rails
1.1. Rails Strengths
1.2. Putting Rails into Action
1.3. Organization
1.4. The Web Server
1.5. Creating a Controller
1.6. Building a View
1.7. Tying the Controller to the View
1.8. Under the Hood
1.9. What's Next?
Active Record Basics
2.1. Active Record Basics
2.2. Introducing Photo Share
2.3. Schema Migrations
2.4. Basic Active Record Classes
2.5. Attributes
2.6. Complex Classes
2.7. Behavior
2.8. Moving Forward
Active Record Relationships
3.1. belongs_to
3.2. has_many
3.3. has_one
3.4. What You Haven't Seen
3.5. Looking Ahead
Scaffolding
4.1. Using the Scaffold Method
4.2. Replacing Scaffolding
4.3. Generating Scaffolding Code
4.4. Moving Forward
Extending Views
5.1. The Big Picture
5.2. Seeing Real Photos
5.3. View Templates
5.4. Setting the Default Root
5.5. Stylesheets
5.6. Hierarchical Categories
5.7. Styling the Slideshows
Ajax
6.1. How Rails Implements Ajax
6.2. Playing a Slideshow
6.3. Using Drag-and-Drop to Reorder Slides
6.4. Drag and Drop Everything (Almost Everything)
6.5. Filtering by Category
Testing
7.1. Background
7.2. Ruby's Test::Unit
7.3. Testing in Rails
7.4. Wrapping Up
Installing Rails
1.1. Windows
2.1. OS X
3.1. Linux
Quick Reference
5.1. General
5.2. Testing
5.3. RJS (Ruby JavaScript)
5.4. Active Record
5.5. Controllers
5.6. Views
5.7. Ajax
5.8. Configuring Your Application
About the Authors
Colophon
Index
A
B
C
D
E
F
G
H
I
J
L
M
N
O
P
R
S
T
U
V
W
X
Y
Z

Ruby on Rails manual

Prev Page Next Page
Previous Page
Next Page

5.6. Hierarchical Categories

When we generated scaffold code for categories, we got some basic CRUD screens. But they ignore the fact that our categories are hierarchical. The basic problem is every category item has a parent (except for the root category), and there is no way in the CRUD screens to specify the parent of a category.

For now, we are going to fix this in a very simple way that will get you get quickly. There will be plenty of time later for a fancier user interface.

Every category has a name, but these names are not always individually unique because they are qualified by their parents in the hierarchy. For example, you might have two categories named Car, but one of them might have a parent named Bruce while the other has a parent named Curt. A unique identifier for a category would prefix the category name with all of its parents. So for these two Car categories, we might have long names like Root:Bruce:Car and Root:Curt:Car.

Let's implement this attribute as a long_name attribute in our Category model. Edit app/models/category.rb to look like this (the new lines are in bold):

class Category < ActiveRecord::Base
  has_and_belongs_to_many :photos
  acts_as_tree
  def ancestors_name
    if parent
      parent.ancestors_name + parent.name + ':'
    else
      ""
    end
  end

  def long_name
    ancestors_name + name
  end
end

The long_name method returns a string that is the concatenation of the names of all of its parents with its own name. ancestors_name is a recursive method that concatenates all of the parent names with a ":" separator.

You can see this working on our category list page. Edit the categories controller, app/controllers/categories_controller.rb, and change the list action to this:

def list
  @all_categories = Category.find(:all, :order=>"name")
end

Notice that we got rid of the pagination, and that we are sorting the categories by name.

Now edit the corresponding view template, app/views/categories/list.rhtml, to look like this:

<h1>Listing categories</h1>

<table>
  <tr>
    <th>Name</th>
  </tr>

<% for category in @all_categories %>
  <tr>
    <td><%=h category.long_name %></td>
    <td><%= link_to 'Edit', :action => 'edit', :id => category %></td>
    <td><%= link_to 'Destroy', { :action => 'destroy', :id => category },
                                 :confirm => 'Are you sure?' %></td>
  </tr>
<% end %>
</table>

<br />

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

The new code is in bold, and the code dealing with pagination and displaying multiple columns has been removed; plus, the show link was removed because the show page doesn't display anything you can't already see on the list page.

The second bolded line calls the new long_name method.

"#rubyrails-chp-5-fig-7">Figure 5-7 shows what you should see when you browse to http://127.0.0.1:3000/categories/list

Figure 5-7. Showing category hierarchy

Now you need to modify creating and editing a category to let you pick the category's parent. Both actions use app/views/categories/_form.rhtml to display a category form, so that's the only view template you need to modify:

<%= error_messages_for 'category' %>

<!--[form:category]-->
<p><label for="category_name">Name</label><br/>
<%= text_field 'category', 'name'  %></p>

<p><label for="category_parent_id">Parent Category</label><br/>
<%= collection_select(:category, :parent_id,
                      @all_categories, :id, :long_name) %></p>
<!--[eoform:category]-->

Again, the code in bold is new. This code uses the form helper collection_select, which generates HTML <select> and <option> tags to create a drop-down select list.

The first two parameters to collection_select give the name of the database table and column whose value this control will set. The remaining three parameters specify the list of choices the user will have. @all_categories is a list of objects containing the valid choices. :id and :long_name specify the object attributes that get the key value and display value for each choice.

For this new form to work, you need to set @all_categories in the controller for the edit and new methods:

def new
    @category = Category.new
    @all_categories = Category.find(:all, :order=>"name")
  end

...

  def edit
    @category = Category.find(params[:id])
    @all_categories = Category.find(:all, :order=>"name")
  end

Click the Edit link for any category to see the results of your handiwork ( "#rubyrails-chp-5-fig-8">Figure 5-8).

Figure 5-8. Drop-down category selection

5.6.1. Assign a Category to a Photo

Let's update our photo CRUD pages so you can assign categories to a photo. For now, we will take a simple approach like we did with categories.

As with categories, both the edit photo and new photo pages use a common partial view template named _form.rhtml. As mentioned earlier, a partial is small template that does not render an entire page, but just a small, reusable element. This is great for rendering elements that are used on more than one page because the code won't have to be duplicated. Edit the file app/views/photos/_form.rhtml, and add the following to the end (just before the HTML comment):

<p>
  <label for="categories">Categories:</label><br/>
  <select id="categories" name="categories[]" multiple="multiple"
          size="10" style="width:250px;">
    <%= options_from_collection_for_select(@all_categories,
                                           :id, :long_name,
                                           @selected) %>
  </select>
</p>

This code creates a multiple-selection HTML list box populated with the category objects in the instance variable @all_categories using the id of each category as the select option's value and the long_name of each category as the select option's display text. Additionally, each category ID in @selected is displayed as already selected.

Next, you need to add code to the photos controller to set @all_categories and @selected and then grab the form results that are posted back to update the database. Edit app/controllers/photos_controller.rb, and change the edit and update methods to look like this (new lines are in bold):

def edit
  @photo = Photo.find(params[:id])
  @all_categories = Category.find(:all, :order=>"name")
  @selected = @photo.categories.collect { |cat| cat.id.to_i }
end

def update
  @photo = Photo.find(params[:id])
  @photo.categories = Category.find(params[:categories]) if params[:categories]
  if @photo.update_attributes(params[:photo])
    flash[:notice] = 'Photo was successfully updated.'
    redirect_to :action => 'show', :id => @photo
  else
    render :action => 'edit'
  end
end

The edit method first retrieves the photo object that has the target ID and then gets a list of all categories, ordered by name. Finally, it assigns a @selected a list of IDs for all categories already assigned to this photo. @photo.categories returns a list of category objects, one for each category assigned to the photo. The Ruby collect method iterates through that list and, using the attached block of code, creates a new list consisting of just the category IDs (cat.id) converted to an integer (cat.id.to_i).

When the user saves changes to the edited photo, the form data is directed to the update method. params[:categories] contains a list of the selected categories (or nil if no categories were selected). The new if modifier we just added to the update method prevents the line from being executed when there are no selected categories.

Category.find(params[:categories]) returns a list of category objects, one for each category ID in params[:categories]. This category list is then assigned to the target photo's categories attribute.

Let's now make a very similar set of changes to the new and create methods. The only difference is that a new photo doesn't have any existing selected categories, so the @selected variable is not set:

def new
  @photo = Photo.new
  @all_categories = Category.find(:all, :order=>"name")
  @selected = []
end

def create
  @photo = Photo.new(params[:photo])
  @photo.categories = Category.find(params[:categories]) if params[:categories]
  if @photo.save
    flash[:notice] = 'Photo was successfully created.'
    redirect_to :action => 'list'
  else
    @all_categories = Category.find(:all, :order=>"name")
    render :action => 'new'
  end
end

Again, the new lines are bold.

That's all there is to it. You can now assign multiple categories to each photo. Give it a try! You should be starting to see how easy it is to incrementally build out your Photo Share application.


Previous Page
Next Page