mongoid_tree 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|