simple_nested_set 0.0.1 → 0.0.2
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/lib/simple_nested_set/act_macro.rb +5 -18
- data/lib/simple_nested_set/class_methods.rb +39 -23
- data/lib/simple_nested_set/instance_methods.rb +40 -221
- data/lib/simple_nested_set/move/by_attributes.rb +64 -0
- data/lib/simple_nested_set/move/protection.rb +53 -0
- data/lib/simple_nested_set/move/to_target.rb +97 -0
- data/lib/simple_nested_set/nested_set.rb +101 -0
- data/lib/simple_nested_set/version.rb +1 -1
- data/lib/simple_nested_set.rb +7 -3
- metadata +8 -4
@@ -10,29 +10,16 @@ module SimpleNestedSet
|
|
10
10
|
# define_callbacks :move, :terminator => "result == false"
|
11
11
|
# before_move :init_as_node
|
12
12
|
|
13
|
-
before_validation
|
14
|
-
before_destroy
|
13
|
+
before_validation lambda { |r| r.nested_set.init_as_node }
|
14
|
+
before_destroy lambda { |r| r.nested_set.prune_branch }
|
15
|
+
|
15
16
|
belongs_to :parent, :class_name => self.name
|
16
17
|
has_many :children, :foreign_key => :parent_id, :class_name => self.base_class.name
|
17
18
|
|
18
19
|
default_scope :order => :lft
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
nested_set_proc = lambda do |*args|
|
24
|
-
args.empty? ? {} : { :conditions => nested_set.conditions(*args) }
|
25
|
-
end
|
26
|
-
|
27
|
-
scope :nested_set, nested_set_proc do
|
28
|
-
define_method(:scope_columns) { scopes }
|
29
|
-
define_method(:klass) { klass }
|
30
|
-
define_method(:conditions) { |record| scopes.inject({}) { |c, name| c.merge(name => record[name]) } }
|
31
|
-
end
|
32
|
-
|
33
|
-
scope :with_levels, lambda {
|
34
|
-
{ :select => "COUNT(id) AS level" } # TODO ... ?
|
35
|
-
}
|
21
|
+
class_inheritable_accessor :nested_set_class
|
22
|
+
self.nested_set_class = NestedSet.build_class(self, options[:scope])
|
36
23
|
end
|
37
24
|
|
38
25
|
def acts_as_nested_set?
|
@@ -2,43 +2,59 @@ require 'active_support/core_ext/hash/slice'
|
|
2
2
|
|
3
3
|
module SimpleNestedSet
|
4
4
|
module ClassMethods
|
5
|
-
NESTED_SET_ATTRIBUTES = [:parent_id, :left_id, :right_id]
|
6
|
-
|
7
5
|
def create(attributes)
|
8
|
-
with_move_by_attributes(attributes) { super }
|
6
|
+
nested_set_class.with_move_by_attributes(attributes) { super }
|
9
7
|
end
|
10
8
|
|
11
9
|
def create!(attributes)
|
12
|
-
with_move_by_attributes(attributes) { super }
|
10
|
+
nested_set_class.with_move_by_attributes(attributes) { super }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the first root node (with the given scope if any)
|
14
|
+
def root(scope = nil)
|
15
|
+
roots(scope).first
|
13
16
|
end
|
14
17
|
|
15
|
-
# Returns the
|
16
|
-
def
|
17
|
-
|
18
|
+
# Returns root nodes (with the given scope if any)
|
19
|
+
def roots(scope = nil)
|
20
|
+
nested_set_class.scope(scope).without_parent
|
18
21
|
end
|
19
22
|
|
20
23
|
# Returns roots when multiple roots (or virtual root, which is the same)
|
21
|
-
def
|
22
|
-
|
24
|
+
def leaves(scope = nil)
|
25
|
+
nested_set_class.scope(scope).with_leaves
|
26
|
+
end
|
27
|
+
|
28
|
+
def without_node(id)
|
29
|
+
where(arel_table[:id].not_eq(id))
|
30
|
+
end
|
31
|
+
|
32
|
+
def without_parent
|
33
|
+
with_parent(nil)
|
23
34
|
end
|
24
35
|
|
25
|
-
def
|
26
|
-
|
36
|
+
def with_parent(parent_id)
|
37
|
+
where(:parent_id => parent_id)
|
27
38
|
end
|
28
39
|
|
29
|
-
|
40
|
+
def with_ancestors(lft, rgt)
|
41
|
+
where(arel_table[:lft].lt(lft).and(arel_table[:rgt].gt(rgt)))
|
42
|
+
end
|
43
|
+
|
44
|
+
def with_descendants(lft, rgt)
|
45
|
+
where(arel_table[:lft].gt(lft).and(arel_table[:rgt].lt(rgt)))
|
46
|
+
end
|
30
47
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
yield.tap { |record| record.send(:move_by_attributes, nested_set_attributes) }
|
35
|
-
end
|
36
|
-
end
|
48
|
+
def with_left_sibling(lft)
|
49
|
+
where(:rgt => lft - 1)
|
50
|
+
end
|
37
51
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
52
|
+
def with_right_sibling(rgt)
|
53
|
+
where(:lft => rgt + 1)
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_leaves
|
57
|
+
where("#{arel_table[:lft].to_sql} = #{arel_table[:rgt].to_sql} - 1")
|
58
|
+
end
|
43
59
|
end
|
44
60
|
end
|
@@ -2,19 +2,24 @@ require 'active_support/core_ext/hash/keys'
|
|
2
2
|
|
3
3
|
module SimpleNestedSet
|
4
4
|
module InstanceMethods
|
5
|
+
def nested_set
|
6
|
+
@nested_set ||= nested_set_class.new(self)
|
7
|
+
end
|
8
|
+
|
9
|
+
# TODO refactor
|
5
10
|
def update_attributes(attributes)
|
6
|
-
|
7
|
-
super
|
11
|
+
nested_set.with_move_by_attributes(attributes) { super }
|
8
12
|
end
|
9
13
|
|
10
14
|
def update_attributes!(attributes)
|
11
|
-
|
12
|
-
super
|
15
|
+
nested_set.with_move_by_attributes(attributes) { super }
|
13
16
|
end
|
14
17
|
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
+
# recursively populates the parent and children associations of self and
|
19
|
+
# all descendants using one query
|
20
|
+
def load_tree
|
21
|
+
nested_set.populate_associations(descendants)
|
22
|
+
self
|
18
23
|
end
|
19
24
|
|
20
25
|
# Returns the level of this object in the tree, root level is 0
|
@@ -27,13 +32,14 @@ module SimpleNestedSet
|
|
27
32
|
parent_id.blank?
|
28
33
|
end
|
29
34
|
|
30
|
-
# Returns true
|
35
|
+
# Returns true if this is a child node
|
31
36
|
def child?
|
32
37
|
!root?
|
33
38
|
end
|
34
39
|
|
40
|
+
# Returns true if this is a leaf node
|
35
41
|
def leaf?
|
36
|
-
rgt - lft == 1
|
42
|
+
rgt.to_i - lft.to_i == 1
|
37
43
|
end
|
38
44
|
|
39
45
|
# compare by left column
|
@@ -48,20 +54,22 @@ module SimpleNestedSet
|
|
48
54
|
|
49
55
|
# Returns the parent
|
50
56
|
def parent
|
51
|
-
|
57
|
+
self.class.find(parent_id) unless root?
|
52
58
|
end
|
53
59
|
|
60
|
+
# Returns true if this is an ancestor of the given node
|
54
61
|
def ancestor_of?(other)
|
55
62
|
lft < other.lft && rgt > other.rgt
|
56
63
|
end
|
57
64
|
|
65
|
+
# Returns true if this is equal to or an ancestor of the given node
|
58
66
|
def self_or_ancestor_of?(other)
|
59
67
|
self == other || ancestor_of?(other)
|
60
68
|
end
|
61
69
|
|
62
70
|
# Returns an array of all parents
|
63
71
|
def ancestors
|
64
|
-
nested_set.
|
72
|
+
nested_set.with_ancestors(lft, rgt)
|
65
73
|
end
|
66
74
|
|
67
75
|
# Returns the array of all parents and self
|
@@ -69,17 +77,19 @@ module SimpleNestedSet
|
|
69
77
|
ancestors + [self]
|
70
78
|
end
|
71
79
|
|
80
|
+
# Returns true if this is a descendent of the given node
|
72
81
|
def descendent_of?(other)
|
73
82
|
lft > other.lft && rgt < other.rgt
|
74
83
|
end
|
75
84
|
|
85
|
+
# Returns true if this is equal to or a descendent of the given node
|
76
86
|
def self_or_descendent_of?(other)
|
77
87
|
self == other || descendent_of?(other)
|
78
88
|
end
|
79
89
|
|
80
90
|
# Returns a set of all of its children and nested children.
|
81
91
|
def descendants
|
82
|
-
rgt - lft == 1 ? [] : nested_set.
|
92
|
+
rgt - lft == 1 ? [] : nested_set.with_descendants(lft, rgt)
|
83
93
|
end
|
84
94
|
|
85
95
|
# Returns a set of itself and all of its nested children.
|
@@ -87,16 +97,11 @@ module SimpleNestedSet
|
|
87
97
|
[self] + descendants
|
88
98
|
end
|
89
99
|
|
90
|
-
# Returns the number of
|
91
|
-
def
|
100
|
+
# Returns the number of descendants
|
101
|
+
def descendants_count
|
92
102
|
rgt > lft ? (rgt - lft - 1) / 2 : 0
|
93
103
|
end
|
94
104
|
|
95
|
-
# # Returns a set of only this entry's immediate children
|
96
|
-
# def children
|
97
|
-
# rgt - lft == 1 ? [] : nested_set.scoped(:conditions => { :parent_id => id })
|
98
|
-
# end
|
99
|
-
|
100
105
|
# Returns a set of only this entry's immediate children including self
|
101
106
|
def self_and_children
|
102
107
|
[self] + children
|
@@ -104,251 +109,65 @@ module SimpleNestedSet
|
|
104
109
|
|
105
110
|
# Returns true if the node has any children
|
106
111
|
def children?
|
107
|
-
|
112
|
+
descendants_count > 0
|
108
113
|
end
|
109
114
|
alias has_children? children?
|
110
115
|
|
111
116
|
# Returns the array of all children of the parent, except self
|
112
117
|
def siblings
|
113
|
-
|
118
|
+
self_and_siblings.without_node(id)
|
114
119
|
end
|
115
120
|
|
116
121
|
# Returns the array of all children of the parent, included self
|
117
122
|
def self_and_siblings
|
118
|
-
nested_set.
|
123
|
+
nested_set.with_parent(parent_id)
|
119
124
|
end
|
120
125
|
|
121
126
|
# Returns the lefthand sibling
|
122
127
|
def previous_sibling
|
123
|
-
nested_set.first
|
128
|
+
nested_set.with_left_sibling(lft).first
|
124
129
|
end
|
125
130
|
alias left_sibling previous_sibling
|
126
131
|
|
127
132
|
# Returns the righthand sibling
|
128
133
|
def next_sibling
|
129
|
-
nested_set.first
|
134
|
+
nested_set.with_right_sibling(rgt).first
|
130
135
|
end
|
131
136
|
alias right_sibling next_sibling
|
132
137
|
|
133
|
-
# Returns all
|
138
|
+
# Returns all descendants that are leaves
|
134
139
|
def leaves
|
135
|
-
rgt - lft == 1 ? [] : nested_set.
|
140
|
+
rgt - lft == 1 ? [] : nested_set.with_descendants(lft, rgt).with_leaves
|
136
141
|
end
|
137
142
|
|
138
|
-
#
|
143
|
+
# Moves the node to the child of another node
|
139
144
|
def move_to_child_of(node)
|
140
|
-
node ? move_to(node, :child) : move_to_root
|
145
|
+
node ? nested_set.move_to(node, :child) : move_to_root
|
141
146
|
end
|
142
147
|
|
148
|
+
# Makes this node a root node
|
143
149
|
def move_to_root
|
144
|
-
move_to(nil, :root)
|
150
|
+
nested_set.move_to(nil, :root)
|
145
151
|
end
|
146
152
|
|
147
|
-
#
|
153
|
+
# Moves the node to the left of its left sibling if any
|
148
154
|
def move_left
|
149
155
|
move_to_left_of(left_sibling) if left_sibling
|
150
156
|
end
|
151
157
|
|
152
|
-
#
|
158
|
+
# Moves the node to the right of its right sibling if any
|
153
159
|
def move_right
|
154
160
|
move_to_right_of(right_sibling) if right_sibling
|
155
161
|
end
|
156
162
|
|
157
163
|
# Move the node to the left of another node
|
158
164
|
def move_to_left_of(node)
|
159
|
-
move_to(node, :left)
|
165
|
+
nested_set.move_to(node, :left)
|
160
166
|
end
|
161
167
|
|
162
168
|
# Move the node to the left of another node
|
163
169
|
def move_to_right_of(node)
|
164
|
-
move_to(node, :right)
|
165
|
-
end
|
166
|
-
|
167
|
-
protected
|
168
|
-
|
169
|
-
def nested_set
|
170
|
-
@nested_set ||= self.class.nested_set(self)
|
171
|
-
end
|
172
|
-
|
173
|
-
def without_self(scope)
|
174
|
-
scope.scoped :conditions => ["#{self.class.table_name}.id <> ?", id]
|
175
|
-
end
|
176
|
-
|
177
|
-
# before validation set lft and rgt to the end of the tree
|
178
|
-
def init_as_node
|
179
|
-
if lft.nil? || rgt.nil?
|
180
|
-
max_right = nested_set.maximum(:rgt) || 0
|
181
|
-
self.lft = max_right + 1
|
182
|
-
self.rgt = max_right + 2
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# Prunes a branch off of the tree, shifting all of the elements on the right
|
187
|
-
# back to the left so the counts still work.
|
188
|
-
def prune_branch
|
189
|
-
unless rgt.nil? || lft.nil?
|
190
|
-
diff = rgt - lft + 1
|
191
|
-
self.class.transaction {
|
192
|
-
nested_set.delete_all "lft > #{lft} AND rgt < #{rgt}"
|
193
|
-
nested_set.update_all "lft = (lft - #{diff})", "lft >= #{rgt}"
|
194
|
-
nested_set.update_all "rgt = (rgt - #{diff} )", "rgt >= #{rgt}"
|
195
|
-
}
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
# reload left, right, and parent
|
200
|
-
def reload_nested_set
|
201
|
-
reload :select => 'lft, rgt, parent_id'
|
202
|
-
end
|
203
|
-
|
204
|
-
def move_by_attributes(attributes)
|
205
|
-
return unless attributes.detect { |key, value| [:parent_id, :left_id, :right_id].include?(key.to_sym) }
|
206
|
-
|
207
|
-
attributes.symbolize_keys!
|
208
|
-
attributes.each { |key, value| attributes[key] = nil if value == 'null' }
|
209
|
-
|
210
|
-
parent_id = attributes[:parent_id] ? attributes[:parent_id] : self.parent_id
|
211
|
-
parent = parent_id.blank? ? nil : nested_set.klass.find(parent_id)
|
212
|
-
|
213
|
-
# if left_id is given but blank, set right_id to leftmost sibling
|
214
|
-
if attributes.has_key?(:left_id) && attributes[:left_id].blank?
|
215
|
-
attributes.delete(:left_id)
|
216
|
-
siblings = parent ? parent.children : self.class.roots(self)
|
217
|
-
attributes[:right_id] = siblings.first.id if siblings.first
|
218
|
-
end
|
219
|
-
|
220
|
-
# if right_id is given but blank, set left_id to rightmost sibling
|
221
|
-
if attributes.has_key?(:right_id) && attributes[:right_id].blank?
|
222
|
-
attributes.delete(:right_id)
|
223
|
-
siblings = parent ? parent.children : self.class.roots(self)
|
224
|
-
attributes[:left_id] = siblings.last.id if siblings.last
|
225
|
-
end
|
226
|
-
|
227
|
-
parent_id, left_id, right_id = [:parent_id, :left_id, :right_id].map do |key|
|
228
|
-
value = attributes.delete(key)
|
229
|
-
value.blank? ? nil : value.to_i
|
230
|
-
end
|
231
|
-
|
232
|
-
protect_inconsistent_move!(parent_id, left_id, right_id)
|
233
|
-
|
234
|
-
if left_id && left_id != id
|
235
|
-
move_to_right_of(left_id)
|
236
|
-
elsif right_id && right_id != id
|
237
|
-
move_to_left_of(right_id)
|
238
|
-
elsif parent_id != self.parent_id
|
239
|
-
move_to_child_of(parent_id)
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
def move_to(target, position)
|
244
|
-
# return if _run_before_move_callbacks == false
|
245
|
-
|
246
|
-
transaction do
|
247
|
-
target.reload_nested_set if target.is_a?(nested_set.klass)
|
248
|
-
self.reload_nested_set
|
249
|
-
|
250
|
-
target = nested_set.klass.find(target) if target && !target.is_a?(ActiveRecord::Base)
|
251
|
-
protect_impossible_move!(position, target) if target
|
252
|
-
|
253
|
-
bound = case position
|
254
|
-
when :child
|
255
|
-
target.rgt
|
256
|
-
when :left
|
257
|
-
target.lft
|
258
|
-
when :right
|
259
|
-
target.rgt + 1
|
260
|
-
when :root
|
261
|
-
roots = self.class.roots
|
262
|
-
roots.empty? ? 1 : roots.last.rgt + 1
|
263
|
-
end
|
264
|
-
|
265
|
-
if bound > rgt
|
266
|
-
bound -= 1
|
267
|
-
other_bound = rgt + 1
|
268
|
-
else
|
269
|
-
other_bound = lft - 1
|
270
|
-
end
|
271
|
-
|
272
|
-
# there would be no change
|
273
|
-
return if bound == rgt || bound == lft
|
274
|
-
|
275
|
-
# we have defined the boundaries of two non-overlapping intervals,
|
276
|
-
# so sorting puts both the intervals and their boundaries in order
|
277
|
-
a, b, c, d = [lft, rgt, bound, other_bound].sort
|
278
|
-
|
279
|
-
parent_id = case position
|
280
|
-
when :child; target.id
|
281
|
-
when :root; nil
|
282
|
-
else target.parent_id
|
283
|
-
end
|
284
|
-
|
285
|
-
sql = <<-sql
|
286
|
-
lft = CASE
|
287
|
-
WHEN lft BETWEEN :a AND :b THEN lft + :d - :b
|
288
|
-
WHEN lft BETWEEN :c AND :d THEN lft + :a - :c
|
289
|
-
ELSE lft END,
|
290
|
-
|
291
|
-
rgt = CASE
|
292
|
-
WHEN rgt BETWEEN :a AND :b THEN rgt + :d - :b
|
293
|
-
WHEN rgt BETWEEN :c AND :d THEN rgt + :a - :c
|
294
|
-
ELSE rgt END,
|
295
|
-
|
296
|
-
parent_id = CASE
|
297
|
-
WHEN id = :id THEN :parent_id
|
298
|
-
ELSE parent_id END
|
299
|
-
sql
|
300
|
-
args = { :a => a, :b => b, :c => c, :d => d, :id => id, :parent_id => parent_id }
|
301
|
-
nested_set.klass.update_all [sql, args], nested_set.conditions(self)
|
302
|
-
|
303
|
-
target.reload_nested_set if target
|
304
|
-
reload_nested_set
|
305
|
-
|
306
|
-
# _run_after_move_callbacks
|
307
|
-
end
|
308
|
-
end
|
309
|
-
|
310
|
-
def protect_impossible_move!(position, target)
|
311
|
-
positions = [:child, :left, :right, :root]
|
312
|
-
impossible_move!("Position must be one of #{positions.inspect} but is #{position.inspect}.") unless positions.include?(position)
|
313
|
-
impossible_move!("A new node can not be moved") if new_record?
|
314
|
-
impossible_move!("A node can't be moved to itself") if self == target
|
315
|
-
impossible_move!("A node can't be moved to a descendant of itself.") if (lft..rgt).include?(target.lft) && (lft..rgt).include?(target.rgt)
|
316
|
-
impossible_move!("A node can't be moved to a different scope") unless same_scope?(target)
|
317
|
-
end
|
318
|
-
|
319
|
-
def protect_inconsistent_move!(parent_id, left_id, right_id)
|
320
|
-
left = self.class.find(left_id) if left_id
|
321
|
-
right = self.class.find(right_id) if right_id
|
322
|
-
|
323
|
-
if left && right && (!left.right_sibling || left.right_sibling.id != right_id)
|
324
|
-
inconsistent_move! <<-msg
|
325
|
-
Both :left_id (#{left_id.inspect}) and :right_id (#{right_id.inspect}) were given but
|
326
|
-
:right_id (#{right_id}) does not refer to the right_sibling (#{left.right_sibling.inspect})
|
327
|
-
of the node referenced by :left_id (#{left.inspect})
|
328
|
-
msg
|
329
|
-
end
|
330
|
-
|
331
|
-
if left && parent_id && left.parent_id != parent_id
|
332
|
-
inconsistent_move! <<-msg
|
333
|
-
Both :left_id (#{left_id.inspect}) and :parent_id (#{parent_id.inspect}) were given but
|
334
|
-
left.parent_id (#{left.parent_id}) does not equal parent_id
|
335
|
-
msg
|
336
|
-
end
|
337
|
-
|
338
|
-
if right && parent_id && right.parent_id != parent_id
|
339
|
-
inconsistent_move! <<-msg
|
340
|
-
Both :right_id (#{right_id.inspect}) and :parent_id (#{parent_id.inspect}) were given but
|
341
|
-
right.parent_id (#{right.parent_id}) does not equal parent_id
|
342
|
-
msg
|
343
|
-
end
|
344
|
-
end
|
345
|
-
|
346
|
-
def inconsistent_move!(message)
|
347
|
-
raise InconsistentMove, "Impossible move: #{message.split("\n").map! { |line| line.strip }.join}"
|
348
|
-
end
|
349
|
-
|
350
|
-
def impossible_move!(message)
|
351
|
-
raise ImpossibleMove, "Impossible move: #{message.split("\n").map! { |line| line.strip }.join}"
|
352
|
-
end
|
170
|
+
nested_set.move_to(node, :right)
|
171
|
+
end
|
353
172
|
end
|
354
173
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module SimpleNestedSet
|
2
|
+
module Move
|
3
|
+
class ByAttributes
|
4
|
+
class << self
|
5
|
+
def attribute_reader(*names)
|
6
|
+
names.each do |name|
|
7
|
+
define_method(name) { attributes[name].blank? ? nil : attributes[name].to_i }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
include Protection
|
13
|
+
|
14
|
+
attr_reader :node, :attributes
|
15
|
+
attribute_reader :parent_id, :left_id, :right_id
|
16
|
+
|
17
|
+
delegate :nested_set, :to => :node
|
18
|
+
|
19
|
+
def initialize(node, attributes)
|
20
|
+
@node, @attributes = node, attributes
|
21
|
+
normalize_attributes!
|
22
|
+
protect_inconsistent_move!(parent_id, left_id, right_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform
|
26
|
+
if left_id && left_id != node.id
|
27
|
+
node.move_to_right_of(left_id)
|
28
|
+
elsif right_id && right_id != node.id
|
29
|
+
node.move_to_left_of(right_id)
|
30
|
+
elsif parent_id != node.parent_id
|
31
|
+
node.move_to_child_of(parent_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def normalize_attributes!
|
38
|
+
attributes.symbolize_keys!
|
39
|
+
attributes.each { |key, value| attributes[key] = nil if value == 'null' }
|
40
|
+
|
41
|
+
# if left_id is given but blank, set right_id to leftmost sibling
|
42
|
+
if attributes.has_key?(:left_id) && attributes[:left_id].blank? && siblings.any?
|
43
|
+
attributes[:right_id] = siblings.first.id
|
44
|
+
end
|
45
|
+
|
46
|
+
# if right_id is given but blank, set left_id to rightmost sibling
|
47
|
+
if attributes.has_key?(:right_id) && attributes[:right_id].blank? && siblings.any?
|
48
|
+
attributes[:left_id] = siblings.last.id
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def siblings
|
53
|
+
@siblings ||= parent ? parent.children : node.nested_set.roots(node)
|
54
|
+
end
|
55
|
+
|
56
|
+
def parent
|
57
|
+
@parent ||= begin
|
58
|
+
parent_id = self.parent_id || node.parent_id
|
59
|
+
parent_id.blank? ? nil : nested_set.find(parent_id)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module SimpleNestedSet
|
2
|
+
module Move
|
3
|
+
class Inconsistent < ActiveRecord::ActiveRecordError ; end
|
4
|
+
class Impossible < ActiveRecord::ActiveRecordError ; end
|
5
|
+
|
6
|
+
module Protection
|
7
|
+
def protect_impossible_move!
|
8
|
+
positions = [:child, :left, :right, :root]
|
9
|
+
impossible_move!("A new node can not be moved") if node.new_record?
|
10
|
+
impossible_move!("Position must be one of #{positions.inspect} but is #{position.inspect}.") unless positions.include?(position)
|
11
|
+
impossible_move!("A new node can not be moved") if node.new_record?
|
12
|
+
impossible_move!("A node can't be moved to itself") if node == target
|
13
|
+
impossible_move!("A node can't be moved to a descendant of itself.") if target && (node.lft..node.rgt).include?(target.lft) && (node.lft..node.rgt).include?(target.rgt)
|
14
|
+
impossible_move!("A node can't be moved to a different scope") if target && !nested_set.same_scope?(target)
|
15
|
+
end
|
16
|
+
|
17
|
+
def protect_inconsistent_move!(parent_id, left_id, right_id)
|
18
|
+
left = nested_set.find(left_id) if left_id
|
19
|
+
right = nested_set.find(right_id) if right_id
|
20
|
+
|
21
|
+
if left && right && (!left.right_sibling || left.right_sibling.id != right_id)
|
22
|
+
inconsistent_move! <<-msg
|
23
|
+
Both :left_id (#{left_id.inspect}) and :right_id (#{right_id.inspect}) were given but
|
24
|
+
:right_id (#{right_id}) does not refer to the right_sibling (#{left.right_sibling.inspect})
|
25
|
+
of the node referenced by :left_id (#{left.inspect})
|
26
|
+
msg
|
27
|
+
end
|
28
|
+
|
29
|
+
if left && parent_id && left.parent_id != parent_id
|
30
|
+
inconsistent_move! <<-msg
|
31
|
+
Both :left_id (#{left_id.inspect}) and :parent_id (#{parent_id.inspect}) were given but
|
32
|
+
left.parent_id (#{left.parent_id}) does not equal parent_id
|
33
|
+
msg
|
34
|
+
end
|
35
|
+
|
36
|
+
if right && parent_id && right.parent_id != parent_id
|
37
|
+
inconsistent_move! <<-msg
|
38
|
+
Both :right_id (#{right_id.inspect}) and :parent_id (#{parent_id.inspect}) were given but
|
39
|
+
right.parent_id (#{right.parent_id}) does not equal parent_id
|
40
|
+
msg
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def inconsistent_move!(message)
|
45
|
+
raise Inconsistent, "Impossible move: #{message.split("\n").map! { |line| line.strip }.join}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def impossible_move!(message)
|
49
|
+
raise Impossible, "Impossible move: #{message.split("\n").map! { |line| line.strip }.join}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module SimpleNestedSet
|
2
|
+
module Move
|
3
|
+
class ToTarget
|
4
|
+
include Protection
|
5
|
+
|
6
|
+
attr_reader :node, :target, :position
|
7
|
+
|
8
|
+
delegate :id, :nested_set, :to => :node
|
9
|
+
|
10
|
+
def initialize(node, target, position)
|
11
|
+
@node, @target, @position = node, target, position
|
12
|
+
@target = nested_set.find(target) if target && !target.is_a?(ActiveRecord::Base)
|
13
|
+
|
14
|
+
protect_impossible_move!
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform
|
18
|
+
unless bound == node.rgt || bound == node.lft # there would be no change
|
19
|
+
reload
|
20
|
+
nested_set.transaction { nested_set.update_all(query) }
|
21
|
+
reload
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def reload
|
26
|
+
target.nested_set.reload if target
|
27
|
+
node.nested_set.reload
|
28
|
+
end
|
29
|
+
|
30
|
+
def parent_id
|
31
|
+
@parent_id ||= case position
|
32
|
+
when :child; target.id
|
33
|
+
when :root; nil
|
34
|
+
else target.parent_id
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def boundaries
|
39
|
+
# we have defined the boundaries of two non-overlapping intervals,
|
40
|
+
# so sorting puts both the intervals and their boundaries in order
|
41
|
+
@boundaries ||= [node.lft, node.rgt, bound, other_bound].sort
|
42
|
+
end
|
43
|
+
|
44
|
+
def bound
|
45
|
+
@bound ||= begin
|
46
|
+
bound = case position
|
47
|
+
when :child ; target.rgt
|
48
|
+
when :left ; target.lft
|
49
|
+
when :right ; target.rgt + 1
|
50
|
+
when :root ; roots.empty? ? 1 : roots.last.rgt + 1
|
51
|
+
end
|
52
|
+
bound > node.rgt ? bound - 1 : bound
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# TODO name other_bound in a more reasonable way
|
57
|
+
def other_bound
|
58
|
+
@other_bound ||= bound > node.rgt ? node.rgt + 1 : node.lft - 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def roots
|
62
|
+
@roots ||= node.nested_set.roots
|
63
|
+
end
|
64
|
+
|
65
|
+
def query
|
66
|
+
sql = <<-sql
|
67
|
+
lft = CASE
|
68
|
+
WHEN lft BETWEEN :a AND :b THEN lft + :d - :b
|
69
|
+
WHEN lft BETWEEN :c AND :d THEN lft + :a - :c
|
70
|
+
ELSE lft END,
|
71
|
+
|
72
|
+
rgt = CASE
|
73
|
+
WHEN rgt BETWEEN :a AND :b THEN rgt + :d - :b
|
74
|
+
WHEN rgt BETWEEN :c AND :d THEN rgt + :a - :c
|
75
|
+
ELSE rgt END,
|
76
|
+
|
77
|
+
parent_id = CASE
|
78
|
+
WHEN id = :id THEN :parent_id
|
79
|
+
ELSE parent_id END,
|
80
|
+
|
81
|
+
level = (
|
82
|
+
SELECT count(id)
|
83
|
+
FROM #{table_name} as t
|
84
|
+
WHERE t.lft < #{table_name}.lft AND rgt > #{table_name}.rgt
|
85
|
+
)
|
86
|
+
sql
|
87
|
+
# TODO name a, b, c, d in a more reasonable way
|
88
|
+
a, b, c, d = boundaries
|
89
|
+
[sql, { :a => a, :b => b, :c => c, :d => d, :id => id, :parent_id => parent_id }]
|
90
|
+
end
|
91
|
+
|
92
|
+
def table_name
|
93
|
+
node.class.quoted_table_name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module SimpleNestedSet
|
2
|
+
class NestedSet < ActiveRecord::Relation
|
3
|
+
NESTED_SET_ATTRIBUTES = [:parent_id, :left_id, :right_id]
|
4
|
+
|
5
|
+
class_inheritable_accessor :node_class, :scope_names
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def build_class(model, scopes)
|
9
|
+
model.const_get(:NestedSet) rescue model.const_set(:NestedSet, Class.new(NestedSet)).tap do |node_class|
|
10
|
+
node_class.node_class = model
|
11
|
+
node_class.scope_names = Array(scopes).map { |s| s.to_s =~ /_id$/ ? s.to_sym : :"#{s}_id" }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def scope(scope)
|
16
|
+
scope.blank? ? node_class.scoped : node_class.where(scope_condition(scope))
|
17
|
+
end
|
18
|
+
|
19
|
+
def scope_condition(scope)
|
20
|
+
scope_names.inject({}) do |c, name|
|
21
|
+
c.merge(name => scope.respond_to?(name) ? scope.send(name) : scope[name])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def with_move_by_attributes(attributes, node = nil)
|
26
|
+
node_class.transaction do
|
27
|
+
nested_set_attributes = extract_nested_set_attributes!(attributes)
|
28
|
+
result = yield
|
29
|
+
(node || result).nested_set.move_by_attributes(nested_set_attributes) unless nested_set_attributes.empty?
|
30
|
+
result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract_nested_set_attributes!(attributes)
|
35
|
+
result = attributes.slice(*NESTED_SET_ATTRIBUTES)
|
36
|
+
attributes.except!(*NESTED_SET_ATTRIBUTES)
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :node
|
42
|
+
|
43
|
+
def initialize(*args)
|
44
|
+
super(node_class, node_class.arel_table)
|
45
|
+
@node = args.first if args.size == 1
|
46
|
+
@where_values = self.class.scope(node).instance_variable_get(:@where_values) if node
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_move_by_attributes(attributes, &block)
|
50
|
+
self.class.with_move_by_attributes(attributes, node, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns true if the node has the same scope as the given node
|
54
|
+
def same_scope?(other)
|
55
|
+
scope_names.all? { |scope| node.send(scope) == other.send(scope) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# reload left, right, and parent
|
59
|
+
def reload
|
60
|
+
node.reload(:select => 'lft, rgt, parent_id')
|
61
|
+
end
|
62
|
+
|
63
|
+
def populate_associations(nodes)
|
64
|
+
node.children.target = nodes.select do |child|
|
65
|
+
next unless child.parent_id == node.id
|
66
|
+
nodes.delete(child)
|
67
|
+
child.nested_set.populate_associations(nodes)
|
68
|
+
child.parent = node
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# before validation set lft and rgt to the end of the tree
|
73
|
+
def init_as_node
|
74
|
+
unless node.rgt && node.lft
|
75
|
+
max_right = maximum(:rgt) || 0
|
76
|
+
node.lft, node.rgt = max_right + 1, max_right + 2
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Prunes a branch off of the tree, shifting all of the elements on the right
|
81
|
+
# back to the left so the counts still work.
|
82
|
+
def prune_branch
|
83
|
+
if node.rgt && node.lft
|
84
|
+
transaction do
|
85
|
+
diff = node.rgt - node.lft + 1
|
86
|
+
delete_all(['lft > ? AND rgt < ?', node.lft, node.rgt])
|
87
|
+
update_all(['lft = (lft - ?)', diff], ['lft >= ?', node.rgt])
|
88
|
+
update_all(['rgt = (rgt - ?)', diff], ['rgt >= ?', node.rgt])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def move_by_attributes(attributes)
|
94
|
+
Move::ByAttributes.new(node, attributes).perform
|
95
|
+
end
|
96
|
+
|
97
|
+
def move_to(target, position)
|
98
|
+
Move::ToTarget.new(node, target, position).perform
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/simple_nested_set.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
module SimpleNestedSet
|
2
|
-
class InconsistentMove < ActiveRecord::ActiveRecordError ; end
|
3
|
-
class ImpossibleMove < ActiveRecord::ActiveRecordError ; end
|
4
|
-
|
5
2
|
autoload :ActMacro, 'simple_nested_set/act_macro'
|
6
3
|
autoload :ClassMethods, 'simple_nested_set/class_methods'
|
7
4
|
autoload :InstanceMethods, 'simple_nested_set/instance_methods'
|
5
|
+
autoload :NestedSet, 'simple_nested_set/nested_set'
|
6
|
+
|
7
|
+
module Move
|
8
|
+
autoload :ByAttributes, 'simple_nested_set/move/by_attributes'
|
9
|
+
autoload :ToTarget, 'simple_nested_set/move/to_target'
|
10
|
+
autoload :Protection, 'simple_nested_set/move/protection'
|
11
|
+
end
|
8
12
|
end
|
9
13
|
|
10
14
|
ActiveRecord::Base.send :extend, SimpleNestedSet::ActMacro
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_nested_set
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Sven Fuchs
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-08-
|
18
|
+
date: 2010-08-04 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -104,6 +104,10 @@ files:
|
|
104
104
|
- lib/simple_nested_set/act_macro.rb
|
105
105
|
- lib/simple_nested_set/class_methods.rb
|
106
106
|
- lib/simple_nested_set/instance_methods.rb
|
107
|
+
- lib/simple_nested_set/move/by_attributes.rb
|
108
|
+
- lib/simple_nested_set/move/protection.rb
|
109
|
+
- lib/simple_nested_set/move/to_target.rb
|
110
|
+
- lib/simple_nested_set/nested_set.rb
|
107
111
|
- lib/simple_nested_set/version.rb
|
108
112
|
has_rdoc: true
|
109
113
|
homepage: http://github.com/svenfuchs/simple_nested_set
|