Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ jobs:
gemfile: gemfiles/rails-7.0.gemfile
- ruby-version: 3.1
gemfile: gemfiles/rails-7.0.gemfile
- ruby-version: 3.2
gemfile: gemfiles/rails-7.0.gemfile
- ruby-version: 3.3
gemfile: gemfiles/rails-7.0.gemfile
- ruby-version: 3.1
gemfile: gemfiles/rails-7.1.gemfile
- ruby-version: 3.2
gemfile: gemfiles/rails-7.1.gemfile
- ruby-version: 3.3
gemfile: gemfiles/rails-7.1.gemfile
# rails 7.2 requires ruby >= 3.1.0
- ruby-version: 3.1
gemfile: gemfiles/rails-7.2.gemfile
- ruby-version: 3.2
gemfile: gemfiles/rails-7.2.gemfile
- ruby-version: 3.3
gemfile: gemfiles/rails-7.2.gemfile

runs-on: ubuntu-latest

Expand Down
6 changes: 6 additions & 0 deletions gemfiles/rails-7.1.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source "https://rubygems.org"

gem "rails", "~> 7.1.0"
gem "sqlite3", "~> 1.4"

gemspec path: "../"
6 changes: 6 additions & 0 deletions gemfiles/rails-7.2.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source "https://rubygems.org"

gem "rails", "~> 7.2.0"
gem "sqlite3", "~> 1.4"

gemspec path: "../"
86 changes: 67 additions & 19 deletions lib/acts_as_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ def acts_as_tree(options = {})

include ActsAsTree::InstanceMethods

define_singleton_method :acts_as_tree_primary_key do
configuration[:primary_key]
end

define_singleton_method :acts_as_tree_foreign_key do
configuration[:foreign_key]
end

define_singleton_method :default_tree_order do
order(configuration[:order])
end
Expand Down Expand Up @@ -250,33 +258,62 @@ 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
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
def ancestors
self_and_ancestors.excluding self
end
else
def ancestors
node, nodes = self, []
nodes << node = node.parent while node.parent
nodes
end
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
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
def descendants
self_and_descendants.excluding self
end
else
def descendants
children.each_with_object(children.to_a) {|child, arr|
arr.concat child.descendants
}.uniq
end
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
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
def self_and_descendants
self.class.where self.class.acts_as_tree_primary_key => self.class.with_recursive(
search_tree: [
self.class.where(self.class.acts_as_tree_primary_key => send(self.class.acts_as_tree_primary_key)),
self.class.joins("JOIN search_tree ON #{self.class.table_name}.#{self.class.acts_as_tree_foreign_key} = search_tree.#{self.class.acts_as_tree_primary_key}")
]
).select(self.class.acts_as_tree_primary_key).from("search_tree")
end
else
def self_and_descendants
[self] + descendants
end
end

# Returns the root node of the tree.
def root
node = self
node = node.parent while node.parent
node
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
def root
self_and_ancestors.find_by self.class.acts_as_tree_foreign_key => nil
end
else
def root
node = self
node = node.parent while node.parent
node
end
end

# Returns all siblings of the current node.
Expand Down Expand Up @@ -307,15 +344,15 @@ def self_and_generation
self.class.select {|node| node.tree_level == self.tree_level }
end

# Returns the level (depth) of the current node
# 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.
# 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
Expand All @@ -337,8 +374,19 @@ def self_and_children
# Returns ancestors and current node itself.
#
# subchild1.self_and_ancestors # => [subchild1, child1, root]
def self_and_ancestors
[self] + self.ancestors
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
def self_and_ancestors
self.class.where self.class.acts_as_tree_primary_key => self.class.with_recursive(
search_tree: [
self.class.where(self.class.acts_as_tree_primary_key => send(self.class.acts_as_tree_primary_key)),
self.class.joins("JOIN search_tree ON #{self.class.table_name}.#{self.class.acts_as_tree_primary_key} = search_tree.#{self.class.acts_as_tree_foreign_key}")
]
).select(self.class.acts_as_tree_primary_key).from("search_tree")
end
else
def self_and_ancestors
[self] + self.ancestors
end
end

# Returns true if node has no parent, false otherwise
Expand Down
73 changes: 65 additions & 8 deletions test/acts_as_tree_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
require 'active_record'
require 'acts_as_tree'

class ActsAsTreeTestCase < (defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase)
parent_class = if defined?(Minitest::Test)
Minitest::Test
else
(defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase)
end

class ActsAsTreeTestCase < parent_class
UPDATE_METHOD = ActiveRecord::VERSION::MAJOR >= 4 ? :update : :update_attributes

def assert_queries(num = 1, &block)
Expand Down Expand Up @@ -183,7 +189,11 @@ def test_insert
def test_ancestors
assert_equal [], @root1.ancestors
assert_equal [@root1], @root_child1.ancestors
assert_equal [@root_child1, @root1], @child1_child.ancestors
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
[@root_child1, @root1].all? { |node| assert_includes @child1_child.ancestors, node }
else
assert_equal [@root_child1, @root1], @child1_child.ancestors
end
assert_equal [@root1], @root_child2.ancestors
assert_equal [], @root2.ancestors
assert_equal [], @root3.ancestors
Expand Down Expand Up @@ -235,18 +245,65 @@ def test_self_and_children
end

def test_self_and_ancestors
assert_equal [@child1_child, @root_child1, @root1], @child1_child.self_and_ancestors
assert_equal [@root2], @root2.self_and_ancestors
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
[@child1_child, @root_child1, @root1].all? { |node| assert_includes @child1_child.self_and_ancestors, node }
[@root2].all? { |node| assert_includes @root2.self_and_ancestors, node }
else
assert_equal [@child1_child, @root_child1, @root1], @child1_child.self_and_ancestors
assert_equal [@root2], @root2.self_and_ancestors
end
end

def test_self_and_descendants
assert_equal [@root1, @root_child1, @root_child2, @child1_child, @child1_child_child], @root1.self_and_descendants
assert_equal [@root2], @root2.self_and_descendants
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
[@root1, @root_child1, @root_child2, @child1_child, @child1_child_child].all? { |node| assert_includes @root1.self_and_descendants, node }
[@root2].all? { |node| assert_includes @root2.self_and_descendants, node }
else
assert_equal [@root1, @root_child1, @root_child2, @child1_child, @child1_child_child], @root1.self_and_descendants
assert_equal [@root2], @root2.self_and_descendants
end
end

def test_descendants
assert_equal [@root_child1, @root_child2, @child1_child, @child1_child_child], @root1.descendants
assert_equal [], @root2.descendants
if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
[@root_child1, @root_child2, @child1_child, @child1_child_child].all? { |node| assert_includes @root1.descendants, node }
assert_empty @root2.descendants
else
assert_equal [@root_child1, @root_child2, @child1_child, @child1_child_child], @root1.descendants
assert_equal [], @root2.descendants
end
end

if ActiveRecord::VERSION::MAJOR > 7 || ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR >= 2
def test_self_and_descendants_performs_one_query
assert_queries 1 do
@root2.self_and_descendants.count
end
end

def test_descendants_performs_one_query
assert_queries 1 do
@root1.descendants.count
end
end

def test_self_and_ancestors_performs_one_query
assert_queries 1 do
@child1_child.self_and_ancestors.count
end
end

def test_ancestors_performs_one_query
assert_queries 1 do
@child1_child.ancestors.count
end
end

def test_root_performs_one_query
assert_queries 1 do
@child1_child.root
end
end
end

def test_nullify
Expand Down