require 'acts_as_tree/version' module ActsAsTree if defined? Rails::Railtie require 'acts_as_tree/railtie' elsif defined? Rails::Initializer raise "acts_as_tree 1.0 is not compatible with Rails 2.3 or older" end def self.included(base) base.extend(ClassMethods) end # Specify this +acts_as+ extension if you want to model a tree structure # by providing a parent association and a children association. This # requires that you have a foreign key column, which by default is called # +parent_id+. # # class Category < ActiveRecord::Base # include ActsAsTree # # acts_as_tree :order => "name" # end # # Example: # root # \_ child1 # \_ subchild1 # \_ subchild2 # # root = Category.create("name" => "root") # child1 = root.children.create("name" => "child1") # subchild1 = child1.children.create("name" => "subchild1") # # root.parent # => nil # child1.parent # => root # root.children # => [child1] # root.children.first.children.first # => subchild1 # # In addition to the parent and children associations, the following # instance methods are added to the class after calling # acts_as_tree: # * siblings - Returns all the children of the parent, excluding # the current node ([subchild2] when called # on subchild1) # * self_and_siblings - Returns all the children of the parent, # including the current node ([subchild1, subchild2] # when called on subchild1) # * ancestors - Returns all the ancestors of the current node # ([child1, root] when called on subchild2) # * root - Returns the root of the current node (root # when called on subchild2) module ClassMethods # Configuration options are: # # * primary_key - specifies the column name for relations # (default: +id+) # * foreign_key - specifies the column name to use for tracking # of the tree (default: +parent_id+) # * order - makes it possible to sort the children according to # this SQL snippet. # * counter_cache - keeps a count in a +children_count+ column # if set to +true+ (default: +false+). Specify # a custom column by passing a symbol or string. def acts_as_tree(options = {}) configuration = { primary_key: "id", foreign_key: "parent_id", order: nil, counter_cache: nil, dependent: :destroy, touch: false } configuration.update(options) if options.is_a?(Hash) if configuration[:counter_cache] == true configuration[:counter_cache] = :children_count end if ActiveRecord::VERSION::MAJOR >= 5 belongs_to :parent, class_name: name, primary_key: configuration[:primary_key], foreign_key: configuration[:foreign_key], counter_cache: configuration[:counter_cache], touch: configuration[:touch], inverse_of: :children, optional: true else belongs_to :parent, class_name: name, primary_key: configuration[:primary_key], foreign_key: configuration[:foreign_key], counter_cache: configuration[:counter_cache], touch: configuration[:touch], inverse_of: :children end if ActiveRecord::VERSION::MAJOR >= 4 has_many :children, lambda { order configuration[:order] }, class_name: name, primary_key: configuration[:primary_key], foreign_key: configuration[:foreign_key], dependent: configuration[:dependent], inverse_of: :parent else has_many :children, class_name: name, primary_key: configuration[:primary_key], foreign_key: configuration[:foreign_key], order: configuration[:order], dependent: configuration[:dependent], inverse_of: :parent end include ActsAsTree::InstanceMethods define_singleton_method :default_tree_order do order(configuration[:order]) end define_singleton_method :root do self.roots.first end define_singleton_method :roots do where(configuration[:foreign_key] => nil).default_tree_order end # Returns a hash of all nodes grouped by their level in the tree structure. # # Class.generations # => { 0=> [root1, root2], 1=> [root1child1, root1child2, root2child1, root2child2] } def self.generations all.group_by{ |node| node.tree_level } end if configuration[:counter_cache] after_update :update_parents_counter_cache def children_counter_cache_column reflect_on_association(:parent).counter_cache_column end def leaves where(children_counter_cache_column => 0).default_tree_order end else # Fallback to less efficient ways to find leaves. class_eval <<-EOV def self.leaves internal_ids = select(:#{configuration[:foreign_key]}).where(arel_table[:#{configuration[:foreign_key]}].not_eq(nil)) where("\#{connection.quote_column_name('#{configuration[:primary_key]}')} NOT IN (\#{internal_ids.to_sql})").default_tree_order end EOV end end end module TreeView # show records in a tree view # Example: # root # |_ child1 # | |_ subchild1 # | |_ subchild2 # |_ child2 # |_ subchild3 # |_ subchild4 # def tree_view(label_method = :to_s, node = nil, level = -1) if node.nil? puts "root" nodes = roots else label = "|_ #{node.send(label_method)}" if level == 0 puts " #{label}" else puts " |#{" "*level}#{label}" end nodes = node.children end nodes.each do |child| tree_view(label_method, child, level+1) end end end module TreeWalker # Traverse the tree and call a block with the current node and current # depth-level. # # options: # algorithm: # :dfs for depth-first search (default) # :bfs for breadth-first search # where: AR where statement to filter certain nodes # # The given block sets two parameters: # first: The current node # second: The current depth-level within the tree # # Example of acts_as_tree for model Page (ERB view): # <% Page.walk_tree do |page, level| %> # <%= link_to "#{' '*level}#{page.name}", page_path(page) %>
# <% end %> # # There is also a walk_tree instance method that starts walking from # the node it is called on. # def walk_tree(options = {}, &block) algorithm = options.fetch :algorithm, :dfs where = options.fetch :where, {} send("walk_tree_#{algorithm}", where, &block) end def self.extended(mod) mod.class_eval do def walk_tree(options = {}, &block) algorithm = options.fetch :algorithm, :dfs where = options.fetch :where, {} self.class.send("walk_tree_#{algorithm}", where, self, &block) end end end private def walk_tree_bfs(where = {}, node = nil, level = -1, &block) nodes = (node.nil? ? roots : node.children).where(where) nodes.each { |child| yield(child, level + 1) } nodes.each { |child| walk_tree_bfs where, child, level + 1, &block } end def walk_tree_dfs(where = {}, node = nil, level = -1, &block) yield(node, level) unless level == -1 nodes = (node.nil? ? roots : node.children).where(where) nodes.each { |child| walk_tree_dfs where, child, level + 1, &block } end end module InstanceMethods # Returns list of ancestors, starting from parent until root. # # subchild1.ancestors # => [child1, root] def ancestors node, nodes = self, [] nodes << node = node.parent while node.parent nodes end # Returns list of descendants, starting from current node, not including current node. # # root.descendants # => [child1, child2, subchild1, subchild2, subchild3, subchild4] def descendants children.each_with_object(children.to_a) {|child, arr| arr.concat child.descendants }.uniq end # Returns list of descendants, starting from current node, including current node. # # root.self_and_descendants # => [root, child1, child2, subchild1, subchild2, subchild3, subchild4] def self_and_descendants [self] + descendants end # Returns the root node of the tree. def root node = self node = node.parent while node.parent node end # Returns all siblings of the current node. # # subchild1.siblings # => [subchild2] def siblings self_and_siblings - [self] end # Returns all siblings and a reference to the current node. # # subchild1.self_and_siblings # => [subchild1, subchild2] def self_and_siblings parent ? parent.children : self.class.roots end # Returns all the nodes at the same level in the tree as the current node. # # root1child1.generation # => [root1child2, root2child1, root2child2] def generation self_and_generation - [self] end # Returns a reference to the current node and all the nodes at the same level as it in the tree. # # root1child1.self_and_generation # => [root1child1, root1child2, root2child1, root2child2] def self_and_generation self.class.select {|node| node.tree_level == self.tree_level } end # Returns the level (depth) of the current node # # root1child1.tree_level # => 1 def tree_level self.ancestors.size end # Returns the level (depth) of the current node unless level is a column on the node. # Allows backwards compatibility with older versions of the gem. # Allows integration with apps using level as a column name. # # root1child1.level # => 1 def level if self.class.column_names.include?('level') super else tree_level end end # Returns children (without subchildren) and current node itself. # # root.self_and_children # => [root, child1] def self_and_children [self] + self.children end # Returns ancestors and current node itself. # # subchild1.self_and_ancestors # => [subchild1, child1, root] def self_and_ancestors [self] + self.ancestors end # Returns true if node has no parent, false otherwise # # subchild1.root? # => false # root.root? # => true def root? parent.nil? end # Returns true if node has no children, false otherwise # # subchild1.leaf? # => true # child1.leaf? # => false def leaf? children.size.zero? end private if ActiveRecord::VERSION::MAJOR > 5 || ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 2 def update_parents_counter_cache end elsif ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR == 1 def update_parents_counter_cache counter_cache_column = self.class.children_counter_cache_column if saved_change_to_parent_id? self.class.decrement_counter(counter_cache_column, parent_id_before_last_save) self.class.increment_counter(counter_cache_column, parent_id) end end else def update_parents_counter_cache counter_cache_column = self.class.children_counter_cache_column if parent_id_changed? self.class.decrement_counter(counter_cache_column, parent_id_was) self.class.increment_counter(counter_cache_column, parent_id) end end end end end # Deprecating the following code in the future. require 'acts_as_tree/active_record/acts/tree'