Index of All Lessons

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.


Want to help spread the word? We’d be grateful if you would include a link to the course in your blog, web site, or emails.

Resources Page: Links, Categories, and HABTM

comments

Goals

In this lesson, we’re creating the database-driven Resources page, with links shown by category. Along the way, we look at join tables and HABTM associations.

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 16. These zip files contain the beginning and ending states of the code:

Adding the Links and Categories Models

For each link, we want to have a title and a description, and of course we need the URL. Let’s use the scaffold generator to create the model and the admin page:

script/generate scaffold link url:string title:string description:text

The “text” field type can hold longer strings that the “string” type, which is why we used it for the description.

We also need a model for the categories, so let’s run another scaffold command for that simple model, which requires only a title and a description for each category:

script/generate scaffold category title:string description:text

Creating the Join Table

When you have an association where one model has a belongs_to declaration, you know that model must have a field to store the foreign key that is what creates the association. In the case of Categories and Links, however, a category can have many links, and a link can belong to many categories, so what we need here is a has_and_belongs_to_many association.

In this type of association, neither of the associated tables stores any foreign keys; instead, a separate join table stores just the pairs of foreign keys that define each association (i.e., associate one link with one category).

We need to explicitly create this join table. There is no model associated with this table; it is just a database table that is use automatically along with the two associated models.

Let’s generate an empty migration file with the migration generator:

script/generate migration LinkCategoryJoin

Note that the name is entirely arbitrary; we just want something that reminds us of what this migration is for.

Now we define the migration by writing the self.up method:

def self.up
  create_table :categories_links, :id => false do |t|
    t.integer :category_id
    t.integer :link_id
  end
end

This creates a table with two columns, each of which is one of the foreign keys. This table doesn’t get an id column of its own, so we add the option :id => false to the create_table method call.

There’s several Rails naming defaults that come into play here, and you need to know what they are to write this code correctly:

  1. Join tables are always named with the names of the two associated tables, in alphabetical order, separated by an underscore. That’s why the table is called categories_links, and not links_categories (which won’t work because of this default).
  2. The foreign key fields are named with the name of the table they are referencing, with _id appended.
  3. The foreign key is referencing a single element in that table, so it uses the singular name (e.g., category_id, not categories_id).

To keep our migrations reversible, we’ll add the down method:

def self.down
  drop_table :categories_links
end

Migrate!

We’ve now created three new migrations, one for categories, one for links, and one for the join table. A single command runs them all:

rake db:migrate

Model Associations

We’ve defined the foreign keys when creating our models, but we still need to specify the associations for the model classes. In models/link.rb, we need to add to the empty Link class:

has_and_belongs_to_many :categories

This statement creates the association “link has and belongs to many categories.” Its presence allows us to write elsewhere in our code link.categories to retrieve the list of categories that has been assigned to this link. The join table that represents these assignments is managed for us automatically by the Rails framework.

While we’re modifying the Link class, let’s require that at least a title be entered; there’s not much use to having a record without one:

validates_presence_of :title

Now we need to make a corresponding set of additions to the Category model. We’ll add to that empty class:

has_and_belongs_to_many :links
validates_presence_of :title

This HABTM declaration allows us to write elsewhere in our code category.links, to find all the links associated with a category.

Finishing up the Admin Interface

You can now browse to localhost:3000/links to see the link admin page, but it still needs a bit of work.

The Rails scaffold generator generates an empty layout file for every scaffold, assuming for some reason that we probably want a unique layout for each one. We don’t — we want to use the standard application layout. So we need to delete the extra files that the scaffold dumped into views/layouts.

To protect the admin pages from public users, add to links_controller and categories_controller:

before_filter :login_required

For ease of access, let’s add links to our new admin pages to the admin home page. Log into the site, click the Admin button, and then click the Page Admin link. Click the Edit link for the Admin page, and add the following to the links that make up the page body:

"Category Admin":/categories

"Link Admin":/links

(You can also use the Edit link on the Admin page, which triggers the in-place editor we created a couple lessons ago.)

Setting Categories

Now you can use the newly-created Category Admin link on the admin home page to create some categories; the scaffolded interface does everything we need. Go ahead and make a few.

You can also add a link, using Link Admin, but there’s no place on the scaffolded link admin pages to specify the category. That’s because when we created the scaffold, the association didn’t exist. To use the standard Rails scaffolding, you need to manually modify the scaffold-generated view files to display or modify fields that come from associations.

To create a category selector control on the new link form, we use the following slightly messy bit of code:

<p>
	<b>Category</b><br />
  <%= f.collection_select :category_ids, Category.find(:all, :order => 'title'), :id, :title, {}, :multiple => true %>
</p>

The collection_select method creates an HTML form element that allows the user to choose from a list of items. The parameters passed are:

  • :category_ids — the attribute this element is setting
  • Category.find(:all, :order => 'title') — an array of objects that creates the list of choices
  • :id — the value field of the objects in the list array
  • :title — the name field of the objects in the list array
  • {} — placeholder for an options hash that we’re not using
  • :multiple => true — option to allow user to select multiple items

With this code inserted in views/links/new.html.erb, you should now have a list of categories from which to choose (provided that you have added some categories to the database already).

The same bit of code for the category selector needs to be added to the edit view as well. Even better, you could pull out the form guts into a partial, and invoke the same partial from both the new and edit view, as we did in a previous lesson.

Now you can create some links and assign them to categories.

The Resources Page

For the Resources page, we want to show a list of all the links, sorted by category. The index action of the links controller is already use as part of the admin interface, so we need another action. We’ll add this list action to links_controller.rb:

def list
  @categories = Category.find(:all, :order => 'title')
end

This action is simply providing the view with a list of categories.

We also need to change the before_filter we set for login, so this action won’t require login. Change the line at the top of the controller to:

before_filter :login_required, :except => [:list]

Now, we need to create a new file, views/links/list.html.erb, to respond to this action; here’s the basic code:

<h1>Resources</h1>
<% for category in @categories %>	
	<h2><%=h category.title %></h2>
	<p><%=h category.description %></p>
	<ul>
	  <% for link in category.links %>
		  <li><%= link_to link.title, link.url %></li>
	  <% end %>
	</ul>
<% end %>

Finally, since we’ve added another method to a RESTful controller, we need to declare that method in the route. In config/routes.rb, there is already a set of standard routes declared:

map.resources :links

To add the route, we modify this as follows:

map.resources :links, :collection => {:list => :get}

This specifies that the additional route operates on the entire collection of objects (not on a specific object), that its name is :list, and it responds to HTTP GET requests.

Our list now works if you access it at localhost:3000/links/list.

One small issue: if there’s an empty category (a category with no links) the category heading is still displayed. To make that go away, add a conditional around the loop in the list view:

	<% for category in @categories %>	
		<% unless category.links.empty? %>
			<h2><%=h category.title %></h2>	

Redirecting Navigation

Now we have our Resources page being created from the database, and admin pages to add categories and links. Our last task is to connect it up the the navigation buttons.

So far, our Resources page is a text page in our CMS. We’d like the Resources button to go to our list of links (/links/list), but the CMS doesn’t currently give us a way to do that. We want to keep using the page model to control navigation buttons, but allow page content to come from another controller, rather than from the page viewer.

There’s many different ways to tackle this problem. The one we’ve chosen here, as the simplest to implement, is to add attributes to the Page model so any page can be specified as a “redirect” page, which should not be rendered by the viewer but instead by another controller and action.

Let’s create a migration to add these elements to the model:

script/generate migration PageRedirect

The up method adds three attributes:

def self.up
  add_column :pages, :redirect, :boolean
  add_column :pages, :action_name, :string
  add_column :pages, :controller_name, :string
end

And what up giveth, down taketh away:

def self.down
  remove_column :pages, :redirect
  remove_column :pages, :action_name
  remove_column :pages, :controller_name
end

Now make the changes to the database:

rake db:migrate

Updating the Page form

And now we just need to add the new fields to views/pages/_form.html.erb:

<p>
  <b>Redirect?</b><br />
  <%= f.check_box :redirect %>
</p>

<p>
  <b>Action</b><br />
  <%= f.text_field :action_name %>
</p>

<p>
  <b>Controller</b><br />
  <%= f.text_field :controller_name %>
</p>

Acting Upon Redirect Pages

Now that we have this redirect information in the page model, we need to use it!

We replace the link_to that generates the nav buttons, in the application layout, to:

<% 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 %>

We pass the page name as a parameter so the action to which we’ve redirected can load the appropriate page object to get the page title and control the tab highlighting. So in the list action in the links controller, we add:

	@page = Page.find_by_name(params[:name])
	@pagetitle = @page.title

Now we need to edit the Resources page’s entry in the CMS to set it to redirect to the list action and the links controller, and voila! Our Resources button now takes us to the database-generated page.


Add a Comment

Have a comment or question about this lesson? Add it here.






Comments on This Lesson

From: jose Ignacio       Date: 09/21/08 10:22 PM

Subject: bit of code does so much...

On the redirect: how was the resources (nav-bar) highlight solved?

I understood pagetitle =page.title in the link_controller, but how can this also solve the highlighting?

-anyone?

From: Walter       Date: 06/28/08 11:23 PM

Subject: Ignore my post/last post

Ingore my last post. I can’t see why I’d have any problems, the steps are so clear, so there must be a setup issue on my end. I’ll try a different approach. Thanks.

From: Walter       Date: 06/27/08 11:11 AM

Subject: Trying to repeat this for an events/activities

This series is great and I see opportunities to stretch my examples. One of which is mentioned here that I’m having a problem with :: http://railsforum.com/viewtopic.php?id=19676

Basically, I’m just doing the same example with events and activities. The join table is not recording the activity_ids selection from the collection_select in exactly the same way as this links/categories category_ids example.

is there a way to test the join? Perhaps I missed a step? see the forums link for code.

Again, much appreciation for any pointers
cheer
walter

From: Michael Slater       Date: 06/15/08 02:14 PM

Subject: HABTM versus has_many :through

Claude, has_many :through is the way to go if there is any additional information you want to include in the association. But for simple relations such as this one, I think HABTM is just fine.

From: Claude Rousseau       Date: 06/15/08 05:05 AM

Subject: HABTM versus has_many :through

Hello!

Again thank you so much for your excellent work.
Since I’m still trying to grasp this whole Rails universe (that’s how big a challenge I see it for now :-)) , I’ve also downloaded Ryan Bates Everyday Active record screencasts, in which I understood that it would probably be much simpler for me to always use has_many :through association, Ryan says that HABTM is somewhat deprecated in favor of has_many :through, and that the latter offers possibility to add related information in the join table that does not fit into the original tables.

Since I value very much your opinion, I would like to read your views on the subject. Cheers, and keep up the good work! Best regards, Claude. ps:(English might be slightly weird, for my native language is French)

From: Michael Slater       Date: 06/10/08 11:23 PM

Subject: download problem

Gareth, it’s working for me now. It’s possible that either Amazon was having some issues. Let me know if it isn’t working now.

From: Gareth Maddock       Date: 06/10/08 05:17 PM

Subject: Download problems

Have been only able to download 20% of this (lesson 17) screencast. Download fails after 36mb (of 160mb) consistently. Have been able to download other screencasts successfully. Has the file been damaged?

From: Tim Morgan       Date: 06/06/08 08:20 PM

Subject: routes

Wouldn’t it be simpler to redirect from the routes file, as in

map.connect '/resources', :controller =&gt; 'links', :action =&gt; 'list'

where resources is the ‘title’ of the page record?

From: Ray Rogers       Date: 06/05/08 06:06 AM

Subject: Another Great Screen cast

For a beginner this is a great series. Please keep up the great work.

 

Sponsored By

New Relic Rails Performance Monitoring

Peepcode Screencasts