Table of Contents

View Screencast (Quicktime)

Right-click on the button above and choose Save As to save file to your computer


Support the Course

There's no charge for the course, but we greatly appreciate any donations.

Suggested donations:
  • One or two lessons: $5
  • Several lessons $10
  • Entire course: $25 – $50

We hope you've found the course to be valuable, and we appreciate any support you care to provide.


Sign Up Now!

If you aren’t already receiving our course lessons via email, sign up now to be sure you don’t miss anything.

Every few days, we’ll send you an email with a link to the next episode, plus a list of additional resources for advancing your knowledge.

There’s no cost and no obligation. And we’ll never share your email address with any third party.

We’ll send you the first lesson right away.

Looking for a Powerful Hosted CMS?

The authors of the Learning Rails course also offer a very powerful hosted content management system for web designers, which enables you to build sophisticated, database-driven sites without programming. This is a great alternative to building a custom Ruby on Rails site for those applications for which you just can't justify the cost of a custom solution.

To learn more, sign up for the free Learning Webvanta course on building database-driven web sites without programming.

Lesson 15
Pages and Subpages

comments Bookmark and Share

Goals

In this lesson we’re adding a hierarchy to our pages. Instead of a single pool of pages with a navigation button for each, we want to have subpages as well, which don’t appear in the top navigation bar but are listed in second-level navigation on their parent page.

Please note that, while we’ve tried to make these notes complete, they aren’t the full tutorial; that’s in the screencast, which you can access via the link on the left.

Setup

We begin with the code with which we ended Lesson 14. These zip files contain the beginning and ending states of the code:

Model association

To associate subpages with their parent pages, we could create a subpage model, and then we could write in the page.rb model:

has_many :subpages

And in the subpage.rb model:

belongs_to :page

But to do it this way creates a lot of duplication, since the subpage model would need to behave just like the page model. So we use what’s called a self-referential association: a page object has many page objects, and a page can have (belong to) a parent. This requires a more complex declaration in the page.rb model, as we’ll see, but once that’s done, the self-referential model works the same as it would if subpage was a separate model.

Extending the Page table

We need to add some fields to the Page model. As with any change to the database structure, we create new migration, with whatever name we’d like:

script/generate migration AddSubpages

These three lines in the “up” method create the new fields:

def self.up
  add_column :pages, :parent_id, :integer
  add_column :pages, :navlabel, :string
  add_column :pages, :position, :integer
end

And the corresponding three lines in the “down” method allow us to set the database back to its prior condition, should we want to roll back the database:

def self.down
  remove_column :pages, :parent_id
  remove_column :pages, :navlabel
  remove_column :pages, :position
end

Once we’ve created and saved the migration file, we apply the migration:

rake db:migrate

Defining the association

In the Page class, we need to specify the relationship between parent pages and subpages. We have a field, parent_id, that we created in the previous migration to serve as the foreign key for the association. Since this is a self-referential association, the default naming schemes don’t apply, and we need to explicitly specify the class name and foreign key field name:

has_many :subpages, :class_name => 'Page', :foreign_key => 'parent_id'

The has_many declaration allows us to then write, typically in our controllers, page.subpages, to retrieve all the pages that have the current page as their parent.

Since this is a self-referential association, the “belongs_to” side of the relationship also goes in the page model:

belongs_to :parent, :class_name => 'Page', :foreign_key => 'parent_id'

This declaration allows us to then write page.parent to find the parent page, if there is one.

Writing Custom Finders

Until now, we’ve mostly used standard find methods directly in our controllers. It’s a better design practice, however, to push logic for finding into the model. By writing a custom method to serve as a special kind of find, we can encapsulate more of how the page model works in that single file.

Here’s a method to provide the complete set of navigation tabs:

def self.find_main
  Page.find(:all, :conditions => ['parent_id IS NULL'], :order => 'position')
end

And here’s a variant that only shows tabs for site visitors (i.e., no pages that have their “admin” attribute set):

def self.find_main_public
  Page.find(:all, :conditions => ["parent_id IS NULL and admin != ?", true], :order => 'position')
end

Creating main navigation tabs

Now we need to update the method that tells the layout what tabs to display. We created this method in an earlier lesson and put it in controllers/application.rb, which is “mixed in” to every other controller. Let’s modify get_pages_for_tabs in application_controller to use the new finds:

def get_pages_for_tabs
  if logged_in?
    @tabs = Page.find_main
  else
    @tabs = Page.find_main_public
  end
end

Using the navlabel text

We have a new attribute in the page model for the navigation button text, so let’s change the line of code in views/layouts/application.rhtml.erb from:

<li><%= link_to page.title, view_page_path(page.name) %></li>

to use the new attribute:

<li><%= link_to page.navlabel, view_page_path(page.name) %></li>

Providing access to the new attributes

We need to update the admin forms for the Page scaffold to let us set and modify the new attributes. Instead of changing both new and edit views (in views/pages), we create a form partial that is used for both the new and edit views.

Copy the guts out of either the new or edit view:

	<p>
	  <b>Name</b><br />
	  <%= f.text_field :name %>
	</p>

	<p>
	  <b>Title</b><br />
	  <%= f.text_field :title %>
	</p>

	<p>
	  <b>Body</b><br />
	  <%= f.text_area :body %>
	</p>

	<p>
	  <b>Admin?</b><br />
	  <%= f.check_box :admin %>
	</p>

Paste this into a new file in views/pages, called “_form.rhtml.erb”. The underscore identifies it as a partial.

Now, in both the new and edit views, all this text can be replaced with:

<%= render :partial => 'form', :locals => {:f => f} %>

To create the HTML pop-up menu for choosing the parent page, we use the collection_select method:

<p>
	<b>Parent Page</b><br />
	<%= f.collection_select :parent_id, Page.find(:all), :id, :title, :include_blank => true %>
</p>

Add a field to specify the position:

<p>
  <b>Position</b><br />
  <%= f.text_field :position, :size => '3' %>
</p>

And a field for the nav label:

<p>
  <b>Nav Label</b><br />
  <%= f.text_field :navlabel %>
</p>

Creating subpages

Now we can use the page admin interface to set the navlabel and position for each of the existing pages.

Then let’s create some subpages. For our example we create two pages, Services and Products, that have About Us as their parent page.

The main navbar should still show only the main pages, labeled and sorted according to our new navlabel and position attributes.

Creating second-level navigation links

To create the second-level menu, we need to find the subpages in the viewer controller’s show method, by adding the following between the two existing lines:

@subpages = @page.subpages

At the top of views/viewer/show, we’ll add a simple list of the any subpages:

<% unless @subpages.empty? %>
	<div id='subnav'>
		<ul>
			<% for page in @subpages %>
				<li><%= link_to page.navlabel, view_page_path(page.name) %></li>
			<% end %>
		</ul>
	</div>
<% end %>

And some minimal styling for this div, to put in the stylesheet:

#subnav {
	width: 200px;
	float: left;
	border-right: 1px solid black;
	margin-right: 20px;
}

The About Us page now shows links for its two subpages, and clicking on those links displays those pages.

Bugfix: there’s been a spurious <div> tag after the navigation links in the application layout, which we’ve deleted in the code for this lesson.

Wrapping up

We now have a usable two-level page structure. Before putting this into real use, we’d want better styling for the level-two navigation links, and some indication of what the parent page is when we’re on a subpage.

In our next lesson, we’ll take are of a few of these lingering details, preparing to move on to the contact form and resources page in later lessons.


Add Your Comments

(not published)

Reader Comments

22 comments

Mysql error

From: Buckwald, 10/20/10 12:06 PM

After completing this I get the following error: Mysql::Error: Unknown column 'parent_id' in 'where clause': SELECT * FROM `pages` WHERE (parent_id is NULL and admin != 1) ORDER BY position Has anyone seen this before?

Great lesson!

From: Wayne Simacek, 09/17/09 08:28 AM

Thanks for the great lesson guys! Wayne

Same error

From: Craig, 08/29/09 03:05 PM

I'm getting the same error as SJS. I even downloaded the code as of the end of lesson 15, cd'd into it, and ran script/server start. I'm getting: NameError in Viewer#show Showing viewer/show.html.erb where line #22 raised: uninitialized constant Err::Acts::Textiled::ClassMethods::RedCloth Extracted source (around line #22): 19: 20: <% else %> 21: 22: <%= @page.body %> 23: 24: <% end %> All of my names look to be right... any ideas what's wrong?

NameError in Viewer#show - has anyone seen this error after making this lesson's changes?

From: SJS, 08/19/09 06:35 PM

After following the edits in the tutorial get the following error when I go to the application. Any one seen this or have suggestions re: how to initialize the variables? NameError in Viewer#show Showing app/views/viewer/show.html.erb where line #22 raised: uninitialized constant Err::Acts::Textiled::ClassMethods::RedCloth Extracted source (around line #22): 19: 20: <% else %> 21: 22: <%= @page.body %> 23: 24: <% end %>

Putting Ruby in page body

From: Michael Slater, 07/09/09 07:18 PM

You can put Ruby code in the page body, but then you need to execute the page body as Ruby code, rather than displaying it as text. This means that anyone who can enter text into your CMS can run arbitrary programs on your server, which is pretty dangerous.

Pages in the Database

From: Robert Sanchez, 06/20/09 07:53 PM

Throughout your entire application, a core premise is that you have all of the page data inside the database. But the pages that you have been creating have generally been static pages. What would happen if inside the "body" of a page, it was some dynamic ruby code? Would it still execute properly?

Conditions hash and custom finders

From: Michael Slater, 02/18/09 08:53 AM

The conditions hash only works on the find method. To make it work on the find_main custom finder, the conditions hash would have to be passed to that method as a parameter, and then merged in with the conditions specified in the find_main method. That's why we wrote it the way we did.

Refactoring find_main_public method

From: Morgan, 02/18/09 12:19 AM

Hello, I tried refactoring the following lines of code

def self.find_main 
  Page.find(:all, :conditions => ['parent_id IS NULL'], :order => 'position') 
end 

def self.find_main_public 
  Page.find(:all, :conditions => ["parent_id IS NULL and admin != ?", true], :order => 'positio 
end 
by changing it into this
def self.find_main
    Page.find(:all, :conditions => ['parent_id IS NULL'], :order => 'created_at DESC')
  end
  
  def self.find_main_public
    Page.find_main(:conditions => ['admin != ?', true])
  end
Unfortunately it doesn't work. Does the :conditions hash only work on find and find_by methods, or is the syntax that I've written incorrect? I've really enjoyed your screencasts, thanks so much!

Page Listings

From: Dustin Brewer, 12/10/08 10:56 PM

If I want to show an unordered list of all the pages with a nested list (under each main page) of subpages how could I achieve this? Does anyone have an answer? I really appreciate the help and the tutorials.

Sub Pages Redirect

From: Alastair Palmer, 11/04/08 03:01 PM

I had to alter the sub-pages navigation select to check for redirect to allow these pages to accept data from my data base. <% for page in @subpages %> <% if page.redirect? %>

  • <%= link_to page.navlabel, :action => page.action_name, :controller => page.controller_name, :name => page.name %>
  • <% else %>
  • <%= link_to page.navlabel, view_page_path(page.name) %>
  • <% end %> <% end %> Great series and a great intro to rails. Thank you very much

    Required a tutrorial on Receiving email

    From: Nicholas, 07/29/08 10:09 PM

    Required a tutrorial on Receiving email , something similar to your add a comment. looking forward to read it. hope you'll add it soon. thank you -Nicholas I

    Tracking down a bug in Parent.id in pages#index.html.erb

    From: Walter, 07/17/08 09:02 PM

    I’m getting the Parent_id off by one (+1) even though I have @pages[page.parent_id.to_i – 1].name in the parent column of pages#index.html.erb. I had to rollback my db since it developed some error when a page was set to a blank index then deleted. Should it fail it will fail the entire index and the db then has to be reset. Oddly if I go to Edit the page, the correct parent page is selected and is not offset. How is the parent_id.name used in the drop down list differ from the view? Maybe I’m getting a conflict? As always thanks for terrific screencasts.

    different stylesheet

    From: Erwin Odendaal, 07/05/08 11:33 PM

    Easier than expected… Added: <% if @page.parent %> <%= stylesheet_link_tag ‘sub’ %> <% end %> to the head-section of application.html.erb

    different stylesheets

    From: Erwin Odendaal, 07/02/08 01:51 AM

    I want to link another stylesheet to the subpages. So when a page has a parent it needs to call the style sub.css (for example). But I just can’t figure it out. :-( I presume you need to check if there’s no parent_id?

    collection select syntax

    From: Michael Slater, 06/09/08 02:18 AM

    Alexei, yes, it’s a small bit of rails magic. Whenever you use a form_for block, you get the form context from the block variable, f. Since we wrote f.collection_select, we don’t specify the page object; it is implicitly specified through the block variable.

    collection_select

    From: Alexei, 06/08/08 06:52 PM

    I looked down at different wikis and official API for rails and seems like the syntax for collection_select is collection_select(:page, :parent_id, Page.find(:all), :id, :title, :include_blank => ‘true’) Does your variant (without first :page) uses some rails magic?

    Menus

    From: Erik, 05/29/08 06:25 AM

    Loved the lesson as always. One other thing you could add as a ‘to-do’ is to somehow generate drop-down menus for the subpages. This is a frequently used feature in many sites and would be a helpful addition to a rails site as well. I would imagine using javascript to invoke a mouseover event that would display a bit of html that would show the buttons below the parent tabs.

    Lesson feedback

    From: Tim Morgan, 05/21/08 07:02 AM

    Hi guys

    Thanks again for another great installment.

    My comments.

    ===

    Last time we did a migration, we were careful to initialize the new fields. Accordingly, I added the following to my ‘up’ method.

        Page.find(:all).each do |page|
          page.update_attributes :position => page.id, :navlabel => page.title
        end
    

    ===

    It would seem that self-refering classes would be a pattern. Is there a macro that would generate the following…

      has_many :subpages, :class_name => self.name, :foreign_key => 'parent_id'
      belongs_to :parent, :class_name => self.name, :foreign_key => 'parent_id'
    
    from, say
      self_reference :subpages
    

    ===

    I refactored ‘get_pages_for_tabs’ / ‘find_main’ as follows:

    //application.rb
      def get_pages_for_tabs
        @tabs = Page.find_main logged_in?
      end
    // page.rb
      def self.find_main( logged_in )
        if logged_in
          Page.find(:all, 
            :conditions => ['parent_id IS NULL'], :order => 'position')
        else
          Page.find(:all, 
            :conditions => ["parent_id IS NULL and admin != ?", true], :order => 'position')
        end
      end
    

    ===

    Navbar is growing into an application level helper. I also refactored the ‘edit’ button into the navabar, so I can find it in a consistant location. Remove it from viewer/show.html.erb altogether and

    // application.html.erb
    <%- if logged_in? -%>
                        <li><a href='#' id='edit'>[Edit]</a></li>
                        <li><%= link_to "Log Out", logout_path %></li>
    <%- else -%>
                        <li><%= link_to "Log In", login_path %></li>
    <%- end -%>
    

    But I still get the tooltip ‘edit’ popping up from the in_place_editing plugin. Is there an option to shut this off inside the Javascript?

    ===

    Here’s a question for restful development. In following the development of the database, we are filling it with some testing records. I was wondering: Is it possible to get a data dump from the database, to fill a SQL script to re-initialize? Then I remembered restfull. Using the url ‘pages.xml’ or ‘users.xml’, we get an xml dump of the data. ‘pages.json’ will send formated as json

    Is there a simple SQL formatter for this?

    ===

    Again, really enjoying the series! Great work, guys

    HTML error corrected

    From: Michael Slater, 05/20/08 01:07 AM

    Scott, thanks for pointing out the error in the HTML layout. I’ve corrected this in the zip file. As for the Akita technique, this works but it isn’t clear to me that it is much of an improvement.

    Correction: Another version of the _form partial

    From: Scott Gardner, 05/19/08 06:27 AM

    The button name local variable is a hash, so the = should be => new.html.erb :locals => {:button_name => “Create”} edit.html.erb :locals => {:button_name => “Update”}

    Another version of the _form partial

    From: Scott Gardner, 05/19/08 05:56 AM

    I got the idea from the Akita On Rails tutorial to move the entire form to a partial, and then assign the button name to a local variable, e.g.:

    _form.html.erb <% form_for(@page) do |f| %>
    1. ... <% end %>

    new.html.erb <%= render :partial => ‘form’, :locals => {:button_name = “Create”} %>

    edit.html.erb <%= render :partial => ‘form’, :locals => {:button_name = “Update”} %>

    Mismatched divs in source code download > application.html.erb

    From: Scott Gardner, 05/19/08 05:19 AM

    You are missing a closing div in this file. Starting from the footer id: # ...

     

    Sponsored By

    New Relic Rails Performance Monitoring