path_tree 1.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/License.txt +674 -0
- data/README.rdoc +11 -0
- data/Rakefile +37 -0
- data/lib/path_tree.rb +248 -0
- data/lib/ruby_18_patterns.rb +24 -0
- data/lib/ruby_19_patterns.rb +24 -0
- data/spec/path_tree_spec.rb +276 -0
- data/spec/spec_helper.rb +16 -0
- metadata +103 -0
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
|
data/spec/spec_helper.rb
ADDED
@@ -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'))
|