pkondzior-sequel_nested_set 0.9.9
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/COPYING +21 -0
- data/README +3 -0
- data/TODO +3 -0
- data/lib/sequel_nested_set.rb +563 -0
- data/spec/nested_set_spec.rb +617 -0
- data/spec/rcov.opts +1 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +39 -0
- metadata +79 -0
data/COPYING
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2009 Paweł Kondzior
|
2
|
+
Copyright (c) 2007-2008 collectiveidea
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
data/TODO
ADDED
@@ -0,0 +1,563 @@
|
|
1
|
+
unless Object.respond_to?(:returning)
|
2
|
+
class Object
|
3
|
+
def returning(value)
|
4
|
+
yield(value)
|
5
|
+
value
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module Sequel
|
11
|
+
module Plugins
|
12
|
+
# This acts provides Nested Set functionality. Nested Set is a smart way to implement
|
13
|
+
# an _ordered_ tree, with the added feature that you can select the children and all of their
|
14
|
+
# descendants with a single query. The drawback is that insertion or move need some complex
|
15
|
+
# sql queries. But everything is done here by this module!
|
16
|
+
#
|
17
|
+
# Nested sets are appropriate each time you want either an orderd tree (menus,
|
18
|
+
# commercial categories) or an efficient way of querying big trees (threaded posts).
|
19
|
+
#
|
20
|
+
# == API
|
21
|
+
#
|
22
|
+
# # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
|
23
|
+
# child = MyClass.new(:name => "child1")
|
24
|
+
# child.save
|
25
|
+
# # now move the item to its right place
|
26
|
+
# child.move_to_child_of my_item
|
27
|
+
#
|
28
|
+
# You can pass an id or an object to:
|
29
|
+
# * <tt>#move_to_child_of</tt>
|
30
|
+
# * <tt>#move_to_right_of</tt>
|
31
|
+
# * <tt>#move_to_left_of</tt>
|
32
|
+
#
|
33
|
+
module NestedSet
|
34
|
+
# Configuration options are:
|
35
|
+
#
|
36
|
+
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: :parent_id)
|
37
|
+
# * +:left_column+ - column name for left boundry data, default :lft
|
38
|
+
# * +:right_column+ - column name for right boundry data, default :rgt
|
39
|
+
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
|
40
|
+
# (if it hasn't been already) and use that as the foreign key restriction. You
|
41
|
+
# can also pass an array to scope by multiple attributes.
|
42
|
+
# Example: <tt>is :nested_set, { :scope => [:notable_id, :notable_type] }</tt>
|
43
|
+
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
|
44
|
+
# child objects are destroyed alongside this object by calling their destroy
|
45
|
+
# method. If set to :delete_all (default), all the child objects are deleted
|
46
|
+
# without calling their destroy method.
|
47
|
+
#
|
48
|
+
# See Sequle::Plugins::NestedSet::ClassMethods for a list of class methods and
|
49
|
+
# Sequle::Plugins::NestedSet::InstanceMethods for a list of instance methods added
|
50
|
+
# to acts_as_nested_set models
|
51
|
+
def self.apply(model, options = {})
|
52
|
+
options = {
|
53
|
+
:parent_column => :parent_id,
|
54
|
+
:left_column => :lft,
|
55
|
+
:right_column => :rgt,
|
56
|
+
:dependent => :delete_all, # or :destroy
|
57
|
+
}.merge(options)
|
58
|
+
|
59
|
+
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
|
60
|
+
options[:scope] = "#{options[:scope]}_id".to_sym
|
61
|
+
end
|
62
|
+
|
63
|
+
model.class.class_eval do
|
64
|
+
attr_accessor :nested_set_options
|
65
|
+
end
|
66
|
+
model.nested_set_options = options
|
67
|
+
|
68
|
+
model.before_create { set_default_left_and_right }
|
69
|
+
model.before_destroy { prune_from_tree }
|
70
|
+
|
71
|
+
model.set_restricted_columns(*([:left, :right, :parent_id, options[:parent_column], options[:left_column], options[:right_column]].uniq))
|
72
|
+
end
|
73
|
+
|
74
|
+
module DatasetMethods
|
75
|
+
# All nested set queries should use this nested dataset method, which returns Dataset that provides
|
76
|
+
# proper :scope which you can configure on is :nested, { :scope => ... }
|
77
|
+
# declaration in your Sequel::Model
|
78
|
+
def nested
|
79
|
+
order(self.model_classes[nil].qualified_left_column)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns dataset for all root nodes
|
83
|
+
def roots
|
84
|
+
nested.filter(self.model_classes[nil].qualified_parent_column => nil)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns dataset for all of nodes which do not have children
|
88
|
+
def leaves
|
89
|
+
nested.filter(self.model_classes[nil].qualified_right_column - self.model_classes[nil].qualified_left_column => 1)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module ClassMethods
|
94
|
+
|
95
|
+
# Returns the first root
|
96
|
+
def root
|
97
|
+
roots.first
|
98
|
+
end
|
99
|
+
|
100
|
+
def qualified_parent_column(table_name = self.implicit_table_name)
|
101
|
+
"#{table_name}__#{self.nested_set_options[:parent_column]}".to_sym
|
102
|
+
end
|
103
|
+
|
104
|
+
def qualified_parent_column_literal
|
105
|
+
self.dataset.literal(self.nested_set_options[:parent_column])
|
106
|
+
end
|
107
|
+
|
108
|
+
def qualified_left_column(table_name = self.implicit_table_name)
|
109
|
+
"#{table_name}__#{self.nested_set_options[:left_column]}".to_sym
|
110
|
+
end
|
111
|
+
|
112
|
+
def qualified_left_column_literal
|
113
|
+
self.dataset.literal(self.nested_set_options[:left_column])
|
114
|
+
end
|
115
|
+
|
116
|
+
def qualified_right_column(table_name = self.implicit_table_name)
|
117
|
+
"#{table_name}__#{self.nested_set_options[:right_column]}".to_sym
|
118
|
+
end
|
119
|
+
|
120
|
+
def qualified_right_column_literal
|
121
|
+
self.dataset.literal(self.nested_set_options[:right_column])
|
122
|
+
end
|
123
|
+
|
124
|
+
def valid?
|
125
|
+
self.left_and_rights_valid? && self.no_duplicates_for_columns? && self.all_roots_valid?
|
126
|
+
end
|
127
|
+
|
128
|
+
def left_and_rights_valid?
|
129
|
+
self.left_outer_join(Client.implicit_table_name.as(:parent), self.qualified_parent_column => "parent__#{self.primary_key}".to_sym).
|
130
|
+
filter({ self.qualified_left_column => nil } |
|
131
|
+
{ self.qualified_right_column => nil } |
|
132
|
+
(self.qualified_left_column >= self.qualified_right_column) |
|
133
|
+
(~{ self.qualified_parent_column => nil } & ((self.qualified_left_column <= self.qualified_left_column(:parent)) |
|
134
|
+
(self.qualified_right_column >= self.qualified_right_column(:parent))))).count == 0
|
135
|
+
end
|
136
|
+
|
137
|
+
def left_and_rights_valid_dataset?
|
138
|
+
self.left_outer_join(Client.implicit_table_name.as(:parent), self.qualified_parent_column => "parent__#{self.primary_key}".to_sym).
|
139
|
+
filter({ self.qualified_left_column => nil } |
|
140
|
+
{ self.qualified_right_column => nil } |
|
141
|
+
(self.qualified_left_column >= self.qualified_right_column) |
|
142
|
+
(~{ self.qualified_parent_column => nil } & ((self.qualified_left_column <= self.qualified_left_column(:parent)) |
|
143
|
+
(self.qualified_right_column >= self.qualified_right_column(:parent)))))
|
144
|
+
end
|
145
|
+
|
146
|
+
def no_duplicates_for_columns?
|
147
|
+
# TODO: scope
|
148
|
+
# scope_columns = Array(self.nested_set_options[:scope]).map do |c|
|
149
|
+
# connection.quote_column_name(c)
|
150
|
+
# end.push(nil).join(", ")
|
151
|
+
[self.qualified_left_column, self.qualified_right_column].all? do |column|
|
152
|
+
self.dataset.select(column, :count[column]).group(column).having(:count[column] > 1).first.nil?
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Wrapper for each_root_valid? that can deal with scope.
|
157
|
+
def all_roots_valid?
|
158
|
+
# TODO: scope
|
159
|
+
# if self.nested_set_options[:scope]
|
160
|
+
# roots.group(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
|
161
|
+
# each_root_valid?(grouped_roots)
|
162
|
+
# end
|
163
|
+
# else
|
164
|
+
each_root_valid?(roots.all)
|
165
|
+
# end
|
166
|
+
end
|
167
|
+
|
168
|
+
def each_root_valid?(roots_to_validate)
|
169
|
+
left = right = 0
|
170
|
+
roots_to_validate.all? do |root|
|
171
|
+
returning(root.left > left && root.right > right) do
|
172
|
+
left = root.left
|
173
|
+
right = root.right
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
|
179
|
+
def rebuild!
|
180
|
+
|
181
|
+
scope = lambda{}
|
182
|
+
# TODO: add scope stuff
|
183
|
+
|
184
|
+
# Don't rebuild a valid tree.
|
185
|
+
return true if valid?
|
186
|
+
indices = {}
|
187
|
+
|
188
|
+
move_to_child_of_lambda = lambda do |parent_node|
|
189
|
+
# Set left
|
190
|
+
parent_node[nested_set_options[:left_column]] = indices[scope.call(parent_node)] += 1
|
191
|
+
# Gather child noodes of parend_node and iterate by children
|
192
|
+
parent_node.children.order(:id).all.each do |child_node|
|
193
|
+
move_to_child_of_lambda.call(child_node)
|
194
|
+
end
|
195
|
+
# Set right
|
196
|
+
parent_node[nested_set_options[:right_column]] = indices[scope.call(parent_node)] += 1
|
197
|
+
parent_node.save
|
198
|
+
end
|
199
|
+
|
200
|
+
# Gatcher root nodes and iterate by them
|
201
|
+
self.roots.all.each do |root_node|
|
202
|
+
# setup index for this scope
|
203
|
+
indices[scope.call(root_node)] ||= 0
|
204
|
+
move_to_child_of_lambda.call(root_node)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def to_text(&block)
|
209
|
+
text = []
|
210
|
+
self.roots.each do |root|
|
211
|
+
text << root.to_text(&block)
|
212
|
+
end
|
213
|
+
text.join("\n")
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the entire set as a nested array. If flat is true then a flat
|
217
|
+
# array is returned instead. Specify mover to exclude any impossible
|
218
|
+
# moves. Pass a block to perform an operation on each item. The block
|
219
|
+
# arguments are |item, level|.
|
220
|
+
def to_nested_a(flat = false, mover = nil, &block)
|
221
|
+
descendants = self.nested.all
|
222
|
+
array = []
|
223
|
+
|
224
|
+
while not descendants.empty?
|
225
|
+
items = descendants.shift.to_nested_a(flat, mover, descendants, 0, &block)
|
226
|
+
array.send flat ? 'concat' : '<<', items
|
227
|
+
end
|
228
|
+
|
229
|
+
return array
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
module InstanceMethods
|
234
|
+
|
235
|
+
# Returns hash of Model nested set options
|
236
|
+
def nested_set_options
|
237
|
+
self.class.nested_set_options
|
238
|
+
end
|
239
|
+
|
240
|
+
# Setter of the left column
|
241
|
+
def left=(value)
|
242
|
+
self[self.nested_set_options[:left_column]] = value
|
243
|
+
end
|
244
|
+
|
245
|
+
# Setter of the right column
|
246
|
+
def right=(value)
|
247
|
+
self[self.nested_set_options[:right_column]] = value
|
248
|
+
end
|
249
|
+
|
250
|
+
# Getter of the left column
|
251
|
+
def left
|
252
|
+
self[self.nested_set_options[:left_column]] || 0
|
253
|
+
end
|
254
|
+
|
255
|
+
# Getter of the right column
|
256
|
+
def right
|
257
|
+
self[self.nested_set_options[:right_column]] || 0
|
258
|
+
end
|
259
|
+
|
260
|
+
# Setter of the parent column
|
261
|
+
def parent_id=(value)
|
262
|
+
self[self.nested_set_options[:parent_column]] = value
|
263
|
+
end
|
264
|
+
|
265
|
+
# Getter of parent column
|
266
|
+
def parent_id
|
267
|
+
self[self.nested_set_options[:parent_column]]
|
268
|
+
end
|
269
|
+
|
270
|
+
# Set left=, right= and parent_id= to be procted methods
|
271
|
+
# this methods should be used only internally by nested set plugin
|
272
|
+
protected :left=, :right=, :parent_id=
|
273
|
+
|
274
|
+
# Returns the level of this object in the tree
|
275
|
+
# root level is 0
|
276
|
+
def level
|
277
|
+
root? ? 0 : ancestors.count
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns true if this is a root node
|
281
|
+
def root?
|
282
|
+
parent_id.nil?
|
283
|
+
end
|
284
|
+
|
285
|
+
# Returns true if this is a leaf node
|
286
|
+
def leaf?
|
287
|
+
right - left == 1
|
288
|
+
end
|
289
|
+
|
290
|
+
# Returns true is this is a child node
|
291
|
+
def child?
|
292
|
+
!root?
|
293
|
+
end
|
294
|
+
|
295
|
+
# order by left column
|
296
|
+
def <=>(x)
|
297
|
+
left <=> x.left
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns root
|
301
|
+
def root
|
302
|
+
self_and_ancestors.first
|
303
|
+
end
|
304
|
+
|
305
|
+
# Returns the immediate parent
|
306
|
+
def parent
|
307
|
+
dataset.nested.filter(self.primary_key => self.parent_id).first if self.parent_id
|
308
|
+
end
|
309
|
+
|
310
|
+
# Returns the dataset for all parent nodes and self
|
311
|
+
def self_and_ancestors
|
312
|
+
dataset.filter((self.class.qualified_left_column <= left) & (self.class.qualified_right_column >= right))
|
313
|
+
end
|
314
|
+
|
315
|
+
# Returns the dataset for all children of the parent, including self
|
316
|
+
def self_and_siblings
|
317
|
+
dataset.nested.filter(self.class.qualified_parent_column => self.parent_id)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Returns dataset for itself and all of its nested children
|
321
|
+
def self_and_descendants
|
322
|
+
dataset.nested.filter((self.class.qualified_left_column >= left) & (self.class.qualified_right_column <= right))
|
323
|
+
end
|
324
|
+
|
325
|
+
# Filter for dataset that will exclude self object
|
326
|
+
def without_self(dataset)
|
327
|
+
dataset.nested.filter(~{self.primary_key => self.id})
|
328
|
+
end
|
329
|
+
|
330
|
+
# Returns dataset for its immediate children
|
331
|
+
def children
|
332
|
+
dataset.nested.filter(self.class.qualified_parent_column => self.id)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Returns dataset for all parents
|
336
|
+
def ancestors
|
337
|
+
without_self(self_and_ancestors)
|
338
|
+
end
|
339
|
+
|
340
|
+
# Returns dataset for all children of the parent, except self
|
341
|
+
def siblings
|
342
|
+
without_self(self_and_siblings)
|
343
|
+
end
|
344
|
+
|
345
|
+
# Returns dataset for all of its children and nested children
|
346
|
+
def descendants
|
347
|
+
without_self(self_and_descendants)
|
348
|
+
end
|
349
|
+
|
350
|
+
# Returns dataset for all of its nested children which do not have children
|
351
|
+
def leaves
|
352
|
+
descendants.filter(self.class.qualified_right_column - self.class.qualified_left_column => 1)
|
353
|
+
end
|
354
|
+
|
355
|
+
def is_descendant_of?(other)
|
356
|
+
other.left < self.left && self.left < other.right && same_scope?(other)
|
357
|
+
end
|
358
|
+
|
359
|
+
def is_or_is_descendant_of?(other)
|
360
|
+
other.left <= self.left && self.left < other.right && same_scope?(other)
|
361
|
+
end
|
362
|
+
|
363
|
+
def is_ancestor_of?(other)
|
364
|
+
self.left < other.left && other.left < self.right && same_scope?(other)
|
365
|
+
end
|
366
|
+
|
367
|
+
def is_or_is_ancestor_of?(other)
|
368
|
+
self.left <= other.left && other.left < self.right && same_scope?(other)
|
369
|
+
end
|
370
|
+
|
371
|
+
# Check if other model is in the same scope
|
372
|
+
def same_scope?(other)
|
373
|
+
Array(nil).all? do |attr|
|
374
|
+
self.send(attr) == other.send(attr)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# Find the first sibling to the left
|
379
|
+
def left_sibling
|
380
|
+
siblings.filter(self.class.qualified_left_column < left).order(self.class.qualified_left_column.desc).first
|
381
|
+
end
|
382
|
+
|
383
|
+
# Find the first sibling to the right
|
384
|
+
def right_sibling
|
385
|
+
siblings.filter(self.class.qualified_left_column > left).first
|
386
|
+
end
|
387
|
+
|
388
|
+
|
389
|
+
# Shorthand method for finding the left sibling and moving to the left of it.
|
390
|
+
def move_left
|
391
|
+
self.move_to_left_of(self.left_sibling)
|
392
|
+
end
|
393
|
+
|
394
|
+
# Shorthand method for finding the right sibling and moving to the right of it.
|
395
|
+
def move_right
|
396
|
+
self.move_to_right_of(self.right_sibling)
|
397
|
+
end
|
398
|
+
|
399
|
+
# Move the node to the left of another node (you can pass id only)
|
400
|
+
def move_to_left_of(node)
|
401
|
+
self.move_to(node, :left)
|
402
|
+
end
|
403
|
+
|
404
|
+
# Move the node to the left of another node (you can pass id only)
|
405
|
+
def move_to_right_of(node)
|
406
|
+
self.move_to(node, :right)
|
407
|
+
end
|
408
|
+
|
409
|
+
# Move the node to the child of another node (you can pass id only)
|
410
|
+
def move_to_child_of(node)
|
411
|
+
self.move_to(node, :child)
|
412
|
+
end
|
413
|
+
|
414
|
+
# Move the node to root nodes
|
415
|
+
def move_to_root
|
416
|
+
self.move_to(nil, :root)
|
417
|
+
end
|
418
|
+
|
419
|
+
# Check if node move is possible for specific target
|
420
|
+
def move_possible?(target)
|
421
|
+
self != target && # Can't target self
|
422
|
+
same_scope?(target) && # can't be in different scopes
|
423
|
+
# !(left..right).include?(target.left..target.right) # this needs tested more
|
424
|
+
# detect impossible move
|
425
|
+
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
|
426
|
+
end
|
427
|
+
|
428
|
+
# You can pass block that will have
|
429
|
+
def to_text
|
430
|
+
self_and_descendants.map do |node|
|
431
|
+
if block_given?
|
432
|
+
inspect = yield(node)
|
433
|
+
else
|
434
|
+
inspect = node.class.inspect
|
435
|
+
end
|
436
|
+
"#{'*'*(node.level+1)} #{inspect} (#{node.parent_id.inspect}, #{node.left}, #{node.right})"
|
437
|
+
end.join("\n")
|
438
|
+
end
|
439
|
+
|
440
|
+
# Returns self and its descendants as a nested array. If flat is true
|
441
|
+
# then a flat array is returned instead. Specify mover to exclude any
|
442
|
+
# impossible moves. Pass a block to perform an operation on each item.
|
443
|
+
# The block arguments are |item, level|. The remaining arguments for
|
444
|
+
# this method are for recursion and should not normally be given.
|
445
|
+
def to_nested_a(flat = false, mover = nil, descendants = nil, level = self.level, &block)
|
446
|
+
descendants ||= self.descendants.all
|
447
|
+
array = [ block_given? ? yield(self, level) : self ]
|
448
|
+
|
449
|
+
while not descendants.empty?
|
450
|
+
break unless descendants.first.parent_id == self.id
|
451
|
+
item = descendants.shift
|
452
|
+
items = item.to_nested_a(flat, mover, descendants, level + 1, &block)
|
453
|
+
array.send flat ? 'concat' : '<<', items if mover.nil? or mover.new? or mover.move_possible?(item)
|
454
|
+
end
|
455
|
+
|
456
|
+
return array
|
457
|
+
end
|
458
|
+
|
459
|
+
protected
|
460
|
+
# on creation, set automatically lft and rgt to the end of the tree
|
461
|
+
def set_default_left_and_right
|
462
|
+
maxright = dataset.nested.max(self.class.qualified_right_column).to_i || 0
|
463
|
+
# adds the new node to the right of all existing nodes
|
464
|
+
self.left = maxright + 1
|
465
|
+
self.right = maxright + 2
|
466
|
+
end
|
467
|
+
|
468
|
+
# Prunes a branch off of the tree, shifting all of the elements on the right
|
469
|
+
# back to the left so the counts still work.
|
470
|
+
def prune_from_tree
|
471
|
+
return if self.right.nil? || self.left.nil?
|
472
|
+
diff = self.right - self.left + 1
|
473
|
+
|
474
|
+
#TODO: implemente :dependent option
|
475
|
+
# delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
|
476
|
+
# :destroy_all : :delete_all
|
477
|
+
|
478
|
+
#TODO: implement prune method
|
479
|
+
# self.class.base_class.transaction do
|
480
|
+
# nested_set_scope.send(delete_method,
|
481
|
+
# ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
|
482
|
+
# left, right]
|
483
|
+
# )
|
484
|
+
# nested_set_scope.update_all(
|
485
|
+
# ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
|
486
|
+
# ["#{quoted_left_column_name} >= ?", right]
|
487
|
+
# )
|
488
|
+
# nested_set_scope.update_all(
|
489
|
+
# ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
|
490
|
+
# ["#{quoted_right_column_name} >= ?", right]
|
491
|
+
# )
|
492
|
+
# end
|
493
|
+
end
|
494
|
+
|
495
|
+
# reload left, right, and parent
|
496
|
+
def reload_nested_set
|
497
|
+
reload(:select => "#{quoted_left_column_name}, " +
|
498
|
+
"#{quoted_right_column_name}, #{quoted_parent_column_name}")
|
499
|
+
end
|
500
|
+
|
501
|
+
def move_to(target, position)
|
502
|
+
raise Error, "You cannot move a new node" if self.new?
|
503
|
+
# #TODO: add callback
|
504
|
+
db.transaction do
|
505
|
+
unless position == :root || self.move_possible?(target)
|
506
|
+
raise Error, "Impossible move, target node cannot be inside moved tree."
|
507
|
+
end
|
508
|
+
|
509
|
+
bound = case position
|
510
|
+
when :child; target.right
|
511
|
+
when :left; target.left
|
512
|
+
when :right; target.right + 1
|
513
|
+
when :root; 1
|
514
|
+
else raise Error, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
515
|
+
end
|
516
|
+
|
517
|
+
if bound > self.right
|
518
|
+
bound = bound - 1
|
519
|
+
other_bound = self.right + 1
|
520
|
+
else
|
521
|
+
other_bound = self.left - 1
|
522
|
+
end
|
523
|
+
|
524
|
+
# there would be no change
|
525
|
+
return if bound == self.right || bound == self.left
|
526
|
+
|
527
|
+
# we have defined the boundaries of two non-overlapping intervals,
|
528
|
+
# so sorting puts both the intervals and their boundaries in order
|
529
|
+
a, b, c, d = [self.left, self.right, bound, other_bound].sort
|
530
|
+
|
531
|
+
new_parent = case position
|
532
|
+
when :child; target.id
|
533
|
+
when :root; 'NULL'
|
534
|
+
else target.parent_id
|
535
|
+
end
|
536
|
+
|
537
|
+
# TODO : scope stuff for update
|
538
|
+
self.dataset.update(
|
539
|
+
"#{self.class.qualified_left_column_literal} = (CASE " +
|
540
|
+
"WHEN #{self.class.qualified_left_column_literal} BETWEEN #{a} AND #{b} " +
|
541
|
+
"THEN #{self.class.qualified_left_column_literal} + #{d} - #{b} " +
|
542
|
+
"WHEN #{self.class.qualified_left_column_literal} BETWEEN #{c} AND #{d} " +
|
543
|
+
"THEN #{self.class.qualified_left_column_literal} + #{a} - #{c} " +
|
544
|
+
"ELSE #{self.class.qualified_left_column_literal} END), " +
|
545
|
+
"#{self.class.qualified_right_column_literal} = (CASE " +
|
546
|
+
"WHEN #{self.class.qualified_right_column_literal} BETWEEN #{a} AND #{b} " +
|
547
|
+
"THEN #{self.class.qualified_right_column_literal} + #{d} - #{b} " +
|
548
|
+
"WHEN #{self.class.qualified_right_column_literal} BETWEEN #{c} AND #{d} " +
|
549
|
+
"THEN #{self.class.qualified_right_column_literal} + #{a} - #{c} " +
|
550
|
+
"ELSE #{self.class.qualified_right_column_literal} END), " +
|
551
|
+
"#{self.class.qualified_parent_column_literal} = (CASE " +
|
552
|
+
"WHEN #{self.primary_key} = #{self.id} THEN #{new_parent} " +
|
553
|
+
"ELSE #{self.class.qualified_parent_column_literal} END)"
|
554
|
+
)
|
555
|
+
target.refresh if target
|
556
|
+
self.refresh
|
557
|
+
#TODO: add after_move
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
@@ -0,0 +1,617 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper")
|
2
|
+
|
3
|
+
describe "Sequel Nested Set" do
|
4
|
+
|
5
|
+
describe "without scope" do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
prepare_nested_set_data
|
9
|
+
@root = Client.filter(:name => 'Top Level').first
|
10
|
+
@node1 = Client.filter(:name => 'Child 1').first
|
11
|
+
@node2 = Client.filter(:name => 'Child 2').first
|
12
|
+
@node2_1 = Client.filter(:name => 'Child 2.1').first
|
13
|
+
@node3 = Client.filter(:name => 'Child 3').first
|
14
|
+
@root2 = Client.filter(:name => 'Top Level 2').first
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "ClassMethods" do
|
18
|
+
|
19
|
+
it "should have nested_set_options" do
|
20
|
+
Client.should respond_to(:nested_set_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have default options :left_column, :right_column, :parent_column, :dependent and :scope" do
|
24
|
+
Client.nested_set_options[:left_column].should == :lft
|
25
|
+
Client.nested_set_options[:right_column].should == :rgt
|
26
|
+
Client.nested_set_options[:parent_column].should == :parent_id
|
27
|
+
Client.nested_set_options[:dependent].should == :delete_all
|
28
|
+
Client.nested_set_options[:scope].should be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should have qualified column methods" do
|
32
|
+
Client.qualified_parent_column.should == :clients__parent_id
|
33
|
+
Client.qualified_left_column.should == :clients__lft
|
34
|
+
Client.qualified_right_column.should == :clients__rgt
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should have roots that contains all root nodes" do
|
38
|
+
roots = Client.roots.all
|
39
|
+
roots.should == Client.filter(:parent_id => nil).all
|
40
|
+
roots.should == [@root, @root2]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should have root that will be root? => true" do
|
44
|
+
Client.roots.first.root?.should be_true
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should have all leaves" do
|
48
|
+
leaves = Client.leaves.all
|
49
|
+
leaves.should == Client.nested.filter(:rgt - :lft => 1).all
|
50
|
+
leaves.should == [@node1, @node2_1, @node3, @root2]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should have root" do
|
54
|
+
Client.root.should == @root
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should have to_text method that returns whole tree from all root nodes as text" do
|
58
|
+
Client.to_text.should == "* Client (nil, 1, 10)\n** Client (1, 2, 3)\n** Client (1, 4, 7)\n*** Client (3, 5, 6)\n** Client (1, 8, 9)\n* Client (nil, 11, 12)"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should have to_text method that returns whole tree from all root nodes as text and should be able to pass block" do
|
62
|
+
Client.to_text { |node| node.name }.should == "* Top Level (nil, 1, 10)\n** Child 1 (1, 2, 3)\n** Child 2 (1, 4, 7)\n*** Child 2.1 (3, 5, 6)\n** Child 3 (1, 8, 9)\n* Top Level 2 (nil, 11, 12)"
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should have to_nested_a method that returns nested array of all nodes from roots to leaves" do
|
66
|
+
Client.to_nested_a.should == [[@root, [@node1], [@node2, [@node2_1]], [@node3]], [@root2]]
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should have to_nested_a method that can pass block with node and level" do
|
70
|
+
Client.to_nested_a { |node, level| level }.should == [[0, [1], [1, [2]], [1]], [0]]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "InstanceMethods" do
|
75
|
+
|
76
|
+
it "should have nested_set_options" do
|
77
|
+
@root.class.should respond_to(:nested_set_options)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should have parent, left, right getter based on nested set config" do
|
81
|
+
node = Client.new.update_all(:parent_id => nil, :lft => 1, :rgt => 2)
|
82
|
+
node.left.should == node[node.class.nested_set_options[:left_column]]
|
83
|
+
node.right.should == node[node.class.nested_set_options[:right_column]]
|
84
|
+
node.parent_id.should == node[node.class.nested_set_options[:parent_column]]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should have parent, left, right setter based on nested set config" do
|
88
|
+
node = Client.new
|
89
|
+
node.send(:left=, 1)
|
90
|
+
node.send(:right=, 2)
|
91
|
+
node.send(:parent_id=, 69)
|
92
|
+
node.left.should == node[node.class.nested_set_options[:left_column]]
|
93
|
+
node.right.should == node[node.class.nested_set_options[:right_column]]
|
94
|
+
node.parent_id.should == node[node.class.nested_set_options[:parent_column]]
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should parent, left and right setters be protected methods" do
|
98
|
+
Client.new.protected_methods.include?("left=").should be_true
|
99
|
+
Client.new.protected_methods.include?("right=").should be_true
|
100
|
+
Client.new.protected_methods.include?("parent_id=").should be_true
|
101
|
+
end
|
102
|
+
|
103
|
+
it "shoud have faild on new when passing keys configured as right_column, left_column, parent_column" do
|
104
|
+
lambda { Client.new(Client.nested_set_options[:left_column] => 1) }.should raise_error(Sequel::Error)
|
105
|
+
lambda { Client.new(Client.nested_set_options[:right_column] => 2) }.should raise_error(Sequel::Error)
|
106
|
+
lambda { Client.new(Client.nested_set_options[:parent_column] => nil) }.should raise_error(Sequel::Error)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "Client.new with {:left => 1, :right => 2, :parent_id => nil} should raise NoMethodError exception" do
|
110
|
+
lambda { Client.new({:left => 1, :right => 2, :parent_id => nil}) }.should raise_error(Sequel::Error)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should have nested_set_options equal to Model.nested_set_options" do
|
114
|
+
@root.nested_set_options.should == Client.nested_set_options
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should have nodes that have common root" do
|
118
|
+
@node1.root.should == @root
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should have nodes that have their parent" do
|
122
|
+
@node2_1.parent.should == @node2
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should have leaf that will be true leaf?" do
|
126
|
+
@root.leaf?.should_not be_true
|
127
|
+
@node2_1.leaf?.should be_true
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should have child that will be true child?" do
|
131
|
+
@root.child?.should_not be_true
|
132
|
+
@node2_1.child?.should be_true
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should have <=> method" do
|
136
|
+
@root.should respond_to(:<=>)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "Should order by left column" do
|
140
|
+
(@node1 <=> @node2).should == -1
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should have level of node" do
|
144
|
+
@root.level.should == 0
|
145
|
+
@node1.level.should == 1
|
146
|
+
@node2.level.should == 1
|
147
|
+
@node2_1.level.should == 2
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should have parent relation" do
|
151
|
+
@node2_1.parent.should == @node2
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should have self_and_sibling that have self node and all its siblings" do
|
155
|
+
@root.self_and_siblings.all.should == [@root, @root2]
|
156
|
+
@node1.self_and_siblings.all.should == [@node1, @node2, @node3]
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should have siblings of node withot itself" do
|
160
|
+
@root.siblings.all.should == [@root2]
|
161
|
+
@node1.siblings.all.should == [@node2, @node3]
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should have self_and_ancestors that have self node and all its ancestors" do
|
165
|
+
@root.self_and_ancestors.all.should == [@root]
|
166
|
+
@node1.self_and_ancestors.all.should == [@root, @node1]
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should have ancestors of node withot itself" do
|
170
|
+
@root.ancestors.all.should == []
|
171
|
+
@node1.ancestors.all.should == [@root]
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should have self_and_descendants that have self node and all its descendents" do
|
175
|
+
@root.self_and_descendants.all.should == [@root, @node1, @node2, @node2_1, @node3]
|
176
|
+
@node2.self_and_descendants.all.should == [@node2, @node2_1]
|
177
|
+
@node2_1.self_and_descendants.all.should == [@node2_1]
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should have descendents that are children and nested children wihout itself" do
|
181
|
+
@root.descendants.all.should == [@node1, @node2, @node2_1, @node3]
|
182
|
+
@node2.descendants.all.should == [@node2_1]
|
183
|
+
@node2_1.descendants.all.should == []
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should have children that returns set of only node immediate children" do
|
187
|
+
@root.children.all.should == [@node1, @node2, @node3]
|
188
|
+
@node2.children.all.should == [@node2_1]
|
189
|
+
@node2_1.children.all.should == []
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should have leaves that are set of all of node nested children which do not have children" do
|
193
|
+
@root.leaves.all.should == [@node1, @node2_1, @node3]
|
194
|
+
@node2.leaves.all.should == [@node2_1]
|
195
|
+
@node2_1.leaves.all.should == []
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should be able to get left sibling" do
|
199
|
+
@node2.left_sibling.should == @node1
|
200
|
+
@node3.left_sibling.should == @node2
|
201
|
+
@node1.left_sibling.should be_nil
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should be able to get proper right sibling" do
|
205
|
+
@node1.right_sibling.should == @node2
|
206
|
+
@node2.right_sibling.should == @node3
|
207
|
+
@node3.right_sibling.should be_nil
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should have node_x.is_or_is_descendant_of?(node_y) that will return proper boolean value" do
|
211
|
+
@node1.is_or_is_descendant_of?(@root).should be_true
|
212
|
+
@node2_1.is_or_is_descendant_of?(@root).should be_true
|
213
|
+
@node2_1.is_or_is_descendant_of?(@node2).should be_true
|
214
|
+
@node2.is_or_is_descendant_of?(@node2_1).should be_false
|
215
|
+
@node2.is_or_is_descendant_of?(@node1).should be_false
|
216
|
+
@node1.is_or_is_descendant_of?(@node1).should be_true
|
217
|
+
end
|
218
|
+
|
219
|
+
it "should have node_x.is_ancestor_of?(node_y) that will return proper boolean value" do
|
220
|
+
@node1.is_descendant_of?(@root).should be_true
|
221
|
+
@node2_1.is_descendant_of?(@root).should be_true
|
222
|
+
@node2_1.is_descendant_of?(@node2).should be_true
|
223
|
+
@node2.is_descendant_of?(@node2_1).should be_false
|
224
|
+
@node2.is_descendant_of?(@node1).should be_false
|
225
|
+
@node1.is_descendant_of?(@node1).should be_false
|
226
|
+
end
|
227
|
+
|
228
|
+
it "should have node_x.is_ancestor_of?(node_y) that will return proper boolean value" do
|
229
|
+
@root.is_ancestor_of?(@node1).should be_true
|
230
|
+
@root.is_ancestor_of?(@node2_1).should be_true
|
231
|
+
@node2.is_ancestor_of?(@node2_1).should be_true
|
232
|
+
@node2_1.is_ancestor_of?(@node2).should be_false
|
233
|
+
@node1.is_ancestor_of?(@node2).should be_false
|
234
|
+
@node1.is_ancestor_of?(@node1).should be_false
|
235
|
+
end
|
236
|
+
|
237
|
+
it "should have node_x.is_or_is_ancestor_of?(node_y) that will return proper boolean value" do
|
238
|
+
@root.is_or_is_ancestor_of?(@node1).should be_true
|
239
|
+
@root.is_or_is_ancestor_of?(@node2_1).should be_true
|
240
|
+
@node2.is_or_is_ancestor_of?(@node2_1).should be_true
|
241
|
+
@node2_1.is_or_is_ancestor_of?(@node2).should be_false
|
242
|
+
@node1.is_or_is_ancestor_of?(@node2).should be_false
|
243
|
+
@node1.is_or_is_ancestor_of?(@node1).should be_true
|
244
|
+
end
|
245
|
+
|
246
|
+
it "should have node2 with left sibling as node1 and node3 with left sibling node2" do
|
247
|
+
@node2.left_sibling.should == @node1
|
248
|
+
@node3.left_sibling.should == @node2
|
249
|
+
end
|
250
|
+
|
251
|
+
it "should have root without left sibling" do
|
252
|
+
@root.left_sibling.should be_nil
|
253
|
+
end
|
254
|
+
|
255
|
+
it "should have node2_1 without left sibling" do
|
256
|
+
@node2_1.left_sibling.should be_nil
|
257
|
+
end
|
258
|
+
|
259
|
+
it "should have node1 without left sibling" do
|
260
|
+
@node1.left_sibling.should be_nil
|
261
|
+
end
|
262
|
+
|
263
|
+
it "should have node2 with right sibling as node3 and node1 with right sibling node2" do
|
264
|
+
@node2.right_sibling.should == @node3
|
265
|
+
@node1.right_sibling.should == @node2
|
266
|
+
end
|
267
|
+
|
268
|
+
it "should have root with right sibling as root2 and root2 with without right sibling" do
|
269
|
+
@root.right_sibling.should == @root2
|
270
|
+
@root2.right_sibling.should be_nil
|
271
|
+
end
|
272
|
+
|
273
|
+
it "should have node2_1 without right sibling" do
|
274
|
+
@node2_1.right_sibling.should be_nil
|
275
|
+
end
|
276
|
+
|
277
|
+
it "should have node3 without right sibling" do
|
278
|
+
@node3.right_sibling.should be_nil
|
279
|
+
end
|
280
|
+
|
281
|
+
it "should have to_text method that returns whole tree of instance node as text" do
|
282
|
+
@root.to_text.should == "* Client (nil, 1, 10)\n** Client (1, 2, 3)\n** Client (1, 4, 7)\n*** Client (3, 5, 6)\n** Client (1, 8, 9)"
|
283
|
+
end
|
284
|
+
|
285
|
+
it "should have to_text method that returns whole tree of instance node as text and should be able to pass block" do
|
286
|
+
@root.to_text { |node| node.name }.should == "* Top Level (nil, 1, 10)\n** Child 1 (1, 2, 3)\n** Child 2 (1, 4, 7)\n*** Child 2.1 (3, 5, 6)\n** Child 3 (1, 8, 9)"
|
287
|
+
end
|
288
|
+
|
289
|
+
it "should node2 be able to move to the left" do
|
290
|
+
@node2.move_left
|
291
|
+
@node2.left_sibling.should be_nil
|
292
|
+
@node2.right_sibling.should == @node1.refresh
|
293
|
+
Client.valid?.should be_true
|
294
|
+
end
|
295
|
+
|
296
|
+
it "should node2 be able to move to the right" do
|
297
|
+
@node2.move_right
|
298
|
+
@node2.right_sibling.should be_nil
|
299
|
+
@node2.left_sibling.should == @node3.refresh
|
300
|
+
Client.valid?.should be_true
|
301
|
+
end
|
302
|
+
|
303
|
+
it "should node3 be able to move to the left of node1" do
|
304
|
+
@node3.move_to_left_of(@node1)
|
305
|
+
@node3.left_sibling.should be_nil
|
306
|
+
@node3.right_sibling.should == @node1.refresh
|
307
|
+
Client.valid?.should be_true
|
308
|
+
end
|
309
|
+
|
310
|
+
it "should node1 be able to move to the right of node1" do
|
311
|
+
@node1.move_to_right_of(@node3)
|
312
|
+
@node1.right_sibling.should be_nil
|
313
|
+
@node1.left_sibling.should == @node3.refresh
|
314
|
+
Client.valid?.should be_true
|
315
|
+
end
|
316
|
+
|
317
|
+
it "should node2 be able to became root" do
|
318
|
+
@node2.move_to_root
|
319
|
+
@node2.parent.should be_nil
|
320
|
+
@node2.level.should == 0
|
321
|
+
@node2_1.level.should == 1
|
322
|
+
@node2.left == 1
|
323
|
+
@node2.right == 4
|
324
|
+
Client.valid?.should be_true
|
325
|
+
end
|
326
|
+
|
327
|
+
it "should node1 be able to move to child of node3" do
|
328
|
+
@node1.move_to_child_of(@node3)
|
329
|
+
@node1.parent_id.should == @node3.id
|
330
|
+
Client.valid?.should be_true
|
331
|
+
end
|
332
|
+
|
333
|
+
it "should be able to move new node to the end of root children" do
|
334
|
+
child = Client.create(:name => 'New Child')
|
335
|
+
child.move_to_child_of(@root)
|
336
|
+
@root.children.last.should == child
|
337
|
+
Client.valid?.should be_true
|
338
|
+
end
|
339
|
+
|
340
|
+
it "should be able to move node2 as child of node1" do
|
341
|
+
@node2.left.should == 4
|
342
|
+
@node2.right.should == 7
|
343
|
+
@node1.left.should == 2
|
344
|
+
@node1.right.should == 3
|
345
|
+
@node2.move_to_child_of(@node1)
|
346
|
+
@node2.parent_id.should == @node1.id
|
347
|
+
Client.valid?.should be_true
|
348
|
+
@node2.left.should == 3
|
349
|
+
@node2.right.should == 6
|
350
|
+
@node1.left.should == 2
|
351
|
+
@node1.right.should == 7
|
352
|
+
end
|
353
|
+
|
354
|
+
it "should be able to move root node to child of new node" do
|
355
|
+
@root2.left.should == 11
|
356
|
+
@root2.right.should == 12
|
357
|
+
|
358
|
+
root3 = Client.create(:name => 'New Root')
|
359
|
+
root3.left.should == 13
|
360
|
+
root3.right.should == 14
|
361
|
+
|
362
|
+
@root2.move_to_child_of(root3)
|
363
|
+
|
364
|
+
Client.valid?.should be_true
|
365
|
+
@root2.parent_id.should == root3.id
|
366
|
+
|
367
|
+
@root2.left.should == 12
|
368
|
+
@root2.right.should == 13
|
369
|
+
|
370
|
+
root3.left.should == 11
|
371
|
+
root3.right.should == 14
|
372
|
+
end
|
373
|
+
|
374
|
+
it "should be able to move root node to child of new node" do
|
375
|
+
@root.left.should == 1
|
376
|
+
@root.right.should == 10
|
377
|
+
@node2_1.left.should == 5
|
378
|
+
@node2_1.right.should == 6
|
379
|
+
|
380
|
+
root3 = Client.create(:name => 'New Root')
|
381
|
+
@root.move_to_child_of(root3)
|
382
|
+
Client.valid?.should be_true
|
383
|
+
@root.parent_id.should == root3.id
|
384
|
+
|
385
|
+
@root.left.should == 4
|
386
|
+
@root.right.should == 13
|
387
|
+
|
388
|
+
@node2_1.refresh
|
389
|
+
@node2_1.left.should == 8
|
390
|
+
@node2_1.right.should == 9
|
391
|
+
end
|
392
|
+
|
393
|
+
it "should be able to rebuild whole tree" do
|
394
|
+
node1 = Client.create(:name => 'Node-1')
|
395
|
+
node2 = Client.create(:name => 'Node-2')
|
396
|
+
node3 = Client.create(:name => 'Node-3')
|
397
|
+
|
398
|
+
node2.move_to_child_of node1
|
399
|
+
node3.move_to_child_of node1
|
400
|
+
|
401
|
+
output = Client.roots.last.to_text
|
402
|
+
Client.update('lft = null, rgt = null')
|
403
|
+
Client.rebuild!
|
404
|
+
|
405
|
+
Client.roots.last.to_text.should == output
|
406
|
+
end
|
407
|
+
|
408
|
+
it "should be invalid which lft = null" do
|
409
|
+
Client.valid?.should be_true
|
410
|
+
Client.update("lft = NULL")
|
411
|
+
Client.valid?.should be_false
|
412
|
+
end
|
413
|
+
|
414
|
+
it "should be invalid which rgt = null" do
|
415
|
+
Client.valid?.should be_true
|
416
|
+
Client.update("rgt = NULL")
|
417
|
+
Client.valid?.should be_false
|
418
|
+
end
|
419
|
+
|
420
|
+
it "should be valid with mising intermediate node" do
|
421
|
+
Client.valid?.should be_true
|
422
|
+
@node2.destroy
|
423
|
+
Client.valid?.should be_true
|
424
|
+
end
|
425
|
+
|
426
|
+
it "should be invalid with overlapping right nodes" do
|
427
|
+
Client.valid?.should be_true
|
428
|
+
@root2[:lft] = 0
|
429
|
+
@root2.save
|
430
|
+
Client.valid?.should be_false
|
431
|
+
end
|
432
|
+
|
433
|
+
it "should be able to rebild" do
|
434
|
+
Client.valid?.should be_true
|
435
|
+
before_text = Client.root.to_text
|
436
|
+
Client.update('lft = NULL, rgt = NULL')
|
437
|
+
Client.rebuild!
|
438
|
+
Client.valid?.should be_true
|
439
|
+
before_text.should == Client.root.to_text
|
440
|
+
end
|
441
|
+
|
442
|
+
it "shold be able to move for sibbling" do
|
443
|
+
@node2.move_possible?(@node1).should be_true
|
444
|
+
end
|
445
|
+
|
446
|
+
it "shold not be able to move for itself" do
|
447
|
+
@root.move_possible?(@root).should be_false
|
448
|
+
end
|
449
|
+
|
450
|
+
it "should not be able to move for parent" do
|
451
|
+
@root.descendants.each do |descendant_node|
|
452
|
+
@root.move_possible?(descendant_node).should be_false
|
453
|
+
descendant_node.move_possible?(@root).should be_true
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
it "should be correct is_or_is_ancestor_of?" do
|
458
|
+
[@node1, @node2, @node2_1, @node3].each do |node|
|
459
|
+
@root.is_or_is_ancestor_of?(node).should be_true
|
460
|
+
end
|
461
|
+
@root.is_or_is_ancestor_of?(@root2).should be_false
|
462
|
+
end
|
463
|
+
|
464
|
+
it "should be invalid left_and_rights_valid? for nil lefts" do
|
465
|
+
Client.left_and_rights_valid?.should be_true
|
466
|
+
@node2[:lft] = nil
|
467
|
+
@node2.save
|
468
|
+
Client.left_and_rights_valid?.should be_false
|
469
|
+
end
|
470
|
+
|
471
|
+
it "should be invalid left_and_rights_valid? for nil rights" do
|
472
|
+
Client.left_and_rights_valid?.should be_true
|
473
|
+
@node2[:rgt] = nil
|
474
|
+
@node2.save
|
475
|
+
Client.left_and_rights_valid?.should be_false
|
476
|
+
end
|
477
|
+
|
478
|
+
it "should return true for left_and_rights_valid? when node lft is equal for root lft" do
|
479
|
+
Client.left_and_rights_valid?.should be_true
|
480
|
+
@node2[:lft] = @root[:lft]
|
481
|
+
@node2.save
|
482
|
+
Client.left_and_rights_valid?.should be_false
|
483
|
+
end
|
484
|
+
|
485
|
+
it "should return true for left_and_rights_valid? when node rgt is equal for root rgt" do
|
486
|
+
Client.left_and_rights_valid?.should be_true
|
487
|
+
@node2[:rgt] = @root[:rgt]
|
488
|
+
@node2.save
|
489
|
+
Client.left_and_rights_valid?.should be_false
|
490
|
+
end
|
491
|
+
|
492
|
+
it "should be valid after moving dirty nodes" do
|
493
|
+
n1 = Client.create
|
494
|
+
n2 = Client.create
|
495
|
+
n3 = Client.create
|
496
|
+
n4 = Client.create
|
497
|
+
|
498
|
+
n2.move_to_child_of(n1)
|
499
|
+
Client.valid?.should be_true
|
500
|
+
|
501
|
+
n3.move_to_child_of(n1)
|
502
|
+
Client.valid?.should be_true
|
503
|
+
|
504
|
+
n4.move_to_child_of(n2)
|
505
|
+
Client.valid?.should be_true
|
506
|
+
end
|
507
|
+
|
508
|
+
it "should have to_nested_a method that returns nested array of all nodes from roots to leaves" do
|
509
|
+
@root.to_nested_a.should == [@root, [@node1], [@node2, [@node2_1]], [@node3]]
|
510
|
+
end
|
511
|
+
|
512
|
+
it "should have to_nested_a method that can pass block with node and level" do
|
513
|
+
@root.to_nested_a { |node, level| level }.should == [0, [1], [1, [2]], [1]]
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
describe "wiht scope" do
|
519
|
+
describe "ClassMethods" do
|
520
|
+
it "should be no duplicates for columns accross different scopes" do
|
521
|
+
|
522
|
+
end
|
523
|
+
|
524
|
+
it "should have all roots valid accross different scopes" do
|
525
|
+
|
526
|
+
end
|
527
|
+
|
528
|
+
it "should have multi scope" do
|
529
|
+
|
530
|
+
end
|
531
|
+
|
532
|
+
it "should be able to rebuild! accross different scopes" do
|
533
|
+
|
534
|
+
end
|
535
|
+
|
536
|
+
it "should have same_scope? true for nodes in the same scope" do
|
537
|
+
|
538
|
+
end
|
539
|
+
|
540
|
+
it "should have equal nodes in the same scope" do
|
541
|
+
|
542
|
+
end
|
543
|
+
|
544
|
+
it "should @root and @node be in same scope" do
|
545
|
+
# @root.same_scope?(@node).should be_true
|
546
|
+
end
|
547
|
+
|
548
|
+
it "should @root and @root_in_other_scope be in different scope" do
|
549
|
+
|
550
|
+
end
|
551
|
+
|
552
|
+
# def test_multi_scoped_no_duplicates_for_columns?
|
553
|
+
# assert_nothing_raised do
|
554
|
+
# Note.no_duplicates_for_columns?
|
555
|
+
# end
|
556
|
+
# end
|
557
|
+
#
|
558
|
+
# def test_multi_scoped_all_roots_valid?
|
559
|
+
# assert_nothing_raised do
|
560
|
+
# Note.all_roots_valid?
|
561
|
+
# end
|
562
|
+
# end
|
563
|
+
#
|
564
|
+
# def test_multi_scoped
|
565
|
+
# note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
|
566
|
+
# note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
|
567
|
+
# note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
|
568
|
+
#
|
569
|
+
# assert_equal [note1, note2], note1.self_and_siblings
|
570
|
+
# assert_equal [note3], note3.self_and_siblings
|
571
|
+
# end
|
572
|
+
#
|
573
|
+
# def test_multi_scoped_rebuild
|
574
|
+
# root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
|
575
|
+
# child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
|
576
|
+
# child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
|
577
|
+
#
|
578
|
+
# child1.move_to_child_of root
|
579
|
+
# child2.move_to_child_of root
|
580
|
+
#
|
581
|
+
# Note.update_all('lft = null, rgt = null')
|
582
|
+
# Note.rebuild!
|
583
|
+
#
|
584
|
+
# assert_equal Note.roots.find_by_body('A'), root
|
585
|
+
# assert_equal [child1, child2], Note.roots.find_by_body('A').children
|
586
|
+
# end
|
587
|
+
#
|
588
|
+
# def test_same_scope_with_multi_scopes
|
589
|
+
# assert_nothing_raised do
|
590
|
+
# notes(:scope1).same_scope?(notes(:child_1))
|
591
|
+
# end
|
592
|
+
# assert notes(:scope1).same_scope?(notes(:child_1))
|
593
|
+
# assert notes(:child_1).same_scope?(notes(:scope1))
|
594
|
+
# assert !notes(:scope1).same_scope?(notes(:scope2))
|
595
|
+
# end
|
596
|
+
#
|
597
|
+
# def test_quoting_of_multi_scope_column_names
|
598
|
+
# assert_equal ["\"notable_id\"", "\"notable_type\""], Note.quoted_scope_column_names
|
599
|
+
# end
|
600
|
+
#
|
601
|
+
# def test_equal_in_same_scope
|
602
|
+
# assert_equal notes(:scope1), notes(:scope1)
|
603
|
+
# assert_not_equal notes(:scope1), notes(:child_1)
|
604
|
+
# end
|
605
|
+
#
|
606
|
+
# def test_equal_in_different_scopes
|
607
|
+
# assert_not_equal notes(:scope1), notes(:scope2)
|
608
|
+
# end
|
609
|
+
end
|
610
|
+
|
611
|
+
describe "InstanceMethods" do
|
612
|
+
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
end
|
617
|
+
|
data/spec/rcov.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-x gems,spec
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'sequel'
|
4
|
+
require 'logger'
|
5
|
+
require 'ruby-debug'
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../lib/sequel_nested_set'
|
8
|
+
|
9
|
+
DB = Sequel.sqlite # memory database
|
10
|
+
DB.logger = Logger.new('log/db.log')
|
11
|
+
|
12
|
+
class Client < Sequel::Model
|
13
|
+
is :nested_set
|
14
|
+
set_schema do
|
15
|
+
primary_key :id
|
16
|
+
column :name, :text
|
17
|
+
column :parent_id, :integer
|
18
|
+
column :lft, :integer
|
19
|
+
column :rgt, :integer
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Client.create_table
|
24
|
+
|
25
|
+
def prepare_nested_set_data
|
26
|
+
Client.drop_table if Client.table_exists?
|
27
|
+
Client.create_table!
|
28
|
+
DB[:clients] << {"name"=>"Top Level 2", "lft"=>11, "id"=>6, "rgt"=>12}
|
29
|
+
DB[:clients] << {"name"=>"Child 2.1", "lft"=>5, "id"=>4, "parent_id"=>3, "rgt"=>6}
|
30
|
+
DB[:clients] << {"name"=>"Child 1", "lft"=>2, "id"=>2, "parent_id"=>1, "rgt"=>3}
|
31
|
+
DB[:clients] << {"name"=>"Top Level", "lft"=>1, "id"=>1, "rgt"=>10}
|
32
|
+
DB[:clients] << {"name"=>"Child 2", "lft"=>4, "id"=>3, "parent_id"=>1, "rgt"=>7}
|
33
|
+
DB[:clients] << {"name"=>"Child 3", "lft"=>8, "id"=>5, "parent_id"=>1, "rgt"=>9}
|
34
|
+
end
|
35
|
+
|
36
|
+
prepare_nested_set_data
|
37
|
+
|
38
|
+
|
39
|
+
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pkondzior-sequel_nested_set
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Pawe\xC5\x82 Kondzior"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-11 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: sequel
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.8.0
|
23
|
+
version:
|
24
|
+
description: Nested set implementation, ported from the Awesome Nested Set Active Record plugin.
|
25
|
+
email: kondzior.p@gmail.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files:
|
31
|
+
- TODO
|
32
|
+
- COPYING
|
33
|
+
- README
|
34
|
+
files:
|
35
|
+
- lib/sequel_nested_set.rb
|
36
|
+
- TODO
|
37
|
+
- COPYING
|
38
|
+
- README
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://sequelns.rubyforge.org/
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options:
|
43
|
+
- --quiet
|
44
|
+
- --title
|
45
|
+
- Sequel Nested Set
|
46
|
+
- --opname
|
47
|
+
- index.html
|
48
|
+
- --line-numbers
|
49
|
+
- --main
|
50
|
+
- README
|
51
|
+
- --inline-source
|
52
|
+
- --charset
|
53
|
+
- utf8
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.2.0
|
72
|
+
signing_key:
|
73
|
+
specification_version: 2
|
74
|
+
summary: Nested set implementation for Sequel Models
|
75
|
+
test_files:
|
76
|
+
- spec/nested_set_spec.rb
|
77
|
+
- spec/rcov.opts
|
78
|
+
- spec/spec.opts
|
79
|
+
- spec/spec_helper.rb
|