path_tree 1.0.11

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/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'))