Section 5.6. Hierarchical Categories


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 up and running 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.

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



Ruby on Rails[c] Up and Running
Ruby on Rails[c] Up and Running
ISBN: B003D3OGCY
EAN: N/A
Year: 2006
Pages: 94

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net