hyrarchy 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +69 -0
- data/lib/hyrarchy.rb +321 -0
- data/lib/hyrarchy/awesome_nested_set_compatibility.rb +375 -0
- data/lib/hyrarchy/collection_proxy.rb +75 -0
- data/lib/hyrarchy/encoded_path.rb +111 -0
- data/rails_plugin/init.rb +2 -0
- data/spec/create_nodes_table.rb +25 -0
- data/spec/database.yml +10 -0
- data/spec/hyrarchy_spec.rb +184 -0
- data/spec/spec_helper.rb +52 -0
- data/test/encoded_path_test.rb +31 -0
- data/test/test_helper.rb +6 -0
- metadata +82 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 The Indianapolis Star
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
= Hyrarchy
|
2
|
+
|
3
|
+
Hyrarchy (Hybrid hieRarchy) is a gem and Rails plugin for working with hierarchic data in ActiveRecord. Your models gain methods for finding an instance's parent, children, ancestors, descendants, and depth, as well as a named scope for finding root nodes.
|
4
|
+
|
5
|
+
To use Hyrarchy in your Rails app, copy the plugin from the gem into your app's vendors/plugins directory. (The plugin is just a two-liner that loads and activates the gem.)
|
6
|
+
|
7
|
+
To use Hyrarchy in one of your models, add the following line to the class:
|
8
|
+
|
9
|
+
class Comment < ActiveRecord::Base
|
10
|
+
is_hierarchic
|
11
|
+
end
|
12
|
+
|
13
|
+
Then add the hierarchic columns to the model's database table:
|
14
|
+
|
15
|
+
class MakeCommentsHierarchic < ActiveRecord::Migration
|
16
|
+
def self.up
|
17
|
+
add_hierarchy :comments
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.down
|
21
|
+
remove_hierarchy :comments
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Or you can put it in the same migration as the table's creation:
|
26
|
+
|
27
|
+
class CreateCommentsTable < ActiveRecord::Migration
|
28
|
+
def self.up
|
29
|
+
create_table :comments do |t|
|
30
|
+
t.integer :author_id
|
31
|
+
t.text :body
|
32
|
+
end
|
33
|
+
add_hierarchy :comments
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.down
|
37
|
+
drop_table :comments
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
== Performance
|
42
|
+
|
43
|
+
On MySQL, Hyrarchy scales to at least one million nodes with insertion and access times below 100ms. On SQLite, times are below 200ms.
|
44
|
+
|
45
|
+
== Database Compatibility
|
46
|
+
|
47
|
+
Hyrarchy has been tested on MySQL 5 and SQLite 3.
|
48
|
+
|
49
|
+
== Replacing awesome_nested_set
|
50
|
+
|
51
|
+
Hyrarchy is designed to be an almost-drop-in replacement for awesome_nested_set. All of awesome_nested_set's methods are implemented by Hyrarchy, but you'll need to replace calls to acts_as_nested_set with is_hierarchic. You'll also need to replace awesome_nested_set's database columns with Hyrarchy's, which you can do with an option to the add_hierarchy migration method:
|
52
|
+
|
53
|
+
add_hierarchy :comments, :convert => :awesome_nested_set
|
54
|
+
|
55
|
+
The convert option will modify the table structure but it won't rebuild the hierarchy information. You can rebuild it by calling rebuild! on your hierarchic model class:
|
56
|
+
|
57
|
+
Comment.rebuild!
|
58
|
+
|
59
|
+
The same option can be used with remove_hierarchy for the down half of a migration.
|
60
|
+
|
61
|
+
Hyrarchy doesn't yet support awesome_nested_set's scoping feature or its view helper.
|
62
|
+
|
63
|
+
== Implementation Details
|
64
|
+
|
65
|
+
Under the hood, Hyrarchy uses a combination of an adjacency list and a rational nested set. The nested set uses a technique developed by (I think) Vadim Tropashko, in which the left and right values are generated using Farey sequences. This makes it possible to insert new records without adjusting the left and right values of any other records. It also makes it possible to do many operations (like determining a record's depth in the tree) without accessing the database. For operations where rational nested sets perform poorly (such as finding a node's immediate descendants), the adjacency list is used.
|
66
|
+
|
67
|
+
== Credits and Copyright
|
68
|
+
|
69
|
+
Heavily based on works by Vadim Tropashko and Wim Lewis. Implemented by Dana Danger. Tolerated by VivaZoya. Copyright (c) 2008 The Indianapolis Star, released under the MIT license. See LICENSE for details.
|
data/lib/hyrarchy.rb
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
require 'hyrarchy/encoded_path'
|
2
|
+
require 'hyrarchy/collection_proxy'
|
3
|
+
require 'hyrarchy/awesome_nested_set_compatibility'
|
4
|
+
|
5
|
+
module Hyrarchy
|
6
|
+
# Fudge factor to account for imprecision with floating point approximations
|
7
|
+
# of a node's left and right fractions.
|
8
|
+
FLOAT_FUDGE_FACTOR = 0.00000000001 # :nodoc:
|
9
|
+
|
10
|
+
# Mixes Hyrarchy into ActiveRecord.
|
11
|
+
def self.activate!
|
12
|
+
ActiveRecord::Base.extend IsHierarchic
|
13
|
+
ActiveRecord::Migration.extend Migrations
|
14
|
+
end
|
15
|
+
|
16
|
+
# These methods are available in ActiveRecord migrations for adding and
|
17
|
+
# removing columns and indexes required by Hyrarchy.
|
18
|
+
module Migrations
|
19
|
+
def add_hierarchy(table, options = {})
|
20
|
+
convert = options.delete(:convert)
|
21
|
+
unless options.empty?
|
22
|
+
raise(ArgumentError, "unknown keys: #{options.keys.join(', ')}")
|
23
|
+
end
|
24
|
+
|
25
|
+
case convert
|
26
|
+
when :awesome_nested_set
|
27
|
+
remove_column table, :lft
|
28
|
+
remove_column table, :rgt
|
29
|
+
when '', nil
|
30
|
+
else
|
31
|
+
raise(ArgumentError, "don't know how to convert hierarchy from #{convert}")
|
32
|
+
end
|
33
|
+
|
34
|
+
add_column table, :lft, :float
|
35
|
+
add_column table, :rgt, :float
|
36
|
+
add_column table, :lft_numer, :integer
|
37
|
+
add_column table, :lft_denom, :integer
|
38
|
+
add_column table, :parent_id, :integer unless convert == :awesome_nested_set
|
39
|
+
add_index table, :lft
|
40
|
+
add_index table, [:lft_numer, :lft_denom], :unique => true
|
41
|
+
add_index table, :parent_id
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_hierarchy(table, options = {})
|
45
|
+
convert = options.delete(:convert)
|
46
|
+
unless options.empty?
|
47
|
+
raise(ArgumentError, "unknown keys: #{options.keys.join(', ')}")
|
48
|
+
end
|
49
|
+
|
50
|
+
remove_column table, :lft
|
51
|
+
remove_column table, :rgt
|
52
|
+
remove_column table, :lft_numer
|
53
|
+
remove_column table, :lft_denom
|
54
|
+
remove_column table, :parent_id, :integer unless convert == :awesome_nested_set
|
55
|
+
|
56
|
+
case convert
|
57
|
+
when :awesome_nested_set
|
58
|
+
add_column table, :lft, :integer
|
59
|
+
add_column table, :rgt, :integer
|
60
|
+
when '', nil
|
61
|
+
else
|
62
|
+
raise(ArgumentError, "don't know how to convert hierarchy to #{convert}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module IsHierarchic
|
68
|
+
# Declares that a model represents hierarchic data. Adds a has_many
|
69
|
+
# association for instances' children, and a named scope for the model's
|
70
|
+
# root nodes (called +roots+).
|
71
|
+
def is_hierarchic
|
72
|
+
extend ClassMethods
|
73
|
+
include InstanceMethods
|
74
|
+
|
75
|
+
has_many :children,
|
76
|
+
:foreign_key => 'parent_id',
|
77
|
+
:order => 'rgt DESC, lft',
|
78
|
+
:class_name => self.to_s,
|
79
|
+
:dependent => :destroy
|
80
|
+
|
81
|
+
before_save :set_encoded_paths
|
82
|
+
before_save :set_parent_id
|
83
|
+
after_save :update_descendant_paths
|
84
|
+
after_save :reset_flags
|
85
|
+
|
86
|
+
named_scope :roots,
|
87
|
+
:conditions => { :parent_id => nil },
|
88
|
+
:order => 'rgt DESC, lft'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# These private methods are available to model classes that have been
|
93
|
+
# declared is_hierarchic. They're used internally and aren't intended to be
|
94
|
+
# used by application developers.
|
95
|
+
module ClassMethods # :nodoc:
|
96
|
+
include Hyrarchy::AwesomeNestedSetCompatibility::ClassMethods
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Finds the first unused child path beneath +parent_path+.
|
101
|
+
def next_child_encoded_path(parent_path)
|
102
|
+
if parent_path == Hyrarchy::EncodedPath::ROOT
|
103
|
+
if sibling = roots.last
|
104
|
+
child_path = sibling.send(:encoded_path).next_sibling
|
105
|
+
else
|
106
|
+
child_path = Hyrarchy::EncodedPath::ROOT.first_child
|
107
|
+
end
|
108
|
+
else
|
109
|
+
node = find_by_encoded_path(parent_path)
|
110
|
+
child_path = node ?
|
111
|
+
node.send(:next_child_encoded_path) : parent_path.first_child
|
112
|
+
end
|
113
|
+
while self.exists?(:lft_numer => child_path.numerator, :lft_denom => child_path.denominator)
|
114
|
+
child_path = child_path.next_sibling
|
115
|
+
end
|
116
|
+
child_path
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the node with the specified encoded path.
|
120
|
+
def find_by_encoded_path(p)
|
121
|
+
find(:first, :conditions => {
|
122
|
+
:lft_numer => p.numerator,
|
123
|
+
:lft_denom => p.denominator
|
124
|
+
})
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# These methods are available to instances of models that have been declared
|
129
|
+
# is_hierarchic.
|
130
|
+
module InstanceMethods
|
131
|
+
include Hyrarchy::AwesomeNestedSetCompatibility::InstanceMethods
|
132
|
+
|
133
|
+
# Returns this node's parent, or +nil+ if this is a root node.
|
134
|
+
def parent
|
135
|
+
return @new_parent if @new_parent
|
136
|
+
p = encoded_path.parent
|
137
|
+
return nil if p.nil?
|
138
|
+
self.class.send(:find_by_encoded_path, p)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Sets this node's parent. To make this node a root node, set its parent to
|
142
|
+
# +nil+.
|
143
|
+
def parent=(other)
|
144
|
+
@make_root = false
|
145
|
+
if other.nil?
|
146
|
+
@new_parent = nil
|
147
|
+
@make_root = true
|
148
|
+
elsif encoded_path && other.encoded_path == (encoded_path.parent rescue nil)
|
149
|
+
@new_parent = nil
|
150
|
+
else
|
151
|
+
@new_parent = other
|
152
|
+
end
|
153
|
+
other
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns an array of this node's descendants: its children, grandchildren,
|
157
|
+
# and so on. The array returned by this method is a named scope.
|
158
|
+
def descendants
|
159
|
+
cached[:descendants] ||=
|
160
|
+
self_and_descendants.scoped :conditions => "id <> #{id}"
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns an array of this node's ancestors--its parent, grandparent, and
|
164
|
+
# so on--ordered from parent to root. The array returned by this method is
|
165
|
+
# a has_many association, so you can do things like this:
|
166
|
+
#
|
167
|
+
# node.ancestors.find(:all, :conditions => { ... })
|
168
|
+
#
|
169
|
+
def ancestors(with_self = false)
|
170
|
+
cache_key = with_self ? :self_and_ancestors : :ancestors
|
171
|
+
return cached[cache_key] if cached[cache_key]
|
172
|
+
|
173
|
+
paths = []
|
174
|
+
path = with_self ? encoded_path : encoded_path.parent
|
175
|
+
while path do
|
176
|
+
paths << path
|
177
|
+
path = path.parent
|
178
|
+
end
|
179
|
+
|
180
|
+
cached[cache_key] = CollectionProxy.new(
|
181
|
+
self,
|
182
|
+
cache_key,
|
183
|
+
:conditions => paths.empty? ? "id <> id" : [
|
184
|
+
paths.collect {|p| "(lft_numer = ? AND lft_denom = ?)"}.join(" OR "),
|
185
|
+
*(paths.collect {|p| [p.numerator, p.denominator]}.flatten)
|
186
|
+
],
|
187
|
+
:order => 'rgt, lft DESC'
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns the root node related to this node, or nil if this node is a root
|
192
|
+
# node.
|
193
|
+
def root
|
194
|
+
return cached[:root] if cached[:root]
|
195
|
+
|
196
|
+
path = encoded_path.parent
|
197
|
+
while path do
|
198
|
+
parent = path.parent
|
199
|
+
break if parent.nil?
|
200
|
+
path = parent
|
201
|
+
end
|
202
|
+
|
203
|
+
if path
|
204
|
+
self.class.send :find_by_encoded_path, path
|
205
|
+
else
|
206
|
+
nil
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns the number of nodes between this one and the top of the tree.
|
211
|
+
def depth
|
212
|
+
encoded_path.depth - 1
|
213
|
+
end
|
214
|
+
|
215
|
+
# Overrides ActiveRecord's reload method to clear cached scopes and ad hoc
|
216
|
+
# associations.
|
217
|
+
def reload(options = nil) # :nodoc:
|
218
|
+
@cached = {}
|
219
|
+
reset_flags
|
220
|
+
super
|
221
|
+
end
|
222
|
+
|
223
|
+
protected
|
224
|
+
|
225
|
+
# Sets the node's encoded path, updating all relevant database columns to
|
226
|
+
# match.
|
227
|
+
def encoded_path=(r) # :nodoc:
|
228
|
+
@cached = {}
|
229
|
+
if r.nil?
|
230
|
+
self.lft_numer = nil
|
231
|
+
self.lft_denom = nil
|
232
|
+
self.lft = nil
|
233
|
+
self.rgt = nil
|
234
|
+
else
|
235
|
+
@path_has_changed = true
|
236
|
+
self.lft_numer = r.numerator
|
237
|
+
self.lft_denom = r.denominator
|
238
|
+
self.lft = r.to_f
|
239
|
+
self.rgt = encoded_path.next_farey_fraction.to_f
|
240
|
+
end
|
241
|
+
r
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns the node's encoded path (its rational left value).
|
245
|
+
def encoded_path # :nodoc:
|
246
|
+
return nil if lft_numer.nil? || lft_denom.nil?
|
247
|
+
Hyrarchy::EncodedPath(lft_numer, lft_denom)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns a hash for caching scopes and ad hoc associations.
|
251
|
+
def cached # :nodoc:
|
252
|
+
@cached ||= {}
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns the first unused child path under this node.
|
256
|
+
def next_child_encoded_path
|
257
|
+
return nil unless encoded_path
|
258
|
+
if children.empty?
|
259
|
+
encoded_path.first_child
|
260
|
+
else
|
261
|
+
children.last.send(:encoded_path).next_sibling
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
# before_save callback to ensure that this node's encoded path is a child
|
268
|
+
# of its parent.
|
269
|
+
def set_encoded_paths # :nodoc:
|
270
|
+
@path_has_changed = false if @path_has_changed.nil?
|
271
|
+
p = nil
|
272
|
+
self.lft_numer = self.lft_denom = nil if @make_root
|
273
|
+
|
274
|
+
if @new_parent.nil?
|
275
|
+
if lft_numer.nil? || lft_denom.nil?
|
276
|
+
p = Hyrarchy::EncodedPath::ROOT
|
277
|
+
end
|
278
|
+
else
|
279
|
+
p = @new_parent.encoded_path
|
280
|
+
end
|
281
|
+
|
282
|
+
if p
|
283
|
+
new_path = self.class.send(:next_child_encoded_path, p)
|
284
|
+
if @path_has_changed = (encoded_path != new_path)
|
285
|
+
self.encoded_path = new_path
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
true
|
290
|
+
end
|
291
|
+
|
292
|
+
# before_save callback to ensure that this node's parent_id attribute
|
293
|
+
# agrees with its encoded path.
|
294
|
+
def set_parent_id # :nodoc:
|
295
|
+
parent = self.class.send(:find_by_encoded_path, encoded_path.parent(false))
|
296
|
+
self.parent_id = parent ? parent.id : nil
|
297
|
+
true
|
298
|
+
end
|
299
|
+
|
300
|
+
# after_save callback to ensure that this node's descendants are updated if
|
301
|
+
# this node has moved.
|
302
|
+
def update_descendant_paths # :nodoc:
|
303
|
+
return true unless @path_has_changed
|
304
|
+
children.reload if children.loaded? && children.empty?
|
305
|
+
|
306
|
+
child_path = encoded_path.first_child
|
307
|
+
children.each do |c|
|
308
|
+
c.encoded_path = child_path
|
309
|
+
c.save!
|
310
|
+
child_path = child_path.next_sibling
|
311
|
+
end
|
312
|
+
|
313
|
+
true
|
314
|
+
end
|
315
|
+
|
316
|
+
# Resets internal flags after saving.
|
317
|
+
def reset_flags # :nodoc:
|
318
|
+
@path_has_changed = @new_parent = @make_root = nil
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
@@ -0,0 +1,375 @@
|
|
1
|
+
module Hyrarchy
|
2
|
+
module AwesomeNestedSetCompatibility
|
3
|
+
module ClassMethods
|
4
|
+
# Returns the first root node.
|
5
|
+
def root
|
6
|
+
roots.first
|
7
|
+
end
|
8
|
+
|
9
|
+
# Returns true if the model's left and right values are valid, and all
|
10
|
+
# root nodes have no ancestors.
|
11
|
+
def valid?
|
12
|
+
left_and_rights_valid? && all_roots_valid?
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns true if the model's left and right values match the parent_id
|
16
|
+
# attributes.
|
17
|
+
def left_and_rights_valid?
|
18
|
+
# Load all nodes and index them by ID so we can leave the database
|
19
|
+
# alone.
|
20
|
+
nodes = connection.select_all("SELECT id, lft_numer, lft_denom, parent_id FROM #{quoted_table_name}")
|
21
|
+
nodes_by_id = {}
|
22
|
+
nodes.each do |node|
|
23
|
+
node['id'] = node['id'].to_i
|
24
|
+
node['encoded_path'] = Hyrarchy::EncodedPath(node['lft_numer'].to_i, node['lft_denom'].to_i)
|
25
|
+
node['parent_id'] = node['parent_id'] ? node['parent_id'].to_i : nil
|
26
|
+
nodes_by_id[node['id']] = node
|
27
|
+
end
|
28
|
+
# Check to see if the structure defined by the nodes' encoded paths
|
29
|
+
# matches the structure defined by their parent_id attributes.
|
30
|
+
nodes.all? do |node|
|
31
|
+
if node['parent_id'].nil?
|
32
|
+
node['encoded_path'].parent == nil rescue false
|
33
|
+
else
|
34
|
+
parent = nodes_by_id[node['parent_id']]
|
35
|
+
parent && node['encoded_path'].parent == parent['encoded_path']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Always returns true. This method exists solely for compatibility with
|
41
|
+
# awesome_nested_set; the test it performs doesn't apply to Hyrarchy.
|
42
|
+
def no_duplicates_for_columns?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true if all roots have no ancestors.
|
47
|
+
def all_roots_valid?
|
48
|
+
each_root_valid?(roots)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns true if all of the nodes in +roots_to_validate+ have no
|
52
|
+
# ancestors.
|
53
|
+
def each_root_valid?(roots_to_validate)
|
54
|
+
roots_to_validate.all? {|r| r.root?}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Rebuilds the model's hierarchy attributes based on the parent_id
|
58
|
+
# attributes.
|
59
|
+
def rebuild!
|
60
|
+
return true if (valid? rescue false)
|
61
|
+
|
62
|
+
update_all("lft = id, rgt = id, lft_numer = id, lft_denom = id")
|
63
|
+
paths_by_id = {}
|
64
|
+
order_by = columns_hash['created_at'] ? :created_at : :id
|
65
|
+
|
66
|
+
nodes = roots :order => order_by
|
67
|
+
until nodes.empty? do
|
68
|
+
nodes.each do |node|
|
69
|
+
parent_path = paths_by_id[node.parent_id] || Hyrarchy::EncodedPath::ROOT
|
70
|
+
node.send(:encoded_path=, next_child_encoded_path(parent_path))
|
71
|
+
node.send(:create_or_update_without_callbacks) || raise(RecordNotSaved)
|
72
|
+
paths_by_id[node.id] = node.send(:encoded_path)
|
73
|
+
end
|
74
|
+
node_ids = nodes.collect {|n| n.id}
|
75
|
+
nodes = find(:all, :conditions => { :parent_id => node_ids }, :order => order_by)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module InstanceMethods
|
81
|
+
# Returns this node's left value. Records that haven't yet been saved
|
82
|
+
# won't have left values.
|
83
|
+
def left
|
84
|
+
encoded_path
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns this node's left value. Records that haven't yet been saved
|
88
|
+
# won't have right values.
|
89
|
+
def right
|
90
|
+
encoded_path && encoded_path.next_farey_fraction
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns true if this is a root node.
|
94
|
+
def root?
|
95
|
+
(encoded_path.nil? || depth == 0 || @make_root) && !@new_parent
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns true if this node has no children.
|
99
|
+
def leaf?
|
100
|
+
children.empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns true if this node is a child of another node.
|
104
|
+
def child?
|
105
|
+
!root?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Compares two nodes by their left values.
|
109
|
+
def <=>(x)
|
110
|
+
x.left <=> left
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns an array containing this node and its ancestors, starting with
|
114
|
+
# this node and ending with its root. The array returned by this method
|
115
|
+
# is a has_many association, so you can do things like this:
|
116
|
+
#
|
117
|
+
# node.self_and_ancestors.find(:all, :conditions => { ... })
|
118
|
+
#
|
119
|
+
def self_and_ancestors
|
120
|
+
ancestors(true)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns an array containing this node and its siblings. The array
|
124
|
+
# returned by this method is a has_many association, so you can do things
|
125
|
+
# like this:
|
126
|
+
#
|
127
|
+
# node.self_and_siblings.find(:all, :conditions => { ... })
|
128
|
+
#
|
129
|
+
def self_and_siblings
|
130
|
+
siblings(true)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns an array containing this node's siblings. The array returned by
|
134
|
+
# this method is a has_many association, so you can do things like this:
|
135
|
+
#
|
136
|
+
# node.siblings.find(:all, :conditions => { ... })
|
137
|
+
#
|
138
|
+
def siblings(with_self = false)
|
139
|
+
cache_key = with_self ? :self_and_siblings : :siblings
|
140
|
+
return cached[cache_key] if cached[cache_key]
|
141
|
+
|
142
|
+
if with_self
|
143
|
+
conditions = { :parent_id => parent_id }
|
144
|
+
else
|
145
|
+
conditions = ["parent_id #{parent_id.nil? ? 'IS' : '='} ? AND id <> ?",
|
146
|
+
parent_id, id]
|
147
|
+
end
|
148
|
+
|
149
|
+
cached[cache_key] = self.class.scoped(
|
150
|
+
:conditions => conditions,
|
151
|
+
:order => 'rgt DESC, lft'
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns an array containing this node's childless descendants. The
|
156
|
+
# array returned by this method is a named scope.
|
157
|
+
def leaves
|
158
|
+
cached[:leaves] ||= descendants.scoped :conditions => "NOT EXISTS (
|
159
|
+
SELECT * FROM #{self.class.quoted_table_name} tt
|
160
|
+
WHERE tt.parent_id = #{self.class.quoted_table_name}.id
|
161
|
+
)"
|
162
|
+
end
|
163
|
+
|
164
|
+
# Alias for depth.
|
165
|
+
def level
|
166
|
+
depth
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns an array of this node and its descendants: its children,
|
170
|
+
# grandchildren, and so on. The array returned by this method is a
|
171
|
+
# has_many association, so you can do things like this:
|
172
|
+
#
|
173
|
+
# node.self_and_descendants.find(:all, :conditions => { ... })
|
174
|
+
#
|
175
|
+
def self_and_descendants
|
176
|
+
cached[:self_and_descendants] ||= CollectionProxy.new(
|
177
|
+
self,
|
178
|
+
:descendants,
|
179
|
+
:conditions => { :lft => (lft - FLOAT_FUDGE_FACTOR)..(rgt + FLOAT_FUDGE_FACTOR) },
|
180
|
+
:order => 'rgt DESC, lft',
|
181
|
+
# The query conditions intentionally load extra records that aren't
|
182
|
+
# descendants to account for floating point imprecision. This
|
183
|
+
# procedure removes the extra records.
|
184
|
+
:after => Proc.new do |records|
|
185
|
+
r = encoded_path.next_farey_fraction
|
186
|
+
records.delete_if do |n|
|
187
|
+
n.encoded_path < encoded_path || n.encoded_path >= r
|
188
|
+
end
|
189
|
+
end,
|
190
|
+
# The regular count method doesn't work because of the fudge factor
|
191
|
+
# in the conditions. This procedure uses the length of the records
|
192
|
+
# array if it's been loaded. Otherwise it does a raw SQL query (to
|
193
|
+
# avoid the expense of instantiating a bunch of ActiveRecord objects)
|
194
|
+
# and prunes the results in the same manner as the :after procedure.
|
195
|
+
:count => Proc.new do
|
196
|
+
if descendants.loaded?
|
197
|
+
descendants.length
|
198
|
+
else
|
199
|
+
rows = self.class.connection.select_all("
|
200
|
+
SELECT lft_numer, lft_denom
|
201
|
+
FROM #{self.class.quoted_table_name}
|
202
|
+
WHERE #{descendants.conditions}")
|
203
|
+
r = encoded_path.next_farey_fraction
|
204
|
+
rows.delete_if do |row|
|
205
|
+
p = Hyrarchy::EncodedPath(
|
206
|
+
row['lft_numer'].to_i,
|
207
|
+
row['lft_denom'].to_i)
|
208
|
+
p < encoded_path || p >= r
|
209
|
+
end
|
210
|
+
rows.length
|
211
|
+
end
|
212
|
+
end,
|
213
|
+
# Associations don't normally have an optimized index method, but
|
214
|
+
# this one does. :)
|
215
|
+
:index => Proc.new do |obj|
|
216
|
+
rows = self.class.connection.select_all("
|
217
|
+
SELECT id, lft_numer, lft_denom
|
218
|
+
FROM #{self.class.quoted_table_name}
|
219
|
+
WHERE #{descendants.conditions}
|
220
|
+
ORDER BY rgt DESC, lft")
|
221
|
+
r = encoded_path.next_farey_fraction
|
222
|
+
rows.delete_if do |row|
|
223
|
+
p = Hyrarchy::EncodedPath(
|
224
|
+
row['lft_numer'].to_i,
|
225
|
+
row['lft_denom'].to_i)
|
226
|
+
row.delete('lft_numer')
|
227
|
+
row.delete('lft_denom')
|
228
|
+
p < encoded_path || p >= r
|
229
|
+
end
|
230
|
+
rows.index({'id' => obj.id.to_s})
|
231
|
+
end
|
232
|
+
)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Returns true if this node is a descendant of +other+.
|
236
|
+
def is_descendant_of?(other)
|
237
|
+
left > other.left && left <= other.right
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns true if this node is a descendant of +other+, or if this node
|
241
|
+
# is +other+.
|
242
|
+
def is_or_is_descendant_of?(other)
|
243
|
+
left >= other.left && left <= other.right
|
244
|
+
end
|
245
|
+
|
246
|
+
# Returns true if this node is an ancestor of +other+.
|
247
|
+
def is_ancestor_of?(other)
|
248
|
+
other.left > left && other.left <= right
|
249
|
+
end
|
250
|
+
|
251
|
+
# Returns true if this node is an ancestor of +other+, or if this node is
|
252
|
+
# +other+.
|
253
|
+
def is_or_is_ancestor_of?(other)
|
254
|
+
other.left >= left && other.left <= right
|
255
|
+
end
|
256
|
+
|
257
|
+
# Always returns true. This method exists solely for compatibility with
|
258
|
+
# awesome_nested_set; Hyrarchy doesn't support scoping (but maybe it will
|
259
|
+
# some day).
|
260
|
+
def same_scope?(other)
|
261
|
+
true
|
262
|
+
end
|
263
|
+
|
264
|
+
# Returns the sibling before this node. If this node is its parent's
|
265
|
+
# first child, returns nil.
|
266
|
+
def left_sibling # :nodoc:
|
267
|
+
path = send(:encoded_path)
|
268
|
+
return nil if path == path.parent.first_child
|
269
|
+
sibling_path = path.previous_sibling
|
270
|
+
until self.class.exists?(:lft_numer => sibling_path.numerator, :lft_denom => sibling_path.denominator)
|
271
|
+
sibling_path = sibling_path.previous_sibling
|
272
|
+
end
|
273
|
+
self.class.send(:find_by_encoded_path, sibling_path)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Returns the sibling after this node. If this node is its parent's last
|
277
|
+
# child, returns nil.
|
278
|
+
def right_sibling
|
279
|
+
siblings = root? ? self.class.roots : other.parent.children
|
280
|
+
return nil if self == siblings.last
|
281
|
+
sibling_path = send(:encoded_path).next_sibling
|
282
|
+
until self.class.exists?(:lft_numer => sibling_path.numerator, :lft_denom => sibling_path.denominator)
|
283
|
+
sibling_path = sibling_path.next_sibling
|
284
|
+
end
|
285
|
+
self.class.send(:find_by_encoded_path, sibling_path)
|
286
|
+
end
|
287
|
+
|
288
|
+
def move_left # :nodoc:
|
289
|
+
raise NotImplementedError, "awesome_nested_set's move_left method isn't implemented in this version of Hyrarchy"
|
290
|
+
end
|
291
|
+
|
292
|
+
def move_right # :nodoc:
|
293
|
+
raise NotImplementedError, "awesome_nested_set's move_right method isn't implemented in this version of Hyrarchy"
|
294
|
+
end
|
295
|
+
|
296
|
+
# The semantics of left and right don't quite map exactly from
|
297
|
+
# awesome_nested_set to Hyrarchy. For the purpose of this method, "left"
|
298
|
+
# means "before."
|
299
|
+
#
|
300
|
+
# If this node isn't a sibling of +other+, its parent will be set to
|
301
|
+
# +other+'s parent.
|
302
|
+
def move_to_left_of(other)
|
303
|
+
# Don't attempt an impossible move.
|
304
|
+
if other.is_descendant_of?(self)
|
305
|
+
raise ArgumentError, "you can't move a node to the left of one of its descendants"
|
306
|
+
end
|
307
|
+
# Find the first unused path after +other+'s path.
|
308
|
+
open_path = other.send(:encoded_path).next_sibling
|
309
|
+
while self.class.exists?(:lft_numer => open_path.numerator, :lft_denom => open_path.denominator)
|
310
|
+
open_path = open_path.next_sibling
|
311
|
+
end
|
312
|
+
# Move +other+, and all nodes following it, down.
|
313
|
+
while open_path != other.send(:encoded_path)
|
314
|
+
p = open_path.previous_sibling
|
315
|
+
n = self.class.send(:find_by_encoded_path, p)
|
316
|
+
n.send(:encoded_path=, open_path)
|
317
|
+
n.save!
|
318
|
+
open_path = p
|
319
|
+
end
|
320
|
+
puts open_path
|
321
|
+
# Insert this node.
|
322
|
+
send(:encoded_path=, open_path)
|
323
|
+
save!
|
324
|
+
end
|
325
|
+
|
326
|
+
# The semantics of left and right don't quite map exactly from
|
327
|
+
# awesome_nested_set to Hyrarchy. For the purpose of this method, "right"
|
328
|
+
# means "after."
|
329
|
+
#
|
330
|
+
# If this node isn't a sibling of +other+, its parent will be set to
|
331
|
+
# +other+'s parent.
|
332
|
+
def move_to_right_of(other)
|
333
|
+
# Don't attempt an impossible move.
|
334
|
+
if other.is_descendant_of?(self)
|
335
|
+
raise ArgumentError, "you can't move a node to the right of one of its descendants"
|
336
|
+
end
|
337
|
+
# If +other+ is its parent's last child, we can simply append this node
|
338
|
+
# to the parent's children.
|
339
|
+
siblings = other.root? ? self.class.roots : other.parent.children
|
340
|
+
if other == siblings.last
|
341
|
+
send(:encoded_path=, other.send(:encoded_path).next_sibling)
|
342
|
+
save!
|
343
|
+
else
|
344
|
+
# Otherwise, this is equivalent to moving this node to the left of
|
345
|
+
# +other+'s right sibling.
|
346
|
+
move_to_left_of(other.right_sibling)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# Sets this node's parent to +node+ and calls save!.
|
351
|
+
def move_to_child_of(node)
|
352
|
+
node = self.class.find(node)
|
353
|
+
self.parent = node
|
354
|
+
save!
|
355
|
+
end
|
356
|
+
|
357
|
+
# Makes this node a root node and calls save!.
|
358
|
+
def move_to_root
|
359
|
+
self.parent = nil
|
360
|
+
save!
|
361
|
+
end
|
362
|
+
|
363
|
+
def move_possible?(target) # :nodoc:
|
364
|
+
raise NotImplementedError, "awesome_nested_set's move_possible? method isn't implemented in this version of Hyrarchy"
|
365
|
+
end
|
366
|
+
|
367
|
+
# Returns a textual representation of this node and its descendants.
|
368
|
+
def to_text
|
369
|
+
self_and_descendants.map do |node|
|
370
|
+
"#{'*'*(node.depth+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
|
371
|
+
end.join("\n")
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|