I've got several models that I'd like to relate together hierarchically. For simplicity's sake, let's say I've got these three:
class Group < ActiveRecord::Base
acts_as_tree
has_many :users
end
class User < ActiveRecord::Base
acts_as_tree
belongs_to :group
has_many :posts
end
class Post < ActiveRecord::Base
acts_as_tree
belongs_to :user
end
Under the current acts_as_tree, each node can individually can relate hierarchically to other nodes provided they are of the same type. What I'd like is to remove this restriction on type identity, so that SomePost.parent could have a User or a Post as its' parent, and that SomeUser.parent could have another user or a group as its parent.
Any thoughts?
The way I have done this in the past is by using a polymorphic container that lives in the tree, mapping to specific individual models.
class Container < ActiveRecord::Base
acts_as_tree
belongs_to :containable, :polymorphic => true
end
class User
has_one :container :as => :containable
end
I managed to do it a little differently, but this might not work for your situation. I was refactoring existing code, and didn't want to do any serious database migrating.
I wanted separate leaf and node classes. Both inherit from a tree class.
I added two functions in ClassMethods in vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb
:
# Configuration options are:
#
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
# * <tt>leaf_class_name</tt> - leaf class subtype of base tree class
# * <tt>node_class_name</tt> - node class subtype of base tree class
def acts_as_tree_node(options = {})
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil, :node_class_name => 'Node', :leaf_class_name => 'Leaf' }
configuration.update(options) if options.is_a?(Hash)
belongs_to :parent, :class_name => configuration[:node_class_name], :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
#has_many :children, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
class_eval <<-EOV
has_many :child_nodes, :class_name => '#{configuration[:node_class_name]}', :foreign_key => "#{configuration[:foreign_key]}", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}, :dependent => :destroy
has_many :child_leaves, :class_name => '#{configuration[:leaf_class_name]}', :foreign_key => "#{configuration[:foreign_key]}", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}, :dependent => :destroy
include ActiveRecord::Acts::Tree::InstanceMethods
def self.roots
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
end
def self.root
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
end
EOV
end
# Configuration options are:
#
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
# * <tt>node_class_name</tt> - the class name of the node (subclass of the tree base)
def acts_as_tree_leaf(options = {})
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil, :node_class_name => 'Node' }
configuration.update(options) if options.is_a?(Hash)
belongs_to :parent, :class_name => configuration[:node_class_name], :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
class_eval <<-EOV
include ActiveRecord::Acts::Tree::InstanceMethods
def self.roots
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
end
def self.root
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
end
EOV
end
Then, in InstanceMethods, I just added one function:
# Returns list of children, whether nodes or leaves.
#
# NOTE: Will not return both, because that would take two queries and
# order will not be preserved.
def children
child_leaves.count == 0 ? child_nodes : child_leaves
end
That's a bit of a hack, but it works for me since every node has all one type of subs. You can play around with the children
function to get different behavior, such as follows:
def children
child_nodes | child_leaves
end
But it still takes an extra query, and you lose your order and scopes and stuff.
Finally, in my Node class, I have
acts_as_tree_node :node_class_name => 'NodeMatrix', :leaf_class_name => 'LeafMatrix'
and in my Leaf class, the following:
acts_as_tree_leaf :node_class_name => 'NodeMatrix'
Both of these inherit from TreeMatrix, which is pure virtual (nothing is actually instantiated as TreeMatrix, it's just a base class).
Again, this is very application-specific. But it gives you an idea how you can modify acts_as_tree.