Displaying a subset of items from an association

Posted by blackrat on December 16, 2006

As of this post I have two classes joined by have_and_belongs_to_many (habtm), and I figured that I’d like to be able to click on an entry in one and have a subset index of the other appear. Not an uncommon task and used by tags and blogs everywhere. Rather than search for a snippet, I thought I’d give it a go myself first.What I came up with surprised me with it’s simplicity and may not be the most expedient manner, but seems to be a neat and easy extension of the index method.

The obvious way was to add an entry to the list of actions which can be applied to a tag item. A simple addition of a “Show Users” link pointing to the Users controller and passing a tag_id parameter was the starting point for this.

Opening up app/views/tags/index.rhtml, we can add a user_path with :tag_id=>tag as the parameter. This will translate to http://localhost:3000/users?tag_id=x.


<h1>Listing tags</h1>

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

<% for tag in @tags %>
  <tr>
    <td><%=h tag.name %></td>
    <td><%= link_to 'Users', user_path(:tag_id=>tag) %></td>
    <td><%= link_to 'Show', tag_path(tag) %></td>
    <td><%= link_to 'Edit', edit_tag_path(tag) %></td>
    <td><%= link_to 'Destroy', tag_path(tag), :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
  <% end %>
</table>
<br />
<%= link_to 'New tag', new_tag_path %>

This will then allow the following in the index view of the users controller.

  # GET /users
  # GET /users.xml
  def index
    @tag=Tag.find(params[:tag_id]) if params[:tag_id]
    @users = @tag.users || throw rescue User.find(:all)

    respond_to do |format|
      format.html # index.rhtml
      format.xml  { render : xml => @users.to_xml }
    end
  end

Note the unusual throw rescue construct. Since @tag.users would throw an exception, as well as having a nil possibility, this is the dryest form of getting the original User.find(:all) executed on error. I mainly wanted to see if this would work, and it does! I’m really starting to like what you can do with Ruby.

RESTful Rails with Associations explored

Posted by blackrat on December 16, 2006

I’ve recently switched to EdgeRails for Rails 1.2 RC1, and decided to try out the resource_scaffold to create RESTful classes. There’s not that much documentation on this so far, so I thought I’d combine using these with creating habtm associations. What follows is a walkthrough of the initial creation of a has_and_belongs_to_many association and the changes required to get it to do what I want within the scaffold_resource RESTful framework.

First of all, some MVC creation. I like the fact that tests are now generated by the scaffold_resource, but be warned that if you modify your migrations, you also need to change the test/fixtures/*.yml files to reflect the changes. Otherwise, your unit tests will start to error. We’re going to be using the extension to scaffold_resource and generate a string “name” for both tables, which will also generate the fixture unit test for that column.

rails –database=sqlite3 blog
cd blog
script/generate scaffold_resource user name:string
script/generate scaffold_resource tag name:string
script/generate migration create_tags_users
Change db/migration/003_create_tags_users.rb to the following:
class CreateTagsUsers < ActiveRecord::Migration
    def self.up
        create_table :tags_users, :id=>false do |t|
            t.column :tag_id, :integer
            t.column :user_id, :integer
        end
    end
    def self.down
        drop_table :tags_users
    end
end

Don’t forget the :id=>false (unless you know what you are doing, that is).
Next, modify the models to point to each other. app/models/tag.rb and app/models/user.rb become:

class Tag < ActiveRecord::Base
    has_and_belongs_to_many :users
end

and

class User < ActiveRecord::Base
    has_and_belongs_to_many :tags
end

At this point, I like to run the migrate and the tests which were created by the scaffold_resource.

rake db:migrate test
Now it’s time for a little association plumbing. I like to have a view to allow me to select the tags from the full list (we’ll ignore adding more from this view for this walkthrough) when in the user display, so the first port of call is the users_controller. I’d also like to be able to see from the index display which ones are selected, so I’ll be modifying the new, edit, create and update methods and all of the views for user.

Ok. For app/views/users the following become the new views

index.rhtml

<h1>Listing users</h1>
<table>
    <tr>
          <th>Name</th>
          <th>Tags</th>
    </tr>
    <% for user in @users %>
          <tr>
               <td><%=h user.name %></td>
               <td>
                    <% for tag in user.tags %>
                         <%= tag.name %><br />
                    <% end %>
               </td>
               <td><%= link_to 'Show', user_path(user) %></td>
               <td><%= link_to 'Edit', edit_user_path(user) %></td>
               <td><%= link_to 'Destroy', user_path(user), :confirm => 'Are you sure?', :method => :delete %></td>
        </tr>
    <% end %>
</table>
<br />
<%= link_to 'New user', new_user_path %>

new.rhtml

<h1>New user</h1>
<%= error_messages_for :user %>
<% form_for(:user, :url => users_path) do |f| %>
    <p>
          <b>Name</b><br />
          <%= f.text_field :name %>
     </p>
    <p>
        <b>Tags</b><br />
          <% for tag in @tags %>
               <br />
            <input type="checkbox"
                            id="<%=tag.id%>"
              name="tag_ids[]"
                            value="<%=tag.id%>"
                        >
            <%=tag.name%>
        <% end %>
    </p>
     <p>
          <%= submit_tag "Create" %>
     </p>
<% end %>
<%= link_to 'Back', users_path %>

show.rhtml

<p>
    <b>Name:</b>
     <%=h @user.name %>
</p>
<p>
    <b>Tags:</b>
     <% for tag in @user.tags %>
          <%= tag.name %><br />
     <% end %>
</p>
<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

and finally, edit.rhtml

<h1>Editing user</h1>
<%= error_messages_for :user %>
<% form_for(:user, :url => user_path(@user), :html => { :method => :put }) do |f| %>
    <p>
          <b>Name</b><br />
          <%= f.text_field :name %>
     </p>
     <p>
          <b>Tags</b><br />
          <% for tag in @tags %>
            <br />
               <input type="checkbox"
                   id="<%=tag.id%>"
                   name="tag_ids[]"
                   value="<%=tag.id%>"
                   <%if @user.tags.include? tag%>checked="checked"<%end%>
               >
               <%=tag.name%>
          <% end %>
     </p>
     <p>
          <%= submit_tag "Update" %>
     </p>
<% end %>
<%= link_to 'Show', user_path(@user) %> |
<%= link_to 'Back', users_path %>

So not that much different to the CRUD scaffolding. I’m going to experiment more with this, and also with the through :class approach of handling associations, which I believe looks to have some strong advantages (not the least of which being that it gives you a join class rather than a join table).