Displaying a subset of items from an association
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
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.
cd blog
script/generate scaffold_resource user name:string
script/generate scaffold_resource tag name:string
script/generate migration create_tags_users
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.
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).