Resources Page: Links, Categories, and HABTM
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:
- Learning Rails example app code as of the start of this lesson
- Learning Rails example app code as of the end of this lesson
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:
- 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 notlinks_categories(which won’t work because of this default). - The foreign key fields are named with the name of the table they are referencing, with
_idappended. - The foreign key is referencing a single element in that table, so it uses the singular name (e.g.,
category_id, notcategories_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 settingCategory.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.
Comments on This Lesson
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 => 'links', :action => '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.


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?