path_tree 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,11 @@
1
+ = PathTree
2
+
3
+ This gem provides support for creating tree data structures. The structure of the tree is defined with dot delimited paths on each node. This has a couple of advantages over the +acts_as_tree+ plugin.
4
+
5
+ 1. Each node gets a unique character identifier that has semantic qualities and indicates the structure in the tree.
6
+
7
+ 2. Queries for all ancestors or all descendants are far more efficient.
8
+
9
+ 3. Out of the box the code works with ActiveRecord, but it can easily be made to work with other ORM's if they implement just a few methods.
10
+
11
+ See PathTree for more details.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ desc 'Default: run unit tests'
5
+ task :default => :test
6
+
7
+ begin
8
+ require 'rspec'
9
+ require 'rspec/core/rake_task'
10
+ desc 'Run the unit tests'
11
+ RSpec::Core::RakeTask.new(:test)
12
+ rescue LoadError
13
+ task :test do
14
+ raise "You must have rspec 2.0 installed to run the tests"
15
+ end
16
+ end
17
+
18
+ begin
19
+ require 'jeweler'
20
+ Jeweler::Tasks.new do |gem|
21
+ gem.name = "path_tree"
22
+ gem.summary = %Q{Helper module for constructing tree data structures}
23
+ gem.description = %Q{Module that defines a tree data structure based on a path.}
24
+ gem.authors = ["Brian Durand"]
25
+ gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com"]
26
+ gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "License.txt"].to_a
27
+ gem.has_rdoc = true
28
+ gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
29
+ gem.extra_rdoc_files = ["README.rdoc"]
30
+ gem.add_dependency('activerecord')
31
+ gem.add_development_dependency('rspec', '>= 2.0.0')
32
+ gem.add_development_dependency('sqlite3')
33
+ gem.add_development_dependency('activerecord')
34
+ end
35
+ Jeweler::RubygemsDotOrgTasks.new
36
+ rescue LoadError
37
+ end
data/lib/path_tree.rb ADDED
@@ -0,0 +1,248 @@
1
+ # This module implements a tree structure by using a convention of converting a name into a path.
2
+ # Paths created by normalizing a name attribute and then separating levels with periods with
3
+ # the lowest level coming last.
4
+ #
5
+ # In order to use this module, the model must respond to the +first+ and +all+ methods like ActiveRecord,
6
+ # have support for after_destroy and after_save callbacks, validates_* macros and include attributes
7
+ # for name, node_path, path, and parent_path.
8
+ module PathTree
9
+ if RUBY_VERSION.match(/^1\.8/)
10
+ require File.expand_path("../ruby_18_patterns.rb", __FILE__)
11
+ else
12
+ require File.expand_path("../ruby_19_patterns.rb", __FILE__)
13
+ end
14
+ include Patterns
15
+
16
+ def self.included (base)
17
+ base.extend(ClassMethods)
18
+
19
+ base.validates_uniqueness_of :path
20
+ base.validates_uniqueness_of :node_path, :scope => :parent_path
21
+ base.validates_presence_of :name, :node_path, :path
22
+
23
+ base.after_save do |record|
24
+ if record.path_changed? and !record.path_was.nil?
25
+ record.children.each do |child|
26
+ child.update_attributes(:parent_path => record.path)
27
+ end
28
+ end
29
+ record.instance_variable_set(:@children, nil)
30
+ end
31
+
32
+ base.after_destroy do |record|
33
+ record.children.each do |child|
34
+ child.update_attributes(:parent_path => record.parent_path)
35
+ end
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ include Patterns
41
+
42
+ NON_WORD_PATTERN = /[^a-z0-9_]+/.freeze
43
+ DASH_AT_START_PATTERN = /^-+/.freeze
44
+ DASH_AT_END_PATTERN = /-+$/.freeze
45
+
46
+ # Get all the root nodes (i.e. those without any parents)
47
+ def roots
48
+ all(:conditions => {:parent_path => nil})
49
+ end
50
+
51
+ # Set the path delimiter (default is '.').
52
+ def path_delimiter= (char)
53
+ @path_delimiter = char
54
+ end
55
+
56
+ def path_delimiter
57
+ @path_delimiter ||= '.'
58
+ end
59
+
60
+ # Load the entire branch of the tree under path at once. If you will be traversing the
61
+ # tree, this is the fastest way to load it. Returns the root node of the branch.
62
+ def branch (path)
63
+ raise ArgumentError.new("branch path must not be blank") if path.blank?
64
+ root = first(:conditions => {:path => path})
65
+ return [] unless root
66
+ nodes = path_like(path).sort{|a,b| b.path <=> a.path}
67
+ nodes << root
68
+ return populate_tree_structure!(nodes.pop, nodes)
69
+ end
70
+
71
+ # Translate a value into a valid path part. By default this will translate it into an ascii
72
+ # lower case value with words delimited by dashes. Implementations can override this logic.
73
+ def pathify (value)
74
+ if value
75
+ asciify(unquote(value)).strip.downcase.gsub(NON_WORD_PATTERN, '-').gsub(DASH_AT_START_PATTERN, '').gsub(DASH_AT_END_PATTERN, '')
76
+ end
77
+ end
78
+
79
+ # Replace accented characters with the closest ascii equivalent
80
+ def asciify (value)
81
+ if value
82
+ value.gsub(UPPER_A_PATTERN, 'A').gsub(LOWER_A_PATTERN, 'a').
83
+ gsub(UPPER_E_PATTERN, 'E').gsub(LOWER_E_PATTERN, 'e').
84
+ gsub(UPPER_I_PATTERN, 'I').gsub(LOWER_I_PATTERN, 'i').
85
+ gsub(UPPER_O_PATTERN, 'O').gsub(LOWER_O_PATTERN, 'o').
86
+ gsub(UPPER_U_PATTERN, 'U').gsub(LOWER_U_PATTERN, 'u').
87
+ gsub(UPPER_Y_PATTERN, 'Y').gsub(LOWER_Y_PATTERN, 'y').
88
+ gsub(UPPER_N_PATTERN, 'N').gsub(LOWER_N_PATTERN, 'n').
89
+ gsub(UPPER_C_PATTERN, 'C').gsub(LOWER_C_PATTERN, 'c').
90
+ gsub(UPPER_AE_PATTERN, 'AE').gsub(LOWER_AE_PATTERN, 'ae').
91
+ gsub(SS_PATTERN, 'ss').gsub(UPPER_D_PATTERN, 'D')
92
+ end
93
+ end
94
+
95
+ # Remove quotation marks from a string.
96
+ def unquote (value)
97
+ value.gsub(/['"]/, '') if value
98
+ end
99
+
100
+ # Abstract way of finding paths that start with a value so it can be overridden by non-SQL implementations.
101
+ def path_like (value)
102
+ all(:conditions => ["path LIKE ?", "#{value}#{path_delimiter}%"])
103
+ end
104
+
105
+ # Expand a path into an array of the path and all its ancestor paths.
106
+ def expanded_paths (path)
107
+ expanded = []
108
+ path.split(path_delimiter).each do |part|
109
+ if expanded.empty?
110
+ expanded << part
111
+ else
112
+ expanded << "#{expanded.last}#{path_delimiter}#{part}"
113
+ end
114
+ end
115
+ expanded
116
+ end
117
+
118
+ private
119
+
120
+ def populate_tree_structure! (root, sorted_nodes)
121
+ while !sorted_nodes.empty? do
122
+ node = sorted_nodes.last
123
+ if node.parent_path == root.path
124
+ sorted_nodes.pop
125
+ node.parent = root
126
+ root.send(:append_child, node)
127
+ else
128
+ last_child = root.children.last
129
+ if last_child and node.parent_path == last_child.path
130
+ populate_tree_structure!(last_child, sorted_nodes)
131
+ else
132
+ break
133
+ end
134
+ end
135
+ end
136
+ return root
137
+ end
138
+ end
139
+
140
+ # Get the full name of a node including the names of all it's parent nodes. Specify the separator string to use
141
+ # between values with :separator (defaults to " > "). You can also specify the context for the full name by
142
+ # specifying a path in :context. This will only render the names up to and not including that part of the tree.
143
+ def full_name (options = {})
144
+ separator = options[:separator] || " > "
145
+ n = ""
146
+ n << parent.full_name(options) if parent_path and parent_path != options[:context]
147
+ n << separator unless n.blank?
148
+ n << name
149
+ end
150
+
151
+ def path_delimiter
152
+ self.class.path_delimiter
153
+ end
154
+
155
+ # Get the parent node.
156
+ def parent
157
+ unless instance_variable_defined?(:@parent)
158
+ if path.index(path_delimiter)
159
+ @parent = self.class.base_class.first(:conditions => {:path => parent_path})
160
+ else
161
+ @parent = nil
162
+ end
163
+ end
164
+ @parent
165
+ end
166
+
167
+ # Set the parent node.
168
+ def parent= (node)
169
+ node_path = node.path if node
170
+ self.parent_path = node_path unless parent_path == node_path
171
+ @parent = node
172
+ end
173
+
174
+ # Set the parent path
175
+ def parent_path= (value)
176
+ unless value == parent_path
177
+ self[:parent_path] = value
178
+ recalculate_path
179
+ remove_instance_variable(:@parent) if instance_variable_defined?(:@parent)
180
+ end
181
+ value
182
+ end
183
+
184
+ def name= (value)
185
+ unless value == name
186
+ self[:name] = value
187
+ self.node_path = value if node_path.blank?
188
+ end
189
+ value
190
+ end
191
+
192
+ def node_path= (value)
193
+ pathified = self.class.pathify(value)
194
+ self[:node_path] = pathified
195
+ recalculate_path
196
+ end
197
+
198
+ # Get all nodes that are direct children of this node.
199
+ def children
200
+ unless @children
201
+ childrens_path = new_record? ? path : path_was
202
+ @children = self.class.base_class.all(:conditions => {:parent_path => childrens_path})
203
+ @children.each{|c| c.parent = self}
204
+ end
205
+ @children
206
+ end
207
+
208
+ # Get all nodes that share the same parent as this node.
209
+ def siblings
210
+ self.class.base_class.all(:conditions => {:parent_path => parent_path}).reject{|node| node == self}
211
+ end
212
+
213
+ # Get all descendant of this node.
214
+ def descendants
215
+ self.class.base_class.path_like(path)
216
+ end
217
+
218
+ # Get all ancestors of this node with the root node first.
219
+ def ancestors
220
+ ancestor_paths = expanded_paths
221
+ ancestor_paths.pop
222
+ if ancestor_paths.empty?
223
+ []
224
+ else
225
+ self.class.base_class.all(:conditions => {:path => ancestor_paths}).sort{|a,b| a.path.length <=> b.path.length}
226
+ end
227
+ end
228
+
229
+ # Returns an array containing the paths of this node and those of all its ancestors.
230
+ def expanded_paths
231
+ self.class.expanded_paths(path)
232
+ end
233
+
234
+ protected
235
+
236
+ def append_child (node)
237
+ @children ||= []
238
+ @children << node
239
+ node.parent = self
240
+ end
241
+
242
+ def recalculate_path
243
+ path = ""
244
+ path << "#{parent_path}#{path_delimiter}" unless parent_path.blank?
245
+ path << node_path if node_path
246
+ self.path = path
247
+ end
248
+ end
@@ -0,0 +1,24 @@
1
+ module PathTree
2
+ module Patterns
3
+ UPPER_A_PATTERN = /\xC3[\x80-\x85]/.freeze
4
+ LOWER_A_PATTERN = /\xC3[\xA0-\xA5]/.freeze
5
+ UPPER_E_PATTERN = /\xC3[\x88-\x8B]/.freeze
6
+ LOWER_E_PATTERN = /\xC3[\xA8-\xAB]/.freeze
7
+ UPPER_I_PATTERN = /\xC3[\x8C-\x8F]/.freeze
8
+ LOWER_I_PATTERN = /\xC3[\xAC-\xAF]/.freeze
9
+ UPPER_O_PATTERN = /\xC3[\x92-\x96\x98]/.freeze
10
+ LOWER_O_PATTERN = /\xC3[\xB2-\xB6\xB8]/.freeze
11
+ UPPER_U_PATTERN = /\xC3[\x99-\x9C]/.freeze
12
+ LOWER_U_PATTERN = /\xC3[\xB9-\xBC]/.freeze
13
+ UPPER_Y_PATTERN = /\xC3\x9D/.freeze
14
+ LOWER_Y_PATTERN = /\xC3[\xBD\xBF]/.freeze
15
+ UPPER_C_PATTERN = /\xC3\x87/.freeze
16
+ LOWER_C_PATTERN = /\xC3\xA7/.freeze
17
+ UPPER_N_PATTERN = /\xC3\x91/.freeze
18
+ LOWER_N_PATTERN = /\xC3\xB1/.freeze
19
+ UPPER_D_PATTERN = /\xC3\x90/.freeze
20
+ UPPER_AE_PATTERN = /\xC3\x86/.freeze
21
+ LOWER_AE_PATTERN = /\xC3\xA6/.freeze
22
+ SS_PATTERN = /\xC3\x9F/.freeze
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module PathTree
2
+ module Patterns
3
+ UPPER_A_PATTERN = /[\xC3\x80-\xC3\x85]/u.freeze
4
+ LOWER_A_PATTERN = /[\xC3\xA0-\xC3\xA5]/u.freeze
5
+ UPPER_E_PATTERN = /[\xC3\x88-\xC3\x8B]/u.freeze
6
+ LOWER_E_PATTERN = /[\xC3\xA8-\xC3\xAB]/u.freeze
7
+ UPPER_I_PATTERN = /[\xC3\x8C-\xC3\x8F]/u.freeze
8
+ LOWER_I_PATTERN = /[\xC3\xAC-\xC3\xAF]/u.freeze
9
+ UPPER_O_PATTERN = /[\xC3\x92-\xC3\x96\xC3\x98]/u.freeze
10
+ LOWER_O_PATTERN = /[\xC3\xB2-\xC3\xB6\xC3\xB8]/u.freeze
11
+ UPPER_U_PATTERN = /[\xC3\x99-\xC3\x9C]/u.freeze
12
+ LOWER_U_PATTERN = /[\xC3\xB9-\xC3\xBC]/u.freeze
13
+ UPPER_Y_PATTERN = /\xC3\x9D/u.freeze
14
+ LOWER_Y_PATTERN = /[\xC3\xBD\xC3\xBF]/u.freeze
15
+ UPPER_C_PATTERN = /\xC3\x87/u.freeze
16
+ LOWER_C_PATTERN = /\xC3\xA7/u.freeze
17
+ UPPER_N_PATTERN = /\xC3\x91/u.freeze
18
+ LOWER_N_PATTERN = /\xC3\xB1/u.freeze
19
+ UPPER_D_PATTERN = /\xC3\x90/u.freeze
20
+ UPPER_AE_PATTERN = /\xC3\x86/u.freeze
21
+ LOWER_AE_PATTERN = /\xC3\xA6/u.freeze
22
+ SS_PATTERN = /\xC3\x9F/u.freeze
23
+ end
24
+ end
@@ -0,0 +1,276 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PathTree do
6
+
7
+ module PathTree
8
+ class Test < ActiveRecord::Base
9
+ self.table_name = :test_path_trees
10
+
11
+ def self.create_tables
12
+ connection.create_table(table_name) do |t|
13
+ t.string :name
14
+ t.string :node_path
15
+ t.string :path
16
+ t.string :parent_path
17
+ end unless table_exists?
18
+ end
19
+
20
+ def self.drop_tables
21
+ connection.drop_table(table_name)
22
+ end
23
+
24
+ include PathTree
25
+ end
26
+ end
27
+
28
+ before :all do
29
+ PathTree::Test.create_tables
30
+ end
31
+
32
+ after :all do
33
+ PathTree::Test.drop_tables
34
+ end
35
+
36
+ context "path construction" do
37
+ it "should turn accented characters into ascii equivalents" do
38
+ PathTree::Test.asciify("ÀÂÄÃÅàâáããä-ÈÊÉËèêéë-ÌÎÍÏìîíï-ÒÔÖØÓÕòôöøóõ-ÚÜÙÛúüùû-ÝýÿÑñÇçÆæßÐ").should == "AAAAAaaaaaa-EEEEeeee-IIIIiiii-OOOOOOoooooo-UUUUuuuu-YyyNnCcAEaessD"
39
+ end
40
+
41
+ it "should unquote strings" do
42
+ PathTree::Test.unquote(%Q("This is a 'test'")).should == "This is a test"
43
+ end
44
+
45
+ it "should translate a value to a path part" do
46
+ PathTree::Test.pathify("This is the 1st / test À...").should == "this-is-the-1st-test-a"
47
+ end
48
+
49
+ it "should expand a path to its component paths" do
50
+ PathTree::Test.expanded_paths("this.is.a.test").should == ["this", "this.is", "this.is.a", "this.is.a.test"]
51
+ end
52
+
53
+ it "should set the parent path when setting the parent" do
54
+ parent = PathTree::Test.new(:name => "parent")
55
+ node = PathTree::Test.new(:name => "child")
56
+ node.parent = parent
57
+ node.parent_path.should == "parent"
58
+ node.parent = nil
59
+ node.parent_path.should == nil
60
+ end
61
+ end
62
+
63
+ context "with default delimiter" do
64
+ before :all do
65
+ @root_1 = PathTree::Test.create!(:name => "Root 1")
66
+ @parent_a = PathTree::Test.create!(:name => "Parent A", :parent_path => "root-1")
67
+ @parent_b = PathTree::Test.create!(:name => "Parent B", :parent_path => "root-1")
68
+ @parent_c = PathTree::Test.create!(:name => "Parent C", :parent_path => "root-1")
69
+ @child_a1 = PathTree::Test.create!(:name => "Child A1", :parent_path => "root-1.parent-a")
70
+ @child_a2 = PathTree::Test.create!(:name => "Child A2", :parent_path => "root-1.parent-a")
71
+ @grandchild = PathTree::Test.create!(:name => "Grandchild A1.1", :parent_path => "root-1.parent-a.child-a1")
72
+ @root_2 = PathTree::Test.create!(:name => "Root 2")
73
+ @parent_z = PathTree::Test.create!(:name => "Parent Z", :parent_path => "root-2")
74
+ end
75
+
76
+ after :all do
77
+ PathTree::Test.delete_all
78
+ end
79
+
80
+ it "should get the root nodes" do
81
+ PathTree::Test.roots.sort{|a,b| a.path <=> b.path}.should == [@root_1, @root_2]
82
+ end
83
+
84
+ it "should load an entire branch structure" do
85
+ branch = PathTree::Test.branch("root-1.parent-a")
86
+ branch.should == @parent_a
87
+ branch.instance_variable_get(:@children).should == [@child_a1, @child_a2]
88
+ branch.children.first.instance_variable_get(:@children).should == [@grandchild]
89
+ end
90
+
91
+ it "should construct a fully qualified name with a delimiter" do
92
+ @grandchild.full_name.should == "Root 1 > Parent A > Child A1 > Grandchild A1.1"
93
+ @grandchild.full_name(:separator => "/").should == "Root 1/Parent A/Child A1/Grandchild A1.1"
94
+ @grandchild.full_name(:context => "root-1.parent-a").should == "Child A1 > Grandchild A1.1"
95
+ end
96
+
97
+ it "should be able to get and set a parent node" do
98
+ node = PathTree::Test.find_by_path("root-1.parent-a")
99
+ node.parent.should == @root_1
100
+ node.parent = @root_2
101
+ node.parent_path.should == "root-2"
102
+ node.path.should == "root-2.parent-a"
103
+ end
104
+
105
+ it "should be able to set the parent by path" do
106
+ node = PathTree::Test.find_by_path("root-1.parent-a")
107
+ node.parent_path = "root-2"
108
+ node.parent.should == @root_2
109
+ node.path.should == "root-2.parent-a"
110
+ end
111
+
112
+ it "should have child nodes" do
113
+ node = PathTree::Test.find_by_path("root-1.parent-a")
114
+ node.children.should == [@child_a1, @child_a2]
115
+ end
116
+
117
+ it "should have descendant nodes" do
118
+ node = PathTree::Test.find_by_path("root-1.parent-a")
119
+ node.descendants.should == [@child_a1, @child_a2, @grandchild]
120
+ end
121
+
122
+ it "should have sibling nodes" do
123
+ node = PathTree::Test.find_by_path("root-1.parent-a")
124
+ node.siblings.should == [@parent_b, @parent_c]
125
+ end
126
+
127
+ it "should have ancestor nodes" do
128
+ node = PathTree::Test.find_by_path("root-1.parent-a.child-a1")
129
+ node.ancestors.should == [@root_1, @parent_a]
130
+ end
131
+
132
+ it "should maintain the path with the name path" do
133
+ node = PathTree::Test.find_by_path("root-1.parent-a")
134
+ node.node_path = "New Name"
135
+ node.path.should == "root-1.new-name"
136
+ end
137
+
138
+ it "should get the expanded paths for a node" do
139
+ @grandchild.expanded_paths.should == ["root-1", "root-1.parent-a", "root-1.parent-a.child-a1", "root-1.parent-a.child-a1.grandchild-a1-1"]
140
+ end
141
+
142
+ it "should update child paths when the path is changed" do
143
+ PathTree::Test.transaction do
144
+ node = PathTree::Test.find_by_path("root-1.parent-a")
145
+ node.node_path = "New Name"
146
+ node.save!
147
+ node.reload
148
+ node.children.collect{|c| c.path}.should == ["root-1.new-name.child-a1", "root-1.new-name.child-a2"]
149
+ node.children.first.children.collect{|c| c.path}.should == ["root-1.new-name.child-a1.grandchild-a1-1"]
150
+ raise ActiveRecord::Rollback
151
+ end
152
+ end
153
+
154
+ it "should update child paths when a node is destroyed" do
155
+ PathTree::Test.transaction do
156
+ node = PathTree::Test.find_by_path("root-1.parent-a")
157
+ node.name = "New Name"
158
+ node.destroy
159
+ root = PathTree::Test.find_by_path("root-1")
160
+ root.children.collect{|c| c.path}.should == ["root-1.parent-b", "root-1.parent-c", "root-1.child-a1", "root-1.child-a2"]
161
+ root.children[2].children.collect{|c| c.path}.should == ["root-1.child-a1.grandchild-a1-1"]
162
+ raise ActiveRecord::Rollback
163
+ end
164
+ end
165
+ end
166
+
167
+
168
+ context "with default delimiter" do
169
+ before :all do
170
+ PathTree::Test.path_delimiter = '/'
171
+ @root_1 = PathTree::Test.create!(:name => "Root 1")
172
+ @parent_a = PathTree::Test.create!(:name => "Parent A", :parent_path => "root-1")
173
+ @parent_b = PathTree::Test.create!(:name => "Parent B", :parent_path => "root-1")
174
+ @parent_c = PathTree::Test.create!(:name => "Parent C", :parent_path => "root-1")
175
+ @child_a1 = PathTree::Test.create!(:name => "Child A1", :parent_path => "root-1/parent-a")
176
+ @child_a2 = PathTree::Test.create!(:name => "Child A2", :parent_path => "root-1/parent-a")
177
+ @grandchild = PathTree::Test.create!(:name => "Grandchild A1.1", :parent_path => "root-1/parent-a/child-a1")
178
+ @root_2 = PathTree::Test.create!(:name => "Root 2")
179
+ @parent_z = PathTree::Test.create!(:name => "Parent Z", :parent_path => "root-2")
180
+ end
181
+
182
+ after :all do
183
+ PathTree::Test.path_delimiter = nil
184
+ end
185
+
186
+ it "should get the root nodes" do
187
+ PathTree::Test.roots.sort{|a,b| a.path <=> b.path}.should == [@root_1, @root_2]
188
+ end
189
+
190
+ it "should load an entire branch structure" do
191
+ branch = PathTree::Test.branch("root-1/parent-a")
192
+ branch.should == @parent_a
193
+ branch.instance_variable_get(:@children).should == [@child_a1, @child_a2]
194
+ branch.children.first.instance_variable_get(:@children).should == [@grandchild]
195
+ end
196
+
197
+ it "should construct a fully qualified name with a delimiter" do
198
+ @grandchild.full_name.should == "Root 1 > Parent A > Child A1 > Grandchild A1.1"
199
+ @grandchild.full_name(:separator => ":").should == "Root 1:Parent A:Child A1:Grandchild A1.1"
200
+ @grandchild.full_name(:context => "root-1/parent-a").should == "Child A1 > Grandchild A1.1"
201
+ end
202
+
203
+ it "should be able to get and set a parent node" do
204
+ node = PathTree::Test.find_by_path("root-1/parent-a")
205
+ node.parent.should == @root_1
206
+ node.parent = @root_2
207
+ node.parent_path.should == "root-2"
208
+ node.path.should == "root-2/parent-a"
209
+ end
210
+
211
+ it "should be able to set the parent by path" do
212
+ node = PathTree::Test.find_by_path("root-1/parent-a")
213
+ node.parent_path = "root-2"
214
+ node.parent.should == @root_2
215
+ node.path.should == "root-2/parent-a"
216
+ end
217
+
218
+ it "should have child nodes" do
219
+ node = PathTree::Test.find_by_path("root-1/parent-a")
220
+ node.children.should == [@child_a1, @child_a2]
221
+ end
222
+
223
+ it "should have descendant nodes" do
224
+ node = PathTree::Test.find_by_path("root-1/parent-a")
225
+ node.descendants.should == [@child_a1, @child_a2, @grandchild]
226
+ end
227
+
228
+ it "should have sibling nodes" do
229
+ node = PathTree::Test.find_by_path("root-1/parent-a")
230
+ node.siblings.should == [@parent_b, @parent_c]
231
+ end
232
+
233
+ it "should have ancestor nodes" do
234
+ node = PathTree::Test.find_by_path("root-1/parent-a/child-a1")
235
+ node.ancestors.should == [@root_1, @parent_a]
236
+ end
237
+
238
+ it "should maintain the path with the name path" do
239
+ node = PathTree::Test.find_by_path("root-1/parent-a")
240
+ node.node_path = "New Name"
241
+ node.path.should == "root-1/new-name"
242
+ end
243
+
244
+ it "should get the expanded paths for a node" do
245
+ @grandchild.expanded_paths.should == ["root-1", "root-1/parent-a", "root-1/parent-a/child-a1", "root-1/parent-a/child-a1/grandchild-a1-1"]
246
+ end
247
+
248
+ it "should update child paths when the path is changed" do
249
+ PathTree::Test.transaction do
250
+ node = PathTree::Test.find_by_path("root-1/parent-a")
251
+ node.node_path = "New Name"
252
+ node.save!
253
+ node.reload
254
+ node.children.collect{|c| c.path}.should == ["root-1/new-name/child-a1", "root-1/new-name/child-a2"]
255
+ node.children.first.children.collect{|c| c.path}.should == ["root-1/new-name/child-a1/grandchild-a1-1"]
256
+ raise ActiveRecord::Rollback
257
+ end
258
+ end
259
+
260
+ it "should update child paths when a node is destroyed" do
261
+ begin
262
+ PathTree::Test.transaction do
263
+ node = PathTree::Test.find_by_path("root-1/parent-a")
264
+ node.name = "New Name"
265
+ node.destroy
266
+ root = PathTree::Test.find_by_path("root-1")
267
+ root.children.collect{|c| c.path}.should == ["root-1/parent-b", "root-1/parent-c", "root-1/child-a1", "root-1/child-a2"]
268
+ root.children[2].children.collect{|c| c.path}.should == ["root-1/child-a1/grandchild-a1-1"]
269
+ raise ActiveRecord::Rollback
270
+ end
271
+ rescue
272
+ puts $@.join("\n")
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'sqlite3'
4
+
5
+ begin
6
+ require 'simplecov'
7
+ SimpleCov.start do
8
+ add_filter "/spec/"
9
+ end
10
+ rescue LoadError
11
+ # simplecov not installed
12
+ end
13
+
14
+ ActiveRecord::Base.establish_connection("adapter" => "sqlite3", "database" => ":memory:")
15
+
16
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'path_tree'))