views:

462

answers:

2

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?

+1  A: 

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
Toby Hede
Perfect! This worked great for me. Thanks...
Joe
A: 

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.

mohawkjohn