1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
|
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
# <tt>acts_as_tree</tt>:
# * <tt>siblings</tt> - Returns all the children of the parent, excluding
# the current node (<tt>[subchild2]</tt> when called
# on <tt>subchild1</tt>)
# * <tt>self_and_siblings</tt> - Returns all the children of the parent,
# including the current node (<tt>[subchild1, subchild2]</tt>
# when called on <tt>subchild1</tt>)
# * <tt>ancestors</tt> - Returns all the ancestors of the current node
# (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
# * <tt>root</tt> - Returns the root of the current node (<tt>root</tt>
# when called on <tt>subchild2</tt>)
module ClassMethods
# Configuration options are:
#
# * <tt>primary_key</tt> - specifies the column name for relations
# (default: +id+)
# * <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+). 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) %><br />
# <% 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'
|