acts_as_tree-2.9.1/ 0000755 0000041 0000041 00000000000 13641041375 014213 5 ustar www-data www-data acts_as_tree-2.9.1/.travis.yml 0000644 0000041 0000041 00000002042 13641041375 016322 0 ustar www-data www-data language: ruby
sudo: false
cache: bundler
before_install: gem install bundler -v '~> 1.15' --conservative --minimal-deps
matrix:
include:
# using ruby 1.9.3 for lack of 1.9.2 rvm binaries
- rvm: 1.9.3
gemfile: gemfiles/rails-3.0.gemfile
- rvm: 1.9.3
gemfile: gemfiles/rails-3.1.gemfile
- rvm: 1.9.3
gemfile: gemfiles/rails-3.2.gemfile
# rails 4.x requries ruby >= 1.9.3
- rvm: 1.9.3
gemfile: gemfiles/rails-4.0.gemfile
- rvm: 1.9.3
gemfile: gemfiles/rails-4.1.gemfile
- rvm: 1.9.3
gemfile: gemfiles/rails-4.2.gemfile
# rails 5.x requires ruby >= 2.2.2
- rvm: 2.2.10
gemfile: gemfiles/rails-5.0.gemfile
- rvm: 2.2.10
gemfile: gemfiles/rails-5.1.gemfile
- rvm: 2.2.10
gemfile: gemfiles/rails-5.2.gemfile
# rails 6.x requires ruby >= 2.5.0
- rvm: 2.5.6
gemfile: gemfiles/rails-6.0.gemfile
- rvm: 2.6.4
gemfile: gemfiles/rails-6.0.gemfile
# test latest rails and ruby
- rvm: 2.7.0
gemfile: gemfiles/rails-6.0.gemfile
acts_as_tree-2.9.1/test/ 0000755 0000041 0000041 00000000000 13641041375 015172 5 ustar www-data www-data acts_as_tree-2.9.1/test/acts_as_tree_test.rb 0000644 0000041 0000041 00000045757 13641041375 021234 0 ustar www-data www-data require 'minitest/autorun'
require 'minitest/benchmark'
require 'active_record'
require 'acts_as_tree'
class ActsAsTreeTestCase < (defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase)
UPDATE_METHOD = ActiveRecord::VERSION::MAJOR >= 4 ? :update : :update_attributes
def assert_queries(num = 1, &block)
query_count, result = count_queries(&block)
result
ensure
assert_equal num, query_count, "#{query_count} instead of #{num} queries were executed."
end
def assert_no_queries(&block)
assert_queries(0, &block)
end
def count_queries &block
count = 0
counter_f = ->(name, started, finished, unique_id, payload) {
unless %w[ CACHE SCHEMA ].include? payload[:name]
count += 1
end
}
begin
subscribed = ActiveSupport::Notifications.subscribe("sql.active_record", &counter_f)
result = block.call
ensure
ActiveSupport::Notifications.unsubscribe subscribed
end
[count, result]
end
def capture_stdout(&block)
real_stdout = $stdout
$stdout = StringIO.new
yield
$stdout.string
ensure
$stdout = real_stdout
end
end
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
def setup_db(options = {})
# AR keeps printing annoying schema statements
capture_stdout do
ActiveRecord::Base.logger
ActiveRecord::Schema.define do
create_table :mixins, force: true do |t|
t.column :type, :string
t.column :parent_id, :integer
t.column :external_id, :integer if options[:external_ids]
t.column :external_parent_id, :integer if options[:external_ids]
t.column :children_count, :integer, default: 0 if options[:counter_cache]
t.timestamps null: false
end
create_table :level_mixins, force: true do |t|
t.column :level, :string
t.column :parent_id, :integer
t.timestamps null: false
end
end
# Fix broken reset_column_information in some activerecord versions.
if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 2 ||
ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 1
ActiveRecord::Base.connection.schema_cache.clear!
end
Mixin.reset_column_information
end
end
class Mixin < ActiveRecord::Base
include ActsAsTree
end
class LevelMixin < ActiveRecord::Base
include ActsAsTree
acts_as_tree foreign_key: "parent_id", order: "id"
end
class TreeMixin < Mixin
acts_as_tree foreign_key: "parent_id", order: "id"
end
class TreeMixinWithLevelMethod < Mixin
acts_as_tree foreign_key: "parent_id", order: "id"
def level
'Has Level Method'
end
end
class TreeMixinWithoutOrder < Mixin
acts_as_tree foreign_key: "parent_id"
end
class TreeMixinNullify < Mixin
acts_as_tree foreign_key: "parent_id", order: "id", dependent: :nullify
end
class TreeMixinWithCounterCache < Mixin
acts_as_tree foreign_key: "parent_id", order: "id", counter_cache: :children_count
end
class RecursivelyCascadedTreeMixin < Mixin
acts_as_tree foreign_key: "parent_id"
has_one :first_child, class_name: 'RecursivelyCascadedTreeMixin', foreign_key: :parent_id
end
class TreeMixinWithTouch < Mixin
acts_as_tree foreign_key: "parent_id", order: "id", touch: true
end
class ExternalTreeMixin < Mixin
acts_as_tree foreign_key: "external_parent_id", primary_key: "external_id"
end
class ExternalTreeMixinNullify < Mixin
acts_as_tree foreign_key: "external_parent_id", primary_key: "external_id", order: "id", dependent: :nullify
end
class TreeTest < ActsAsTreeTestCase
def setup
setup_db
@tree_mixin = TreeMixin
@root1 = @tree_mixin.create!
@root_child1 = @tree_mixin.create! parent_id: @root1.id
@child1_child = @tree_mixin.create! parent_id: @root_child1.id
@child1_child_child = @tree_mixin.create! parent_id: @child1_child.id
@root_child2 = @tree_mixin.create! parent_id: @root1.id
@root2 = @tree_mixin.create!
@root3 = @tree_mixin.create!
end
def test_children
assert_equal @root1.children, [@root_child1, @root_child2]
assert_equal @root_child1.children, [@child1_child]
assert_equal @child1_child.children, [@child1_child_child]
assert_equal @child1_child_child.children, []
assert_equal @root_child2.children, []
end
def test_parent
assert_equal @root_child1.parent, @root1
assert_equal @root_child1.parent, @root_child2.parent
assert_nil @root1.parent
end
def test_delete
assert_equal 7, @tree_mixin.count
@root1.destroy
assert_equal 2, @tree_mixin.count
@root2.destroy
@root3.destroy
assert_equal 0, @tree_mixin.count
end
def test_insert
@extra = @root1.children.create
assert @extra
assert_equal @extra.parent, @root1
assert_equal 3, @root1.reload.children.count
assert @root1.children.include?(@extra)
assert @root1.children.include?(@root_child1)
assert @root1.children.include?(@root_child2)
end
def test_ancestors
assert_equal [], @root1.ancestors
assert_equal [@root1], @root_child1.ancestors
assert_equal [@root_child1, @root1], @child1_child.ancestors
assert_equal [@root1], @root_child2.ancestors
assert_equal [], @root2.ancestors
assert_equal [], @root3.ancestors
end
def test_root
assert_equal @root1, @tree_mixin.root
assert_equal @root1, @root1.root
assert_equal @root1, @root_child1.root
assert_equal @root1, @child1_child.root
assert_equal @root1, @root_child2.root
assert_equal @root2, @root2.root
assert_equal @root3, @root3.root
end
def test_roots
assert_equal [@root1, @root2, @root3], @tree_mixin.roots
end
def test_leaves
assert_equal [@child1_child_child, @root_child2, @root2, @root3], @tree_mixin.leaves
end
def test_default_tree_order
assert_equal [@root1, @root_child1, @child1_child, @child1_child_child, @root_child2, @root2, @root3], @tree_mixin.default_tree_order
end
def test_siblings
assert_equal [@root2, @root3], @root1.siblings
assert_equal [@root_child2], @root_child1.siblings
assert_equal [], @child1_child.siblings
assert_equal [@root_child1], @root_child2.siblings
assert_equal [@root1, @root3], @root2.siblings
assert_equal [@root1, @root2], @root3.siblings
end
def test_self_and_siblings
assert_equal [@root1, @root2, @root3], @root1.self_and_siblings
assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings
assert_equal [@child1_child], @child1_child.self_and_siblings
assert_equal [@root_child1, @root_child2], @root_child2.self_and_siblings
assert_equal [@root1, @root2, @root3], @root2.self_and_siblings
assert_equal [@root1, @root2, @root3], @root3.self_and_siblings
end
def test_self_and_children
assert_equal [@root1, @root_child1, @root_child2], @root1.self_and_children
assert_equal [@root2], @root2.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
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
end
def test_descendants
assert_equal [@root_child1, @root_child2, @child1_child, @child1_child_child], @root1.descendants
assert_equal [], @root2.descendants
end
def test_nullify
root4 = TreeMixinNullify.create!
root4_child = TreeMixinNullify.create! parent_id: root4.id
assert_equal 2, TreeMixinNullify.count
assert_equal root4.id, root4_child.parent_id
root4.destroy
assert_equal 1, TreeMixinNullify.count
assert_nil root4_child.reload.parent_id
end
def test_is_root
assert_equal true, @root1.root?
assert_equal true, @root2.root?
assert_equal true, @root3.root?
assert_equal false, @root_child1.root?
assert_equal false, @child1_child.root?
assert_equal false, @child1_child_child.root?
assert_equal false, @root_child2.root?
end
def test_is_leaf
assert_equal true, @root2.leaf?
assert_equal true, @root3.leaf?
assert_equal true, @child1_child_child.leaf?
assert_equal true, @root_child2.leaf?
assert_equal false, @root1.leaf?
assert_equal false, @root_child1.leaf?
assert_equal false, @child1_child.leaf?
end
def test_tree_view
assert_equal false, @tree_mixin.respond_to?(:tree_view)
@tree_mixin.extend ActsAsTree::TreeView
assert_equal true, @tree_mixin.respond_to?(:tree_view)
tree_view_outputs = <<-END.gsub(/^ {6}/, '')
root
|_ 1
| |_ 2
| |_ 3
| |_ 4
| |_ 5
|_ 6
|_ 7
END
assert_equal tree_view_outputs, capture_stdout { @tree_mixin.tree_view(:id) }
end
def test_tree_walker
assert_equal false, @tree_mixin.respond_to?(:walk_tree)
assert_equal false, @tree_mixin.new.respond_to?(:walk_tree)
@tree_mixin.extend ActsAsTree::TreeWalker
assert_equal true, @tree_mixin.respond_to?(:walk_tree)
assert_equal true, @tree_mixin.new.respond_to?(:walk_tree)
walk_tree_dfs_output = <<-END.gsub(/^\s+/, '')
1
-2
--3
---4
-5
6
7
END
assert_equal walk_tree_dfs_output, capture_stdout { @tree_mixin.walk_tree{|elem, level| puts "#{'-'*level}#{elem.id}"} }
walk_tree_dfs_sub_output = <<-END.gsub(/^\s+/, '')
2
-3
--4
5
END
assert_equal walk_tree_dfs_sub_output, capture_stdout { @root1.walk_tree{|elem, level| puts "#{'-'*level}#{elem.id}"} }
walk_tree_bfs_output = <<-END.gsub(/^\s+/, '')
1
6
7
-2
-5
--3
---4
END
assert_equal walk_tree_bfs_output, capture_stdout { @tree_mixin.walk_tree(:algorithm => :bfs){|elem, level| puts "#{'-'*level}#{elem.id}"} }
walk_tree_bfs_sub_output = <<-END.gsub(/^\s+/, '')
2
5
-3
--4
END
assert_equal walk_tree_bfs_sub_output, capture_stdout { @root1.walk_tree(:algorithm => :bfs){|elem, level| puts "#{'-'*level}#{elem.id}"} }
end
end
class TestDeepDescendantsPerformance < ActsAsTreeTestCase
def setup
setup_db
@root1 = TreeMixin.create!
create_cascade_children @root1, "root1", 10
@root2 = TreeMixin.create!
create_cascade_children @root2, "root2", 20
@root3 = TreeMixin.create!
create_cascade_children @root3, "root3", 30
@root4 = TreeMixin.create!
create_cascade_children @root4, "root4", 40
@root5 = TreeMixin.create!
create_cascade_children @root5, "root5", 50
end
def self.bench_range
[1, 2, 3, 4, 5]
end
def bench_descendants
skip("until I deal with the performance difference on travis")
assert_performance_linear 0.99 do |x|
obj = instance_variable_get "@root#{x}"
obj.descendants
end
end
def create_cascade_children parent, parent_name, count
first_child_name = "@#{parent_name}_child1"
first_record = TreeMixin.create! parent_id: parent.id
instance_variable_set first_child_name, first_record
(2...count).each do |child_count|
name = "@#{parent_name}_child#{child_count}"
prev = instance_variable_get "@#{parent_name}_child#{child_count - 1}"
new_record = TreeMixin.create! parent_id: prev.id
instance_variable_set name, new_record
end
end
end
class TreeTestWithEagerLoading < ActsAsTreeTestCase
def setup
setup_db
@root1 = TreeMixin.create!
@root_child1 = TreeMixin.create! parent_id: @root1.id
@child1_child = TreeMixin.create! parent_id: @root_child1.id
@root_child2 = TreeMixin.create! parent_id: @root1.id
@root2 = TreeMixin.create!
@root3 = TreeMixin.create!
@rc1 = RecursivelyCascadedTreeMixin.create!
@rc2 = RecursivelyCascadedTreeMixin.create! parent_id: @rc1.id
@rc3 = RecursivelyCascadedTreeMixin.create! parent_id: @rc2.id
@rc4 = RecursivelyCascadedTreeMixin.create! parent_id: @rc3.id
end
def test_eager_association_loading
roots = TreeMixin.includes(:children)
.where('mixins.parent_id IS NULL')
.order('mixins.id')
assert_equal [@root1, @root2, @root3], roots
assert_no_queries do
assert_equal 2, roots[0].children.size
assert_equal 0, roots[1].children.size
assert_equal 0, roots[2].children.size
end
end
def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
root_node = RecursivelyCascadedTreeMixin.includes({children: {children: :children}})
.order('mixins.id')
.first
assert_equal @rc4, assert_no_queries { root_node.children.first.children.first.children.first }
end
def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
root_node = RecursivelyCascadedTreeMixin.includes({first_child: {first_child: :first_child}})
.order('mixins.id')
.first
assert_equal @rc4, assert_no_queries { root_node.first_child.first_child.first_child }
end
def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
leaf_node = RecursivelyCascadedTreeMixin.includes({parent: {parent: :parent}})
.order('mixins.id DESC')
.first
assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent }
end
end
class TreeTestWithoutOrder < ActsAsTreeTestCase
def setup
setup_db
@root1 = TreeMixinWithoutOrder.create!
@root2 = TreeMixinWithoutOrder.create!
end
def test_root
assert [@root1, @root2].include? TreeMixinWithoutOrder.root
end
def test_roots
assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots
end
end
class UnsavedTreeTest < ActsAsTreeTestCase
def setup
setup_db
@root = TreeMixin.new
@root_child = @root.children.build
end
def test_inverse_of
# We want children to be aware of their parent before saving either
assert_equal @root, @root_child.parent
end
end
class TreeTestWithCounterCache < ActsAsTreeTestCase
def setup
setup_db counter_cache: true
@root = TreeMixinWithCounterCache.create!
@child1 = TreeMixinWithCounterCache.create! parent_id: @root.id
@child1_child1 = TreeMixinWithCounterCache.create! parent_id: @child1.id
@child2 = TreeMixinWithCounterCache.create! parent_id: @root.id
[@root, @child1, @child1_child1, @child2].map(&:reload)
end
def test_counter_cache
assert_equal 2, @root.children_count
assert_equal 1, @child1.children_count
end
def test_update_parents_counter_cache
@child1_child1.public_send(UPDATE_METHOD, :parent_id => @root.id)
assert_equal 3, @root.reload.children_count
assert_equal 0, @child1.reload.children_count
end
def test_leaves
assert_equal [@child1_child1, @child2], TreeMixinWithCounterCache.leaves
assert !@root.leaf?
assert @child2.leaf?
end
def test_counter_cache_being_used
assert_no_queries { @root.leaf? }
assert_no_queries { @child2.leaf? }
end
end
class TreeTestWithTouch < ActsAsTreeTestCase
def setup
setup_db
@root = TreeMixinWithTouch.create!
@child = TreeMixinWithTouch.create! parent_id: @root.id
end
def test_updated_at
previous_root_updated_at = @root.updated_at
@child.public_send(UPDATE_METHOD, :type => "new_type")
@root.reload
assert @root.updated_at != previous_root_updated_at
end
end
class ExternalTreeTest < TreeTest
def setup
setup_db external_ids: true
@tree_mixin = ExternalTreeMixin
@root1 = @tree_mixin.create! external_id: 1101
@root_child1 = @tree_mixin.create! external_id: 1102, external_parent_id: @root1.external_id
@child1_child = @tree_mixin.create! external_id: 1103, external_parent_id: @root_child1.external_id
@child1_child_child = @tree_mixin.create! external_id: 1104, external_parent_id: @child1_child.external_id
@root_child2 = @tree_mixin.create! external_id: 1105, external_parent_id: @root1.external_id
@root2 = @tree_mixin.create! external_id: 1106
@root3 = @tree_mixin.create! external_id: 1107
end
def test_nullify
root4 = ExternalTreeMixinNullify.create! external_id: 1108
root4_child = ExternalTreeMixinNullify.create! external_id: 1109, external_parent_id: root4.external_id
assert_equal 2, ExternalTreeMixinNullify.count
assert_equal root4.external_id, root4_child.external_parent_id
root4.destroy
assert_equal 1, ExternalTreeMixinNullify.count
assert_nil root4_child.reload.external_parent_id
end
end
class GenerationMethods < ActsAsTreeTestCase
def setup
setup_db
@root1 = TreeMixin.create!
@root_child1 = TreeMixin.create! parent_id: @root1.id
@child1_child = TreeMixin.create! parent_id: @root_child1.id
@child1_child_child = TreeMixin.create! parent_id: @child1_child.id
@root_child2 = TreeMixin.create! parent_id: @root1.id
@root2 = TreeMixin.create!
@root2_child1 = TreeMixin.create! parent_id: @root2.id
@root2_child2 = TreeMixin.create! parent_id: @root2.id
@root2_child1_child = TreeMixin.create! parent_id: @root2_child1.id
@root3 = TreeMixin.create!
@level_column = LevelMixin.create! level: 'Has Level Column'
@level_method = TreeMixinWithLevelMethod.create!
end
def test_generations
assert_equal(
{
0 => [@root1, @root2, @root3],
1 => [@root_child1, @root_child2, @root2_child1, @root2_child2],
2 => [@child1_child, @root2_child1_child],
3 => [@child1_child_child]
},
TreeMixin.generations
)
end
def test_generation
assert_equal [@root2, @root3], @root1.generation
assert_equal [@root_child2, @root2_child1, @root2_child2],
@root_child1.generation
assert_equal [@root2_child1_child], @child1_child.generation
assert_equal [], @child1_child_child.generation
end
def test_self_and_generation
assert_equal [@root1, @root2, @root3], @root1.self_and_generation
assert_equal [@root_child1, @root_child2, @root2_child1, @root2_child2],
@root_child1.self_and_generation
assert_equal [@child1_child, @root2_child1_child],
@child1_child.self_and_generation
assert_equal [@child1_child_child], @child1_child_child.self_and_generation
end
def test_tree_level
assert_equal 0, @root1.tree_level
assert_equal 1, @root_child1.tree_level
assert_equal 2, @child1_child.tree_level
assert_equal 3, @child1_child_child.tree_level
end
def test_level
assert_equal 0, @root1.level
assert_equal 1, @root_child1.level
assert_equal 2, @child1_child.level
assert_equal 3, @child1_child_child.level
end
def test_alias_tree_level
assert_equal 'Has Level Method', @level_method.level
assert_equal 'Has Level Column', @level_column.level
end
end
acts_as_tree-2.9.1/README.md 0000644 0000041 0000041 00000011526 13641041375 015477 0 ustar www-data www-data # ActsAsTree
[](http://travis-ci.org/amerine/acts\_as\_tree)
[](http://badge.fury.io/rb/acts\_as\_tree)
ActsAsTree extends ActiveRecord to add simple support for organizing items into parent–children relationships. By default, ActsAsTree expects a foreign key column called `parent_id`.
## Example
```ruby
class Category < ActiveRecord::Base
acts_as_tree order: "name"
end
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
```
We also have a convenient `TreeView` module you can mixin if you want a little visual representation of the tree strucuture. Example:
```ruby
class Category < ActiveRecord::Base
extend ActsAsTree::TreeView
acts_as_tree order: 'name'
end
> Category.tree_view(:name)
root
|_ child1
| |_ subchild1
| |_ subchild2
|_ child2
|_ subchild3
|_ subchild4
=> nil
```
And there's a `TreeWalker` module (traversing the tree using depth-first search (default) or breadth-first search) as well. Example given the Model `Page` as
```ruby
class Page < ActiveRecord::Base
extend ActsAsTree::TreeWalker
acts_as_tree order: 'rank'
end
```
In your view you could traverse the tree using
```erb
<% Page.walk_tree do |page, level| %>
<%= link_to "#{'-'*level}#{page.name}", page_path(page) %>
<% end %>
```
You also could use walk\_tree as an instance method such as:
```erb
<% Page.first.walk_tree do |page, level| %>
<%= link_to "#{'-'*level}#{page.name}", page_path(page) %>
<% end %>
```
## Compatibility
We no longer support Ruby 1.8 or versions of Rails/ActiveRecord older than 3.0. If you're using a version of ActiveRecord older than 3.0 please use 0.1.1.
Moving forward we will do our best to support the latest versions of ActiveRecord and Ruby.
## Change Log
The Change Log has moved to the [releases](https://github.com/amerine/acts_as_tree/releases) page.
## Note on Patches/Pull Requests
1. Fork the project.
2. Make your feature addition or bug fix.
3. Add tests for it. This is important so we don't break it in a future version
unintentionally.
4. Commit, do not mess with rakefile, version, or history. (if you want to have
your own version, that is fine but bump version in a commit by itself so we can
ignore when we pull)
5. Send us a pull request. Bonus points for topic branches.
6. All contributors to this project, after their first accepted patch, are given push
access to the repository and are welcome as full contributors to ActsAsTree. All
we ask is that all changes go through CI and a Pull Request before merging.
## Releasing new versions
1. We follow Semver. So if you're shipping interface breaking changes then bump
the major version. We don't care if we ship version 1101.1.1, as long as
people know that 1101.1.1 has breaking differences from 1100.0. If you're
adding new features, but not changing existing functionality bump the minor
version, if you're shipping a bugfix, just bump the patch.
2. Following the above rules, change the version found in lib/acts\_as\_tree/version.rb.
3. Commit these changes in one "release-prep" commit (on the master branch).
4. Push that commit up to the repo.
5. Run `rake release`
This will create and push a tag to GitHub, then generate a gem and push it to
Rubygems.
6. Create a new release from the tag on GitHub, by choosing "Draft a new release" button
on the [releases](https://github.com/amerine/acts_as_tree/releases) tab and include
the relevant changes in the description.
7. Profit.
## License (MIT)
Copyright (c) 2007 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the 'Software'), to deal in the
Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
acts_as_tree-2.9.1/gemfiles/ 0000755 0000041 0000041 00000000000 13641041375 016006 5 ustar www-data www-data acts_as_tree-2.9.1/gemfiles/rails-4.0.gemfile 0000644 0000041 0000041 00000000176 13641041375 020755 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 4.0.0"
gem "mime-types", "< 3"
gem "sqlite3", "~> 1.3.6"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-3.1.gemfile 0000644 0000041 0000041 00000000247 13641041375 020754 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 3.1.0"
gem "i18n", "< 0.7"
gem "rack-cache", "< 1.3"
gem "rake", "< 11"
gem "sqlite3", "~> 1.3.4"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-3.2.gemfile 0000644 0000041 0000041 00000000250 13641041375 020747 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 3.2.22"
gem "i18n", "< 0.7"
gem "rack-cache", "< 1.3"
gem "rake", "< 11"
gem "sqlite3", "~> 1.3.5"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-6.0.gemfile 0000644 0000041 0000041 00000000144 13641041375 020752 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 6.0.0"
gem "sqlite3", "~> 1.4"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-5.2.gemfile 0000644 0000041 0000041 00000000160 13641041375 020751 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 5.2.0"
gem "sqlite3", "~> 1.3", ">= 1.3.6"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-4.2.gemfile 0000644 0000041 0000041 00000000226 13641041375 020753 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 4.2.0"
gem "mime-types", "< 3"
gem "nokogiri", "< 1.7"
gem "sqlite3", "~> 1.3.6"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-3.0.gemfile 0000644 0000041 0000041 00000000215 13641041375 020746 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 3.0.0"
gem "i18n", "< 0.7"
gem "rake", "< 11"
gem "sqlite3", "~> 1.3.4"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-5.1.gemfile 0000644 0000041 0000041 00000000160 13641041375 020750 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 5.1.0"
gem "sqlite3", "~> 1.3", ">= 1.3.6"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-4.1.gemfile 0000644 0000041 0000041 00000000176 13641041375 020756 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 4.1.0"
gem "mime-types", "< 3"
gem "sqlite3", "~> 1.3.6"
gemspec path: "../"
acts_as_tree-2.9.1/gemfiles/rails-5.0.gemfile 0000644 0000041 0000041 00000000146 13641041375 020753 0 ustar www-data www-data source "https://rubygems.org"
gem "rails", "~> 5.0.0"
gem "sqlite3", "~> 1.3.6"
gemspec path: "../"
acts_as_tree-2.9.1/.gitignore 0000644 0000041 0000041 00000000100 13641041375 016172 0 ustar www-data www-data *.gem
.bundle
pkg/*
.rvmrc
Gemfile.lock
gemfiles/*.gemfile.lock
acts_as_tree-2.9.1/Rakefile 0000644 0000041 0000041 00000000751 13641041375 015663 0 ustar www-data www-data require 'bundler/gem_tasks'
desc "Run the tests."
task :test do
$: << "lib" << "test"
Dir["test/*_test.rb"].each { |f| require f[5..-4] }
end
task :default => :test
# Run the rdoc task to generate rdocs for this gem
require 'rdoc/task'
RDoc::Task.new do |rdoc|
require "acts_as_tree/version"
version = ActsAsTree::VERSION
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "acts_as_tree-rails3 #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
acts_as_tree-2.9.1/lib/ 0000755 0000041 0000041 00000000000 13641041375 014761 5 ustar www-data www-data acts_as_tree-2.9.1/lib/acts_as_tree/ 0000755 0000041 0000041 00000000000 13641041375 017415 5 ustar www-data www-data acts_as_tree-2.9.1/lib/acts_as_tree/version.rb 0000644 0000041 0000041 00000000052 13641041375 021424 0 ustar www-data www-data module ActsAsTree
VERSION = "2.9.1"
end
acts_as_tree-2.9.1/lib/acts_as_tree/railtie.rb 0000644 0000041 0000041 00000000361 13641041375 021373 0 ustar www-data www-data module ActsAsTree
class Railtie < Rails::Railtie
initializer 'acts_as_tree.insert_into_active_record' do
ActiveSupport.on_load :active_record do
ActiveRecord::Base.send(:include, ActsAsTree)
end
end
end
end
acts_as_tree-2.9.1/lib/acts_as_tree/active_record/ 0000755 0000041 0000041 00000000000 13641041375 022226 5 ustar www-data www-data acts_as_tree-2.9.1/lib/acts_as_tree/active_record/acts/ 0000755 0000041 0000041 00000000000 13641041375 023160 5 ustar www-data www-data acts_as_tree-2.9.1/lib/acts_as_tree/active_record/acts/tree.rb 0000644 0000041 0000041 00000000471 13641041375 024446 0 ustar www-data www-data require 'acts_as_tree'
module ActiveRecord
module Acts #:nodoc:
module Tree
include ::ActsAsTree
def self.included(base)
Kernel.warn "[DEPRECATION] The module ActiveRecord::Acts::Tree has moved to ActsAsTree"
base.extend ::ActsAsTree::ClassMethods
end
end
end
end
acts_as_tree-2.9.1/lib/acts_as_tree.rb 0000644 0000041 0000041 00000030242 13641041375 017743 0 ustar www-data www-data 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'
acts_as_tree-2.9.1/acts_as_tree.gemspec 0000644 0000041 0000041 00000002265 13641041375 020221 0 ustar www-data www-data # -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require 'acts_as_tree/version'
Gem::Specification.new do |s|
s.name = 'acts_as_tree'
s.version = ActsAsTree::VERSION
s.authors = ['Erik Dahlstrand', 'Rails Core', 'Mark Turner', 'Swanand Pagnis', 'Felix Bünemann']
s.email = ['erik.dahlstrand@gmail.com', 'mark@amerine.net', 'swanand.pagnis@gmail.com', 'felix.buenemann@gmail.com']
s.homepage = 'https://github.com/amerine/acts_as_tree'
s.summary = %q{Provides a simple tree behaviour to active_record models.}
s.description = %q{A gem that adds simple support for organizing ActiveRecord models into parent–children relationships.}
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
s.rdoc_options = ["--charset=UTF-8"]
s.add_dependency "activerecord", ">= 3.0.0"
# Dependencies (installed via 'bundle install')...
s.add_development_dependency "sqlite3"
s.add_development_dependency "rdoc"
s.add_development_dependency "minitest", ">= 4.7.5"
end
acts_as_tree-2.9.1/Gemfile 0000644 0000041 0000041 00000000110 13641041375 015476 0 ustar www-data www-data source 'https://rubygems.org'
gemspec
group :test do
gem 'rake'
end