forked from parasew/acts_as_tree
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Turned ActiveRecord::Acts::Tree into a plugin #9514 [lifolifo]
- Loading branch information
0 parents
commit ebb360a
Showing
10 changed files
with
408 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
acts_as_tree | ||
============ | ||
|
||
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 | ||
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 | ||
|
||
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
require 'rake' | ||
require 'rake/testtask' | ||
require 'rake/rdoctask' | ||
|
||
desc 'Default: run unit tests.' | ||
task :default => :test | ||
|
||
desc 'Test acts_as_tree plugin.' | ||
Rake::TestTask.new(:test) do |t| | ||
t.libs << 'lib' | ||
t.pattern = 'test/**/*_test.rb' | ||
t.verbose = true | ||
end | ||
|
||
desc 'Generate documentation for in_place_editing plugin.' | ||
Rake::RDocTask.new(:rdoc) do |rdoc| | ||
rdoc.rdoc_dir = 'rdoc' | ||
rdoc.title = 'InPlaceEditing' | ||
rdoc.options << '--line-numbers' << '--inline-source' | ||
rdoc.rdoc_files.include('README') | ||
rdoc.rdoc_files.include('lib/**/*.rb') | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require 'acts_as_tree' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
module ActsAsTree | ||
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 | ||
# 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>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+). | ||
def acts_as_tree(options = {}) | ||
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil } | ||
configuration.update(options) if options.is_a?(Hash) | ||
|
||
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache] | ||
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy | ||
|
||
class_eval <<-EOV | ||
include ActsAsTree::InstanceMethods | ||
def self.roots | ||
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) | ||
end | ||
def self.root | ||
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) | ||
end | ||
EOV | ||
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 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 | ||
end | ||
end | ||
|
||
ActiveRecord::Base.send(:include, ActsAsTree) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib') | ||
$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib') | ||
$:.unshift(File.dirname(__FILE__) + '/../lib') | ||
|
||
require 'test/unit' | ||
require 'active_support' | ||
require 'active_record' | ||
require 'active_record/fixtures' | ||
require 'acts_as_tree' | ||
|
||
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) | ||
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") | ||
ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']} | ||
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) | ||
|
||
load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb") | ||
|
||
class Test::Unit::TestCase #:nodoc: | ||
self.fixture_path = File.dirname(__FILE__) + "/fixtures/" | ||
self.use_transactional_fixtures = true | ||
self.use_instantiated_fixtures = false | ||
|
||
def create_fixtures(*table_names, &block) | ||
Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names, {}, &block) | ||
end | ||
|
||
def assert_queries(num = 1) | ||
$query_count = 0 | ||
yield | ||
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 | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
require File.join(File.dirname(__FILE__), 'abstract_unit') | ||
require File.join(File.dirname(__FILE__), 'fixtures/mixin') | ||
|
||
class TreeTest < Test::Unit::TestCase | ||
fixtures :mixins | ||
|
||
def test_children | ||
assert_equal mixins(:tree_1).children, mixins(:tree_2, :tree_4) | ||
assert_equal mixins(:tree_2).children, [mixins(:tree_3)] | ||
assert_equal mixins(:tree_3).children, [] | ||
assert_equal mixins(:tree_4).children, [] | ||
end | ||
|
||
def test_parent | ||
assert_equal mixins(:tree_2).parent, mixins(:tree_1) | ||
assert_equal mixins(:tree_2).parent, mixins(:tree_4).parent | ||
assert_nil mixins(:tree_1).parent | ||
end | ||
|
||
def test_delete | ||
assert_equal 6, TreeMixin.count | ||
mixins(:tree_1).destroy | ||
assert_equal 2, TreeMixin.count | ||
mixins(:tree2_1).destroy | ||
mixins(:tree3_1).destroy | ||
assert_equal 0, TreeMixin.count | ||
end | ||
|
||
def test_insert | ||
@extra = mixins(:tree_1).children.create | ||
|
||
assert @extra | ||
|
||
assert_equal @extra.parent, mixins(:tree_1) | ||
|
||
assert_equal 3, mixins(:tree_1).children.size | ||
assert mixins(:tree_1).children.include?(@extra) | ||
assert mixins(:tree_1).children.include?(mixins(:tree_2)) | ||
assert mixins(:tree_1).children.include?(mixins(:tree_4)) | ||
end | ||
|
||
def test_ancestors | ||
assert_equal [], mixins(:tree_1).ancestors | ||
assert_equal [mixins(:tree_1)], mixins(:tree_2).ancestors | ||
assert_equal mixins(:tree_2, :tree_1), mixins(:tree_3).ancestors | ||
assert_equal [mixins(:tree_1)], mixins(:tree_4).ancestors | ||
assert_equal [], mixins(:tree2_1).ancestors | ||
assert_equal [], mixins(:tree3_1).ancestors | ||
end | ||
|
||
def test_root | ||
assert_equal mixins(:tree_1), TreeMixin.root | ||
assert_equal mixins(:tree_1), mixins(:tree_1).root | ||
assert_equal mixins(:tree_1), mixins(:tree_2).root | ||
assert_equal mixins(:tree_1), mixins(:tree_3).root | ||
assert_equal mixins(:tree_1), mixins(:tree_4).root | ||
assert_equal mixins(:tree2_1), mixins(:tree2_1).root | ||
assert_equal mixins(:tree3_1), mixins(:tree3_1).root | ||
end | ||
|
||
def test_roots | ||
assert_equal mixins(:tree_1, :tree2_1, :tree3_1), TreeMixin.roots | ||
end | ||
|
||
def test_siblings | ||
assert_equal mixins(:tree2_1, :tree3_1), mixins(:tree_1).siblings | ||
assert_equal [mixins(:tree_4)], mixins(:tree_2).siblings | ||
assert_equal [], mixins(:tree_3).siblings | ||
assert_equal [mixins(:tree_2)], mixins(:tree_4).siblings | ||
assert_equal mixins(:tree_1, :tree3_1), mixins(:tree2_1).siblings | ||
assert_equal mixins(:tree_1, :tree2_1), mixins(:tree3_1).siblings | ||
end | ||
|
||
def test_self_and_siblings | ||
assert_equal mixins(:tree_1, :tree2_1, :tree3_1), mixins(:tree_1).self_and_siblings | ||
assert_equal mixins(:tree_2, :tree_4), mixins(:tree_2).self_and_siblings | ||
assert_equal [mixins(:tree_3)], mixins(:tree_3).self_and_siblings | ||
assert_equal mixins(:tree_2, :tree_4), mixins(:tree_4).self_and_siblings | ||
assert_equal mixins(:tree_1, :tree2_1, :tree3_1), mixins(:tree2_1).self_and_siblings | ||
assert_equal mixins(:tree_1, :tree2_1, :tree3_1), mixins(:tree3_1).self_and_siblings | ||
end | ||
end | ||
|
||
class TreeTestWithEagerLoading < Test::Unit::TestCase | ||
fixtures :mixins | ||
|
||
def test_eager_association_loading | ||
roots = TreeMixin.find(:all, :include=>"children", :conditions=>"mixins.parent_id IS NULL", :order=>"mixins.id") | ||
assert_equal mixins(:tree_1, :tree2_1, :tree3_1), 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.find(:first, :include=>{:children=>{:children=>:children}}, :order => 'mixins.id') | ||
assert_equal mixins(:recursively_cascaded_tree_4), 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.find(:first, :include=>{:first_child=>{:first_child=>:first_child}}, :order => 'mixins.id') | ||
assert_equal mixins(:recursively_cascaded_tree_4), 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.find(:first, :include=>{:parent=>{:parent=>:parent}}, :order => 'mixins.id DESC') | ||
assert_equal mixins(:recursively_cascaded_tree_1), assert_no_queries { leaf_node.parent.parent.parent } | ||
end | ||
end | ||
|
||
class TreeTestWithoutOrder < Test::Unit::TestCase | ||
fixtures :mixins | ||
|
||
def test_root | ||
assert mixins(:tree_without_order_1, :tree_without_order_2).include?(TreeMixinWithoutOrder.root) | ||
end | ||
|
||
def test_roots | ||
assert_equal [], mixins(:tree_without_order_1, :tree_without_order_2) - TreeMixinWithoutOrder.roots | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
sqlite: | ||
:adapter: sqlite | ||
:dbfile: acts_as_tree_plugin.sqlite.db | ||
sqlite3: | ||
:adapter: sqlite3 | ||
:dbfile: acts_as_tree_plugin.sqlite3.db | ||
postgresql: | ||
:adapter: postgresql | ||
:username: postgres | ||
:password: postgres | ||
:database: acts_as_tree_plugin_test | ||
:min_messages: ERROR | ||
mysql: | ||
:adapter: mysql | ||
:host: localhost | ||
:username: rails | ||
:password: | ||
:database: acts_as_tree_plugin_test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
class Mixin < ActiveRecord::Base | ||
end | ||
|
||
class TreeMixin < Mixin | ||
acts_as_tree :foreign_key => "parent_id", :order => "id" | ||
end | ||
|
||
class TreeMixinWithoutOrder < Mixin | ||
acts_as_tree :foreign_key => "parent_id" | ||
end | ||
|
||
class RecursivelyCascadedTreeMixin < Mixin | ||
acts_as_tree :foreign_key => "parent_id" | ||
has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id | ||
end |
Oops, something went wrong.