mongo_mapper_acts_as_tree 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +49 -0
- data/lib/mongo_mapper/plugins/acts_as_tree.rb +63 -0
- data/lib/mongo_mapper/plugins/version.rb +8 -0
- data/test/acts_as_tree_test.rb +202 -0
- data/test/test_helper.rb +31 -0
- metadata +98 -0
data/README.rdoc
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
= mongo_mapper_acts_as_tree
|
2
|
+
|
3
|
+
This is a port of classic Rails {acts_as_tree}[http://github.com/rails/acts_as_tree] to Mongo Mapper. Specify this MongoMapper plugin 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, which by default is called parent_id.
|
4
|
+
|
5
|
+
It has (almost) the same functionality and passes the original test-suite. Scope needs to be defined as symbol or array of symbols. It does not work for Embedded Documents. Please note, it is not yet optimized and therefore issues more queries than necessary.
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
mongo_mapper_acts_as_list is available as RubyGem:
|
10
|
+
|
11
|
+
gem install mongo_mapper_acts_as_tree
|
12
|
+
|
13
|
+
== Example
|
14
|
+
|
15
|
+
class Category
|
16
|
+
include MongoMapper::Document
|
17
|
+
|
18
|
+
plugin MongoMapper::Plugins::ActsAsTree
|
19
|
+
|
20
|
+
key :parent_id, ObjectId
|
21
|
+
acts_as_tree :order => :name
|
22
|
+
end
|
23
|
+
|
24
|
+
Example:
|
25
|
+
root
|
26
|
+
\_ child1
|
27
|
+
\_ subchild1
|
28
|
+
\_ subchild2
|
29
|
+
|
30
|
+
root = Category.create(:name => "root")
|
31
|
+
child1 = root.children.create(:name => "child1")
|
32
|
+
subchild1 = child1.children.create(:name => "subchild1")
|
33
|
+
|
34
|
+
root.parent # => nil
|
35
|
+
child1.parent # => root
|
36
|
+
root.children # => [child1]
|
37
|
+
root.children.first.children.first # => subchild1
|
38
|
+
|
39
|
+
== Note on Patches/Pull Requests
|
40
|
+
|
41
|
+
* Fork the project.
|
42
|
+
* Make your feature addition or bug fix.
|
43
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
44
|
+
* 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 I can ignore when I pull)
|
45
|
+
* Send me a pull request. Bonus points for topic branches.
|
46
|
+
|
47
|
+
== Copyright
|
48
|
+
|
49
|
+
Original Rails acts_as_tree Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
module Plugins
|
3
|
+
module ActsAsTree
|
4
|
+
|
5
|
+
require 'mongo_mapper'
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def acts_as_tree(options = {})
|
9
|
+
configuration = { :foreign_key => :parent_id, :order => nil, :counter_cache => nil }
|
10
|
+
configuration.update(options) if options.is_a?(Hash)
|
11
|
+
|
12
|
+
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
|
13
|
+
many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
|
14
|
+
|
15
|
+
class_eval <<-EOV
|
16
|
+
include MongoMapper::Plugins::ActsAsTree::InstanceMethods
|
17
|
+
|
18
|
+
def self.roots
|
19
|
+
where("#{configuration[:foreign_key]}".to_sym => nil).sort("#{configuration[:order]}").all
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.root
|
23
|
+
where("#{configuration[:foreign_key]}".to_sym => nil).sort("#{configuration[:order]}").first
|
24
|
+
end
|
25
|
+
EOV
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
# Returns list of ancestors, starting from parent until root.
|
31
|
+
#
|
32
|
+
# subchild1.ancestors # => [child1, root]
|
33
|
+
def ancestors
|
34
|
+
node, nodes = self, []
|
35
|
+
nodes << node = node.parent while node.parent?
|
36
|
+
nodes
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the root node of the tree.
|
40
|
+
def root
|
41
|
+
node = self
|
42
|
+
node = node.parent while node.parent?
|
43
|
+
node
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns all siblings of the current node.
|
47
|
+
#
|
48
|
+
# subchild1.siblings # => [subchild2]
|
49
|
+
def siblings
|
50
|
+
self_and_siblings - [self]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns all siblings and a reference to the current node.
|
54
|
+
#
|
55
|
+
# subchild1.self_and_siblings # => [subchild1, subchild2]
|
56
|
+
def self_and_siblings
|
57
|
+
parent? ? parent.children : self.class.roots
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
# SETUP TEST
|
6
|
+
|
7
|
+
# class ActiveSupport::TestCase
|
8
|
+
# def assert_queries(num = 1)
|
9
|
+
# $query_count = 0
|
10
|
+
# yield
|
11
|
+
# ensure
|
12
|
+
# assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def assert_no_queries(&block)
|
16
|
+
# assert_queries(0, &block)
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
# SETUP CLASSES
|
23
|
+
|
24
|
+
class Mixin
|
25
|
+
include MongoMapper::Document
|
26
|
+
plugin MongoMapper::Plugins::ActsAsTree
|
27
|
+
key :parent_id, ObjectId
|
28
|
+
end
|
29
|
+
|
30
|
+
class TreeMixin < Mixin
|
31
|
+
acts_as_tree :foreign_key => :parent_id, :order => :id
|
32
|
+
end
|
33
|
+
|
34
|
+
class TreeMixinWithoutOrder < Mixin
|
35
|
+
acts_as_tree :foreign_key => :parent_id
|
36
|
+
end
|
37
|
+
|
38
|
+
class RecursivelyCascadedTreeMixin < Mixin
|
39
|
+
acts_as_tree :foreign_key => :parent_id
|
40
|
+
has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
# TESTS
|
46
|
+
|
47
|
+
class TreeTest < ActiveSupport::TestCase
|
48
|
+
|
49
|
+
def setup
|
50
|
+
@root1 = TreeMixin.create!
|
51
|
+
@root_child1 = TreeMixin.create! :parent_id => @root1.id
|
52
|
+
@child1_child = TreeMixin.create! :parent_id => @root_child1.id
|
53
|
+
@root_child2 = TreeMixin.create! :parent_id => @root1.id
|
54
|
+
@root2 = TreeMixin.create!
|
55
|
+
@root3 = TreeMixin.create!
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_children
|
59
|
+
assert_equal @root1.children, [@root_child1, @root_child2]
|
60
|
+
assert_equal @root_child1.children, [@child1_child]
|
61
|
+
assert_equal @child1_child.children, []
|
62
|
+
assert_equal @root_child2.children, []
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def test_parent
|
67
|
+
assert_equal @root_child1.parent, @root1
|
68
|
+
assert_equal @root_child1.parent, @root_child2.parent
|
69
|
+
assert @root1.parent.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_delete
|
73
|
+
assert_equal 6, TreeMixin.count
|
74
|
+
@root1.destroy
|
75
|
+
assert_equal 2, TreeMixin.count
|
76
|
+
@root2.destroy
|
77
|
+
@root3.destroy
|
78
|
+
assert_equal 0, TreeMixin.count
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_insert
|
82
|
+
@extra = @root1.children.create
|
83
|
+
|
84
|
+
assert @extra
|
85
|
+
|
86
|
+
assert_equal @extra.parent, @root1
|
87
|
+
|
88
|
+
assert_equal 3, @root1.children.size
|
89
|
+
assert @root1.children.include?(@extra)
|
90
|
+
assert @root1.children.include?(@root_child1)
|
91
|
+
assert @root1.children.include?(@root_child2)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_ancestors
|
95
|
+
assert_equal [], @root1.ancestors
|
96
|
+
assert_equal [@root1], @root_child1.ancestors
|
97
|
+
assert_equal [@root_child1, @root1], @child1_child.ancestors
|
98
|
+
assert_equal [@root1], @root_child2.ancestors
|
99
|
+
assert_equal [], @root2.ancestors
|
100
|
+
assert_equal [], @root3.ancestors
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_root
|
104
|
+
assert_equal @root1, TreeMixin.root
|
105
|
+
assert_equal @root1, @root1.root
|
106
|
+
assert_equal @root1, @root_child1.root
|
107
|
+
assert_equal @root1, @child1_child.root
|
108
|
+
assert_equal @root1, @root_child2.root
|
109
|
+
assert_equal @root2, @root2.root
|
110
|
+
assert_equal @root3, @root3.root
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_roots
|
114
|
+
assert_equal [@root1, @root2, @root3], TreeMixin.roots
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_siblings
|
118
|
+
assert_equal [@root2, @root3], @root1.siblings
|
119
|
+
assert_equal [@root_child2], @root_child1.siblings
|
120
|
+
assert_equal [], @child1_child.siblings
|
121
|
+
assert_equal [@root_child1], @root_child2.siblings
|
122
|
+
assert_equal [@root1, @root3], @root2.siblings
|
123
|
+
assert_equal [@root1, @root2], @root3.siblings
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_self_and_siblings
|
127
|
+
assert_equal [@root1, @root2, @root3], @root1.self_and_siblings
|
128
|
+
assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings
|
129
|
+
assert_equal [@child1_child], @child1_child.self_and_siblings
|
130
|
+
assert_equal [@root_child1, @root_child2], @root_child2.self_and_siblings
|
131
|
+
assert_equal [@root1, @root2, @root3], @root2.self_and_siblings
|
132
|
+
assert_equal [@root1, @root2, @root3], @root3.self_and_siblings
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# SKIP THIS AS THERE IS NO EAGER LOADING IN MONGOMAPPER
|
138
|
+
|
139
|
+
# class TreeTestWithEagerLoading < Test::Unit::TestCase
|
140
|
+
#
|
141
|
+
# def setup
|
142
|
+
# teardown_db
|
143
|
+
# setup_db
|
144
|
+
# @root1 = TreeMixin.create!
|
145
|
+
# @root_child1 = TreeMixin.create! :parent_id => @root1.id
|
146
|
+
# @child1_child = TreeMixin.create! :parent_id => @root_child1.id
|
147
|
+
# @root_child2 = TreeMixin.create! :parent_id => @root1.id
|
148
|
+
# @root2 = TreeMixin.create!
|
149
|
+
# @root3 = TreeMixin.create!
|
150
|
+
#
|
151
|
+
# @rc1 = RecursivelyCascadedTreeMixin.create!
|
152
|
+
# @rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id
|
153
|
+
# @rc3 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc2.id
|
154
|
+
# @rc4 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc3.id
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# def test_eager_association_loading
|
158
|
+
# roots = TreeMixin.find(:all, :include => :children, :conditions => "mixins.parent_id IS NULL", :order => "mixins.id")
|
159
|
+
# assert_equal [@root1, @root2, @root3], roots
|
160
|
+
# assert_no_queries do
|
161
|
+
# assert_equal 2, roots[0].children.size
|
162
|
+
# assert_equal 0, roots[1].children.size
|
163
|
+
# assert_equal 0, roots[2].children.size
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
|
168
|
+
# root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :children => { :children => :children } }, :order => 'mixins.id')
|
169
|
+
# assert_equal @rc4, assert_no_queries { root_node.children.first.children.first.children.first }
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
|
173
|
+
# root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :first_child => { :first_child => :first_child } }, :order => 'mixins.id')
|
174
|
+
# assert_equal @rc4, assert_no_queries { root_node.first_child.first_child.first_child }
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
|
178
|
+
# leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :parent => { :parent => :parent } }, :order => 'mixins.id DESC')
|
179
|
+
# assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent }
|
180
|
+
# end
|
181
|
+
# end
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
class TreeTestWithoutOrder < ActiveSupport::TestCase
|
189
|
+
|
190
|
+
def setup
|
191
|
+
@root1 = TreeMixinWithoutOrder.create!
|
192
|
+
@root2 = TreeMixinWithoutOrder.create!
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_root
|
196
|
+
assert [@root1, @root2].include?(TreeMixinWithoutOrder.root)
|
197
|
+
end
|
198
|
+
|
199
|
+
def test_roots
|
200
|
+
assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots
|
201
|
+
end
|
202
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
3
|
+
|
4
|
+
require 'mongo_mapper'
|
5
|
+
require 'mongo_mapper/plugins/acts_as_tree'
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
class ActiveSupport::TestCase
|
11
|
+
|
12
|
+
# Drop all collections after each test case.
|
13
|
+
def teardown
|
14
|
+
MongoMapper.database.collections.each { |coll| coll.remove }
|
15
|
+
end
|
16
|
+
|
17
|
+
# Make sure that each test case has a teardown
|
18
|
+
# method to clear the db after each test.
|
19
|
+
def inherited(base)
|
20
|
+
base.define_method teardown do
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017)
|
30
|
+
MongoMapper.database = "mongo_mapper_acts_as_tree_test"
|
31
|
+
MongoMapper.database.collections.each { |c| c.drop_indexes }
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mongo_mapper_acts_as_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Tomas Celizna
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-31 01:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: mongo_mapper
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rake
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
description:
|
49
|
+
email:
|
50
|
+
- tomas.celizna@gmail.com
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files: []
|
56
|
+
|
57
|
+
files:
|
58
|
+
- lib/mongo_mapper/plugins/acts_as_tree.rb
|
59
|
+
- lib/mongo_mapper/plugins/version.rb
|
60
|
+
- test/acts_as_tree_test.rb
|
61
|
+
- test/test_helper.rb
|
62
|
+
- README.rdoc
|
63
|
+
has_rdoc: true
|
64
|
+
homepage: http://github.com/tomasc/mongo_mapper_acts_as_tree
|
65
|
+
licenses: []
|
66
|
+
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 3
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 3
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
requirements: []
|
91
|
+
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 1.3.7
|
94
|
+
signing_key:
|
95
|
+
specification_version: 3
|
96
|
+
summary: Port of classic Rails ActsAsTree for MongoMapper
|
97
|
+
test_files: []
|
98
|
+
|