by dylan
So, I have been using Evan Weaver’s awesome has_many_polymorphs rails plugin for a while now. It has saved me a lot of frustration, for sure. Evan is also working up a tagging generator for has_many_polymorphs which should help solidify has_many_polymorphs position as the replacement for acts_as_taggable (which is basically deprecated at this point, I think). The plugin is actually much more than just a tagging engine tho. You can have a look at the plugin’s own page for a full (and current) run down of what has_many_polymorphs can do.
For a couple recent projects, I have used has_many_polymorphs to set up has_many :through relationships and it’s actually been such a boon that I reckon it may benefit other folks for me to commit the process to writing.
I am assuming that you have already installed the plugin in your rails app as per Evan’s instructions—it should be just
script/plugin install svn://rubyforge.org/var/svn/polymorphs/has_many_polymorphs
but I am not responsible if the the svn location has changed.
Anyhow, for the purposes of the example, we are setting up an app with a Gallery and Page models that may each have many Photos through an Attachment model. We are also going to set it up so that you can add the joins in the CRUD views for each of the models themselves (in the same params with the rest other model data). Additionally, the joins themselves will be set up for acts_as_list so that if you are creating a gallery, for example, with many photos, you can sort the photos in the gallery (via the join)—I think that’s clear, but it may not be. [Evan actually helped clean up the abstract_id_assignment method below so additional props for that.]
First, the code:
class Photo < ActiveRecord::Base
has_many_polymorphs :attachables,
:from => [:galleries, :pages],
:through => :attachments,
:parent_order => 'attachments.position',
:dependent => :destroy
def abstract_id_assignment(klass_name, ids)
self.save!
ids.map!(&:to_i)
objs = self.send(klass_name.pluralize)
objs.delete objs.reject{|obj| ids.include? obj.id}
objs << klass_name.classify.constantize.find(ids - objs.map(&:id))
end
# let's curry some methods
def gallery_ids= ids
abstract_id_assignment("gallery", ids)
end
def page_ids= ids
abstract_id_assignment("page", ids)
end
end
class Attachment < ActiveRecord::Base
belongs_to :photo
belongs_to :attachable,
:polymorphic => true
# http://lists.rubyonrails.org/pipermail/rails/2006-May/042362.html
acts_as_list :scope => 'attachable_id=#{attachable_id} and attachable_type=#{quote_value attachable_type}'
end
The attachment migration would look something like this.
class CreateAttachments < ActiveRecord::Migration
def self.up
create_table :attachments do |t|
t.column :photo_id, :integer, :null => false
t.column :attachable_id, :integer, :null => false
t.column :attachable_type, :string, :null => false
t.column :position, :integer
end
add_index :attachments, [:photo_id, :attachable_id, :attachable_type], :unique => true, :name => 'index_attachment_polymorphs'
end
def self.down
drop_table :attachments
end
end
class Page < ActiveRecord::Base end
class Gallery < ActiveRecord::Base end
Okay so now you will probably need to add something like the following to the photo controller.
def edit
@photo = Photo.find(params[:id])
@galleries = Gallery.find(:all)
@pages = Page.find(:all)
end
And for the photo/edit action form you can now do something like this.
<ul>Galleries
<% @galleries.each do |gallery| %>
<li><%= check_box_tag "photo[gallery_ids][]", gallery.id, @photo.galleries.include?(gallery) %> <%= gallery.name %></li>
<% end %>
</ul>
<ul>Pages
<% @pages.each do |page| %>
<li><%= check_box_tag "photo[page_ids][]", page.id, @photo.pages.include?(page) %> <%= page.name %></li>
<% end %>
</ul>
Oh and if you want the ability to create the joins on views for the page and gallery models, you can do something like the following.
# create a file called lib/activerecord_base_photo_extensions.rb
# this gets required in environment.rb
class ActiveRecord::Base
def photo_ids= photo_ids
photo_ids.map!(&:to_i)
photos.delete photos.reject{|photo| photo_ids.include? photo.id}
photos << Photo.find(photo_ids - photos.map(&:id))
end
end
# in environment.rb require 'activerecord_base_photo_extensions'
And I reckon you can now include something like this on the page/edit and gallery/edit forms (though this would probably end up being kind of a ui disaster).
<ul>Photos
<% @photos.each do |photo| %>
<li><%= check_box_tag "page[photo_ids][]", photo.id, @page.photos.include?(photo) %> <%= photo.name %></li>
<% end %>
</ul>
This is just a very simple example of what is possible with has_many_polymorphs, but the plugin has really saved me a lot of time and headache, so I hope this ends up helping somebody out there.



