mongoid_tree 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/features/modify_tree.feature +46 -0
- data/features/step_definitions/mongoid_tree_steps.rb +36 -20
- data/features/traversal.feature +81 -57
- data/lib/mongoid_tree.rb +61 -19
- data/mongoid_tree.gemspec +4 -3
- data/spec/mongoid_tree_spec.rb +13 -0
- metadata +5 -4
data/Rakefile
CHANGED
@@ -8,7 +8,7 @@ begin
|
|
8
8
|
gem.summary = %Q{Materialized paths based tree implementation for Mongoid}
|
9
9
|
gem.description = %Q{Fully featured tree implementation for Mongoid using materialized paths and relative associations. Featuring Depth and Breadth first search.}
|
10
10
|
gem.email = "rkuhn@littleweblab.com"
|
11
|
-
gem.homepage = "http://github.com/
|
11
|
+
gem.homepage = "http://github.com/ticktricktrack/mongoid_tree"
|
12
12
|
gem.authors = ["Rainer Kuhn"]
|
13
13
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
14
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
@@ -0,0 +1,46 @@
|
|
1
|
+
Feature: Modify tree
|
2
|
+
In order make changes to a tree
|
3
|
+
As a tree hugger
|
4
|
+
I want move and delete branches
|
5
|
+
|
6
|
+
Scenario: Move Subtree
|
7
|
+
Given I have no nodes
|
8
|
+
And I create a tree
|
9
|
+
#
|
10
|
+
# http://en.wikipedia.org/wiki/File:Depth-first-tree.svg
|
11
|
+
# Node 9 and subtree are moved to Node 6
|
12
|
+
When I move a subtree
|
13
|
+
And I request the children depth first
|
14
|
+
Then I should get the children in the following order
|
15
|
+
| Node_1 |
|
16
|
+
| Node_2 |
|
17
|
+
| Node_3 |
|
18
|
+
| Node_4 |
|
19
|
+
| Node_5 |
|
20
|
+
| Node_6 |
|
21
|
+
| Node_9 |
|
22
|
+
| Node_10 |
|
23
|
+
| Node_11 |
|
24
|
+
| Node_7 |
|
25
|
+
| Node_8 |
|
26
|
+
| Node_12 |
|
27
|
+
|
28
|
+
Scenario: Delete Subtree
|
29
|
+
Given I have no nodes
|
30
|
+
And I create a tree
|
31
|
+
#
|
32
|
+
# http://en.wikipedia.org/wiki/File:Depth-first-tree.svg
|
33
|
+
# Node 9 and subtree are deleted
|
34
|
+
When I delete a subtree
|
35
|
+
And I request the children depth first
|
36
|
+
Then I should get the children in the following order
|
37
|
+
| Node_1 |
|
38
|
+
| Node_2 |
|
39
|
+
| Node_3 |
|
40
|
+
| Node_4 |
|
41
|
+
| Node_5 |
|
42
|
+
| Node_6 |
|
43
|
+
| Node_7 |
|
44
|
+
| Node_8 |
|
45
|
+
| Node_12 |
|
46
|
+
And I should have 9 Nodes
|
@@ -1,25 +1,25 @@
|
|
1
1
|
Given /^I have no nodes$/ do
|
2
|
-
|
2
|
+
Node.delete_all
|
3
3
|
end
|
4
4
|
|
5
5
|
Given /^the following nodes exist:$/ do |nodes_table|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
6
|
+
# table is a Cucumber::Ast::Table
|
7
|
+
nodes_table.hashes.each do |hash|
|
8
|
+
node = Node.new(:name => hash[:name])
|
9
|
+
if !(hash[:parent] == "")
|
10
|
+
parent = Node.find(:first, :conditions => { :name => hash[:parent]})
|
11
|
+
parent.children << node
|
12
|
+
parent.save!
|
13
|
+
node.save!
|
14
|
+
else
|
15
|
+
Node.create( :name => hash[:name])
|
16
|
+
end
|
16
17
|
end
|
17
|
-
end
|
18
18
|
end
|
19
19
|
|
20
20
|
Given /^I create a tree$/ do
|
21
|
-
12.times{ Factory.create(:node) }
|
22
|
-
|
21
|
+
12.times{ |i| Factory.create(:node, :name => "Node_#{i+1}") }
|
22
|
+
|
23
23
|
Node.where(:name => "Node_1").first.children << Node.where(:name => "Node_7").first
|
24
24
|
Node.where(:name => "Node_7").first.insert_before(Node.where(:name => "Node_2").first)
|
25
25
|
Node.where(:name => "Node_1").first.children << Node.where(:name => "Node_8").first
|
@@ -35,16 +35,32 @@ end
|
|
35
35
|
|
36
36
|
|
37
37
|
When /^I request the children depth first$/ do
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
@root = Node.find(:first, :conditions => { :name => "Node_1" })
|
39
|
+
@root.should_not be(nil)
|
40
|
+
@children = @root.depth_first.map{|node| [node.name]}
|
41
|
+
puts @children.inspect
|
42
|
+
end
|
43
|
+
|
44
|
+
When /^I request the children breadth first$/ do
|
45
|
+
@root = Node.find(:first, :conditions => { :name => "Node_1" })
|
46
|
+
@root.should_not be(nil)
|
47
|
+
@children = @root.breadth_first.map{|node| [node.name]}
|
48
|
+
end
|
49
|
+
|
50
|
+
When /^I move a subtree$/ do
|
51
|
+
Node.first(:conditions => {:name => "Node_9"}).move_to(Node.first(:conditions => {:name => "Node_6"}))
|
52
|
+
end
|
53
|
+
|
54
|
+
When /^I delete a subtree$/ do
|
55
|
+
Node.first(:conditions => {:name => "Node_9"}).destroy
|
41
56
|
end
|
42
57
|
|
58
|
+
|
43
59
|
Then /^I should get the children in the following order$/ do |expected_children_order|
|
44
|
-
|
45
|
-
|
60
|
+
# table is a Cucumber::Ast::Table
|
61
|
+
expected_children_order.diff!(@children)
|
46
62
|
end
|
47
63
|
|
48
64
|
Then /^I should have (\d+) Nodes$/ do |count|
|
49
|
-
|
65
|
+
Node.count.should be(count.to_i)
|
50
66
|
end
|
data/features/traversal.feature
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Feature: Tree Searches / Traversals
|
2
2
|
In order to traverse
|
3
|
-
As a
|
3
|
+
As a developer
|
4
4
|
I want a depth-first representation of the tree
|
5
5
|
|
6
6
|
Scenario: Depth First Search
|
@@ -22,6 +22,41 @@ Feature: Tree Searches / Traversals
|
|
22
22
|
| Node_11 | Node_9 |
|
23
23
|
And I should have 12 Nodes
|
24
24
|
When I request the children depth first
|
25
|
+
Then I should get the children in the following order
|
26
|
+
| Node_1 |
|
27
|
+
| Node_2 |
|
28
|
+
| Node_3 |
|
29
|
+
| Node_4 |
|
30
|
+
| Node_5 |
|
31
|
+
| Node_6 |
|
32
|
+
| Node_7 |
|
33
|
+
| Node_8 |
|
34
|
+
| Node_9 |
|
35
|
+
| Node_10 |
|
36
|
+
| Node_11 |
|
37
|
+
| Node_12 |
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
Scenario: Depth First Search with other create Order
|
42
|
+
# For a visual representation see http://en.wikipedia.org/wiki/File:Depth-first-tree.svg
|
43
|
+
Given I have no nodes
|
44
|
+
And the following nodes exist:
|
45
|
+
| name | parent |
|
46
|
+
| Node_1 | |
|
47
|
+
| Node_2 | Node_1 |
|
48
|
+
| Node_7 | Node_1 |
|
49
|
+
| Node_8 | Node_1 |
|
50
|
+
| Node_9 | Node_8 |
|
51
|
+
| Node_10 | Node_9 |
|
52
|
+
| Node_11 | Node_9 |
|
53
|
+
| Node_3 | Node_2 |
|
54
|
+
| Node_6 | Node_2 |
|
55
|
+
| Node_12 | Node_8 |
|
56
|
+
| Node_4 | Node_3 |
|
57
|
+
| Node_5 | Node_3 |
|
58
|
+
And I should have 12 Nodes
|
59
|
+
When I request the children depth first
|
25
60
|
Then I should get the children in the following order
|
26
61
|
|Node_1|
|
27
62
|
|Node_2|
|
@@ -35,59 +70,48 @@ Feature: Tree Searches / Traversals
|
|
35
70
|
|Node_10|
|
36
71
|
|Node_11|
|
37
72
|
|Node_12|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|Node_2|
|
84
|
-
|Node_3|
|
85
|
-
|Node_4|
|
86
|
-
|Node_5|
|
87
|
-
|Node_6|
|
88
|
-
|Node_7|
|
89
|
-
|Node_8|
|
90
|
-
|Node_9|
|
91
|
-
|Node_10|
|
92
|
-
|Node_11|
|
93
|
-
|Node_12|
|
73
|
+
|
74
|
+
# This scenario creates a tree using ".children << node", insert_before(node), insert_after(node)
|
75
|
+
# Details in the "And i have a tree" step
|
76
|
+
Scenario: Depth First Search with insert
|
77
|
+
# For a visual representation see http://en.wikipedia.org/wiki/File:Depth-first-tree.svg
|
78
|
+
Given I have no nodes
|
79
|
+
And I create a tree
|
80
|
+
And I should have 12 Nodes
|
81
|
+
When I request the children depth first
|
82
|
+
Then I should get the children in the following order
|
83
|
+
| Node_1 |
|
84
|
+
| Node_2 |
|
85
|
+
| Node_3 |
|
86
|
+
| Node_4 |
|
87
|
+
| Node_5 |
|
88
|
+
| Node_6 |
|
89
|
+
| Node_7 |
|
90
|
+
| Node_8 |
|
91
|
+
| Node_9 |
|
92
|
+
| Node_10 |
|
93
|
+
| Node_11 |
|
94
|
+
| Node_12 |
|
95
|
+
|
96
|
+
# Same tree as in the Depth-First-Examples
|
97
|
+
# For a visual representation see http://en.wikipedia.org/wiki/File:Depth-first-tree.svg
|
98
|
+
# More info on BFS
|
99
|
+
# http://en.wikipedia.org/wiki/Breadth-first_search
|
100
|
+
# http://www.cs.bu.edu/teaching/cs112/spring-2000/breadth-first/
|
101
|
+
Scenario: Bread First Search
|
102
|
+
Given I have no nodes
|
103
|
+
And I create a tree
|
104
|
+
When I request the children breadth first
|
105
|
+
Then I should get the children in the following order
|
106
|
+
| Node_1 |
|
107
|
+
| Node_2 |
|
108
|
+
| Node_7 |
|
109
|
+
| Node_8 |
|
110
|
+
| Node_3 |
|
111
|
+
| Node_6 |
|
112
|
+
| Node_9 |
|
113
|
+
| Node_12 |
|
114
|
+
| Node_4 |
|
115
|
+
| Node_5 |
|
116
|
+
| Node_10 |
|
117
|
+
| Node_11 |
|
data/lib/mongoid_tree.rb
CHANGED
@@ -3,9 +3,13 @@ module Mongoid
|
|
3
3
|
module Tree
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
include Comparable
|
6
|
-
|
6
|
+
|
7
7
|
included do
|
8
|
-
references_many :children,
|
8
|
+
references_many :children,
|
9
|
+
:class_name => self.name,
|
10
|
+
:stored_as => :array,
|
11
|
+
:inverse_of => :parents,
|
12
|
+
:dependent => :destroy do
|
9
13
|
def <<(*objects)
|
10
14
|
objects.flatten.each_with_index do |object, index|
|
11
15
|
reverse_key = reverse_key(object)
|
@@ -18,13 +22,15 @@ module Mongoid
|
|
18
22
|
object.send(reverse_key).concat(@parent.send(reverse_key))
|
19
23
|
end
|
20
24
|
super(objects)
|
21
|
-
|
25
|
+
|
22
26
|
end
|
23
27
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
|
29
|
+
references_many :parents,
|
30
|
+
:class_name => self.name,
|
31
|
+
:stored_as => :array,
|
32
|
+
:inverse_of => :children
|
33
|
+
|
28
34
|
# This stores the position in the children array of the parent object.
|
29
35
|
# Makes it easier to flatten / export / import a tree
|
30
36
|
field :position, :type => Integer
|
@@ -32,34 +38,49 @@ module Mongoid
|
|
32
38
|
end
|
33
39
|
|
34
40
|
module InstanceMethods
|
35
|
-
|
41
|
+
|
36
42
|
def parent
|
37
43
|
self.parents.last
|
38
44
|
end
|
39
|
-
|
45
|
+
|
40
46
|
def depth
|
41
47
|
self.parents.count
|
42
48
|
end
|
43
|
-
|
49
|
+
|
44
50
|
#Comparable
|
45
51
|
def <=> (another_node)
|
46
52
|
self.position <=> another_node.position
|
47
53
|
end
|
48
|
-
|
49
|
-
|
50
|
-
# TODO change this into a Mongoid Query
|
54
|
+
|
55
|
+
# Returns the whole subtree including itself as array
|
51
56
|
def depth_first
|
52
57
|
result = [self]
|
53
|
-
if
|
58
|
+
if self.child_ids.empty?
|
54
59
|
return result
|
55
60
|
else
|
56
|
-
self.children.each do |child|
|
61
|
+
self.children.sort.each do |child|
|
57
62
|
result += child.depth_first
|
58
63
|
end
|
59
64
|
end
|
60
65
|
return result
|
61
66
|
end
|
67
|
+
alias :dfs :depth_first
|
62
68
|
|
69
|
+
# Returns the whole subtree including itself as array
|
70
|
+
def breadth_first
|
71
|
+
result = []
|
72
|
+
queue = [self]
|
73
|
+
while !queue.empty?
|
74
|
+
node = queue.shift
|
75
|
+
result << node
|
76
|
+
node.children.sort.each do |child|
|
77
|
+
queue << child
|
78
|
+
end
|
79
|
+
end
|
80
|
+
return result
|
81
|
+
end
|
82
|
+
alias :bfs :breadth_first
|
83
|
+
|
63
84
|
def insert_before( new_child )
|
64
85
|
new_child.position = self.position
|
65
86
|
self.parent.children.each do |child|
|
@@ -79,10 +100,31 @@ module Mongoid
|
|
79
100
|
end
|
80
101
|
self.parent.children << new_child
|
81
102
|
end
|
103
|
+
|
104
|
+
def move_to(target_node)
|
105
|
+
# unhinge - I was getting a nil on another implementation, so this is a bit longer but works
|
106
|
+
child_ids_array = self.parent.child_ids.clone
|
107
|
+
child_ids_array.delete(self.id)
|
108
|
+
parent.update_attributes(:child_ids => child_ids_array )
|
109
|
+
self.update_attributes(:parent_ids => [])
|
110
|
+
# and append
|
111
|
+
target_node.children << self
|
112
|
+
# recurse through subtree
|
113
|
+
self.rebuild_paths
|
114
|
+
end
|
115
|
+
|
116
|
+
def rebuild_paths
|
117
|
+
self.update_path
|
118
|
+
self.children.each do |child|
|
119
|
+
child.rebuild_paths
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def update_path
|
124
|
+
self.update_attributes(:parent_ids => self.parent.parent_ids + [self.parent.id])
|
125
|
+
end
|
126
|
+
|
82
127
|
end
|
83
|
-
|
84
|
-
|
85
|
-
|
86
128
|
end
|
87
129
|
end
|
88
|
-
end
|
130
|
+
end
|
data/mongoid_tree.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{mongoid_tree}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Rainer Kuhn"]
|
12
|
-
s.date = %q{2010-08-
|
12
|
+
s.date = %q{2010-08-13}
|
13
13
|
s.description = %q{Fully featured tree implementation for Mongoid using materialized paths and relative associations. Featuring Depth and Breadth first search.}
|
14
14
|
s.email = %q{rkuhn@littleweblab.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -45,6 +45,7 @@ Gem::Specification.new do |s|
|
|
45
45
|
"doc/js/jquery.js",
|
46
46
|
"doc/method_list.html",
|
47
47
|
"doc/top-level-namespace.html",
|
48
|
+
"features/modify_tree.feature",
|
48
49
|
"features/step_definitions/mongoid_tree_steps.rb",
|
49
50
|
"features/support/env.rb",
|
50
51
|
"features/traversal.feature",
|
@@ -55,7 +56,7 @@ Gem::Specification.new do |s|
|
|
55
56
|
"spec/mongoid_tree_spec.rb",
|
56
57
|
"spec/spec_helper.rb"
|
57
58
|
]
|
58
|
-
s.homepage = %q{http://github.com/
|
59
|
+
s.homepage = %q{http://github.com/ticktricktrack/mongoid_tree}
|
59
60
|
s.rdoc_options = ["--charset=UTF-8"]
|
60
61
|
s.require_paths = ["lib"]
|
61
62
|
s.rubygems_version = %q{1.3.7}
|
data/spec/mongoid_tree_spec.rb
CHANGED
@@ -122,6 +122,19 @@ describe "MongoidTree" do
|
|
122
122
|
getNode(5).parent_ids.should eql([getNode(1).id, getNode(2).id, getNode(3).id])
|
123
123
|
getNode(10).parent_ids.should eql([getNode(1).id, getNode(8).id, getNode(9).id])
|
124
124
|
end
|
125
|
+
|
126
|
+
context "Moving a subtree" do
|
127
|
+
it "should rebuild the paths" do
|
128
|
+
getNode(9).move_to(getNode(6))
|
129
|
+
getNode(2).child_ids.should eql([getNode(3).id, getNode(6).id])
|
130
|
+
getNode(8).child_ids.should eql([getNode(12).id])
|
131
|
+
|
132
|
+
|
133
|
+
getNode(9).parent_ids.should eql([getNode(1).id, getNode(2).id, getNode(6).id])
|
134
|
+
getNode(10).parent_ids.should eql([getNode(1).id, getNode(2).id, getNode(6).id, getNode(9).id])
|
135
|
+
getNode(11).parent_ids.should eql([getNode(1).id, getNode(2).id, getNode(6).id, getNode(9).id])
|
136
|
+
end
|
137
|
+
end
|
125
138
|
|
126
139
|
end
|
127
140
|
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 3
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.3.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Rainer Kuhn
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-08-
|
17
|
+
date: 2010-08-13 00:00:00 +02:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- doc/js/jquery.js
|
57
57
|
- doc/method_list.html
|
58
58
|
- doc/top-level-namespace.html
|
59
|
+
- features/modify_tree.feature
|
59
60
|
- features/step_definitions/mongoid_tree_steps.rb
|
60
61
|
- features/support/env.rb
|
61
62
|
- features/traversal.feature
|
@@ -66,7 +67,7 @@ files:
|
|
66
67
|
- spec/mongoid_tree_spec.rb
|
67
68
|
- spec/spec_helper.rb
|
68
69
|
has_rdoc: true
|
69
|
-
homepage: http://github.com/
|
70
|
+
homepage: http://github.com/ticktricktrack/mongoid_tree
|
70
71
|
licenses: []
|
71
72
|
|
72
73
|
post_install_message:
|