acts_as_tree-2.8.0/0000755000004100000410000000000013341233424014204 5ustar www-datawww-dataacts_as_tree-2.8.0/.travis.yml0000644000004100000410000000361113341233424016316 0ustar www-datawww-datalanguage: ruby sudo: false cache: bundler before_install: gem install bundler -v '~> 1.15' --conservative --minimal-deps rvm: - 1.9.2 - 1.9.3 - 2.0.0 - 2.1.8 - 2.2.4 gemfile: - gemfiles/rails-3.0.gemfile - gemfiles/rails-3.1.gemfile - gemfiles/rails-3.2.gemfile - gemfiles/rails-4.0.gemfile - gemfiles/rails-4.1.gemfile - gemfiles/rails-4.2.gemfile - gemfiles/rails-5.0.gemfile - gemfiles/rails-5.1.gemfile - gemfiles/rails-5.2.gemfile matrix: exclude: # rails 4.x requries ruby 1.9.3 or newer - rvm: 1.9.2 gemfile: gemfiles/rails-4.0.gemfile - rvm: 1.9.2 gemfile: gemfiles/rails-4.1.gemfile - rvm: 1.9.2 gemfile: gemfiles/rails-4.2.gemfile # rails < 3.2 is unsupported on ruby 2.0+ - rvm: 2.0.0 gemfile: gemfiles/rails-3.0.gemfile - rvm: 2.0.0 gemfile: gemfiles/rails-3.1.gemfile - rvm: 2.1.8 gemfile: gemfiles/rails-3.0.gemfile - rvm: 2.1.8 gemfile: gemfiles/rails-3.1.gemfile - rvm: 2.2.4 gemfile: gemfiles/rails-3.0.gemfile - rvm: 2.2.4 gemfile: gemfiles/rails-3.1.gemfile # rails 5.0 requires ruby 2.2.2+ - rvm: 1.9.2 gemfile: gemfiles/rails-5.0.gemfile - rvm: 1.9.3 gemfile: gemfiles/rails-5.0.gemfile - rvm: 2.0.0 gemfile: gemfiles/rails-5.0.gemfile - rvm: 2.1.8 gemfile: gemfiles/rails-5.0.gemfile # rails 5.1 requires ruby 2.2.2+ - rvm: 1.9.2 gemfile: gemfiles/rails-5.1.gemfile - rvm: 1.9.3 gemfile: gemfiles/rails-5.1.gemfile - rvm: 2.0.0 gemfile: gemfiles/rails-5.1.gemfile - rvm: 2.1.8 gemfile: gemfiles/rails-5.1.gemfile # rails 5.2 requires ruby 2.2.2+ - rvm: 1.9.2 gemfile: gemfiles/rails-5.2.gemfile - rvm: 1.9.3 gemfile: gemfiles/rails-5.2.gemfile - rvm: 2.0.0 gemfile: gemfiles/rails-5.2.gemfile - rvm: 2.1.8 gemfile: gemfiles/rails-5.2.gemfile acts_as_tree-2.8.0/test/0000755000004100000410000000000013341233424015163 5ustar www-datawww-dataacts_as_tree-2.8.0/test/acts_as_tree_test.rb0000644000004100000410000004562513341233424021217 0ustar www-datawww-datarequire 'minitest/autorun' require 'minitest/benchmark' require 'active_record' require 'acts_as_tree' class ActsAsTreeTestCase < (defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase) 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(version: 1) 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.update_attributes(: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.update_attributes(: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.8.0/README.md0000644000004100000410000001633013341233424015466 0ustar www-datawww-data# ActsAsTree [![Build Status](https://secure.travis-ci.org/amerine/acts_as_tree.svg?branch=master)](http://travis-ci.org/amerine/acts\_as\_tree) [![Gem Version](https://badge.fury.io/rb/acts_as_tree.svg)](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 * 2.8.0 - August 27, 2018 * Added support for rails 5.2, see #76, #77 -- felixbuenemann * 2.7.1 - January 30, 2018 * Fix column quoting if the `:order` option is a symbol, see #73, #74 -- felixbuenemann * 2.7.0 - September 15, 2017 * Added support for rails 5.1, see #67, #68 -- felixbuenemann, marcinwierzbicki * 2.6.1 - January 18, 2017 * Avoid conflicts of `#level` method with existing column, see #57, #58, #60 -- markhgbrewster * Fix tests on rails 4.2 with ruby < 2.1 -- felixbuenemann * 2.6.0 - October 9, 2016 * Add generations methods, see #56 -- markhgbrewster * 2.5.1 - September 8, 2016 * Fix early database connection in acts\_as\_tree, see #55 -- felixbuenemann * 2.5.0 - August 14, 2016 * Allow for use of a different primary key, see #50 -- Two9A * 2.4.0 - January 12, 2016 * Added support for rails 5.0, see #46 -- klacointe * 2.3.0 - November 6, 2015 * Added touch option to acts\_as\_tree relation. See #40 -- mbenitezm * Fix tests on rails 3.x with ruby 1.9.2. See #41 -- felixbuenemann * 2.2.0 - June 15, 2015 * Added TreeWalker.walk\_tree instance method. See #32, #37, #38 -- felixbuenemann, genewoo * Fix tests on rails 3.x. See #36 -- marshall-lee * 2.1.0 - September 25, 2014 * Added TreeWalker. See #30 -- 545ch4 * 2.0.0 - July 3, 2014 * Renamed Presentation module to TreeView, see #27, #28 -- felixbuenemann * 1.6.1 - May 29, 2014 * Readme Improvements, see #26 -- schlick * Improvements and Fixes for counter cache (fix counter\_cache: true). See #24, #25 -- dv * Cleanup and fix tests, see #24. * 1.6.0 - April 21, 2014 * Added new `leaves` method. See #23 -- MichalPokorny * 1.5.1 - March 28, 2014 * Fixing descendants modification bug. See #20 -- amerine, tmuerell * 1.5.0 - December 16, 2013 * Added new `descendants` method -- adamkleingit * Fixed warning message -- akicho8 * 1.4.0 - June 25, 2013 * `Presentation#tree_view` -- rainchen * `root?` && `leaf?` methods. -- xuanxu * 1.3.0 - March 29, 2013 * Rails 4.0 Support! -- mischa78 * Readme Fixes -- mischa78 & seanhussey * 1.2.0 - October 29, 2012 * Adding new `self_with_ancestors` accessor -- felixbuenemann * `roots` is now a scope. * 1.1.0 - April 24, 2012 * Deprecate the ActiveRecord::Acts::Tree module in favor of ActsAsTree * 1.0.1 - April 18, 2012 * Include the Railtie for easier loading in Rails. Will reassess the forced module inclusion in the future. * 1.0.0 - April 14, 2012 * Official 1.0 release. Force users to include the ActiveRecord::Acts::Tree module. * 0.2.0 - April 9, 2012 * Rails 3 Support * 0.1.1 - February 3, 2010 * Bug Fixes * 0.1.0 - October 9, 2009 * First Gem Release ## 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. Make sure the Change log in the README includes a brief summary of the versions changes, with credit to the contributors. 4. Commit these changes in one "release-prep" commit (on the master branch). 5. Push that commit up to the repo. 6. Run `rake release` This will create and push a tag to Github, then generate a gem and push it to Rubygems. 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.8.0/gemfiles/0000755000004100000410000000000013341233424015777 5ustar www-datawww-dataacts_as_tree-2.8.0/gemfiles/rails-4.0.gemfile0000644000004100000410000000014413341233424020741 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 4.0.0" gem "mime-types", "< 3" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-3.1.gemfile0000644000004100000410000000021513341233424020740 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 3.1.0" gem "i18n", "< 0.7" gem "rack-cache", "< 1.3" gem "rake", "< 11" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-3.2.gemfile0000644000004100000410000000021613341233424020742 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 3.2.22" gem "i18n", "< 0.7" gem "rack-cache", "< 1.3" gem "rake", "< 11" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-5.2.gemfile0000644000004100000410000000011413341233424020741 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 5.2.0" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-4.2.gemfile0000644000004100000410000000017413341233424020746 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 4.2.0" gem "mime-types", "< 3" gem "nokogiri", "< 1.7" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-3.0.gemfile0000644000004100000410000000016313341233424020741 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 3.0.0" gem "i18n", "< 0.7" gem "rake", "< 11" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-5.1.gemfile0000644000004100000410000000011413341233424020740 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 5.1.0" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-4.1.gemfile0000644000004100000410000000014413341233424020742 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 4.1.0" gem "mime-types", "< 3" gemspec path: "../" acts_as_tree-2.8.0/gemfiles/rails-5.0.gemfile0000644000004100000410000000011413341233424020737 0ustar www-datawww-datasource "https://rubygems.org" gem "rails", "~> 5.0.0" gemspec path: "../" acts_as_tree-2.8.0/.gitignore0000644000004100000410000000010013341233424016163 0ustar www-datawww-data*.gem .bundle pkg/* .rvmrc Gemfile.lock gemfiles/*.gemfile.lock acts_as_tree-2.8.0/Rakefile0000644000004100000410000000075113341233424015654 0ustar www-datawww-datarequire '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.8.0/lib/0000755000004100000410000000000013341233424014752 5ustar www-datawww-dataacts_as_tree-2.8.0/lib/acts_as_tree/0000755000004100000410000000000013341233424017406 5ustar www-datawww-dataacts_as_tree-2.8.0/lib/acts_as_tree/version.rb0000644000004100000410000000005213341233424021415 0ustar www-datawww-datamodule ActsAsTree VERSION = "2.8.0" end acts_as_tree-2.8.0/lib/acts_as_tree/railtie.rb0000644000004100000410000000036113341233424021364 0ustar www-datawww-datamodule 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.8.0/lib/acts_as_tree/active_record/0000755000004100000410000000000013341233424022217 5ustar www-datawww-dataacts_as_tree-2.8.0/lib/acts_as_tree/active_record/acts/0000755000004100000410000000000013341233424023151 5ustar www-datawww-dataacts_as_tree-2.8.0/lib/acts_as_tree/active_record/acts/tree.rb0000644000004100000410000000047113341233424024437 0ustar www-datawww-datarequire '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.8.0/lib/acts_as_tree.rb0000644000004100000410000002765413341233424017751 0ustar www-datawww-datarequire '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 belongs_to_opts = { class_name: name, primary_key: configuration[:primary_key], foreign_key: configuration[:foreign_key], counter_cache: configuration[:counter_cache], touch: configuration[:touch], inverse_of: :children } belongs_to_opts[:optional] = true if ActiveRecord::VERSION::MAJOR >= 5 belongs_to :parent, belongs_to_opts 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 class_eval <<-EOV include ActsAsTree::InstanceMethods def self.default_tree_order order_option = #{configuration[:order].inspect} order(order_option) end def self.root self.roots.first end def self.roots where(:#{configuration[:foreign_key]} => nil).default_tree_order end EOV # 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.8.0/acts_as_tree.gemspec0000644000004100000410000000226513341233424020212 0ustar www-datawww-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.8.0/Gemfile0000644000004100000410000000011013341233424015467 0ustar www-datawww-datasource 'https://rubygems.org' gemspec group :test do gem 'rake' end