simple_nested_set 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 :init_as_node
14
- before_destroy :prune_branch
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
- klass = options[:class] || self
21
- scopes = Array(options[:scope]).map { |s| s.to_s !~ /_id$/ ? :"#{s}_id" : s }
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 single root
16
- def root(*args)
17
- nested_set(*args).first(:conditions => { :parent_id => nil })
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 roots(*args)
22
- nested_set(*args).scoped(:conditions => { :parent_id => nil } )
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 leaves(*args)
26
- nested_set(*args).scoped(:conditions => 'lft = rgt - 1' )
36
+ def with_parent(parent_id)
37
+ where(:parent_id => parent_id)
27
38
  end
28
39
 
29
- protected
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
- def with_move_by_attributes(attributes)
32
- transaction do
33
- nested_set_attributes = extract_nested_set_attributes!(attributes)
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
- def extract_nested_set_attributes!(attributes)
39
- result = attributes.slice(*NESTED_SET_ATTRIBUTES)
40
- attributes.except!(*NESTED_SET_ATTRIBUTES)
41
- result
42
- end
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
- move_by_attributes(attributes)
7
- super
11
+ nested_set.with_move_by_attributes(attributes) { super }
8
12
  end
9
13
 
10
14
  def update_attributes!(attributes)
11
- move_by_attributes(attributes)
12
- super
15
+ nested_set.with_move_by_attributes(attributes) { super }
13
16
  end
14
17
 
15
- # Returns true if the node has the same scope as the given node
16
- def same_scope?(other)
17
- nested_set.scope_columns.all? { |name| self.send(name) == other.send(name) }
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 is this is a child node
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
- nested_set.klass.find(parent_id) unless root?
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.scoped(:conditions => "lft < #{lft} AND rgt > #{rgt}")
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.scoped(:conditions => ['lft > ? AND rgt < ?', lft, rgt])
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 descendents
91
- def descendents_count
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
- descendents_count > 0
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
- without_self(self_and_siblings)
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.scoped(:conditions => { :parent_id => parent_id })
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 :conditions => { :rgt => lft - 1 }
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 :conditions => { :lft => rgt + 1 }
134
+ nested_set.with_right_sibling(rgt).first
130
135
  end
131
136
  alias right_sibling next_sibling
132
137
 
133
- # Returns all descendents that are leaves
138
+ # Returns all descendants that are leaves
134
139
  def leaves
135
- rgt - lft == 1 ? [] : nested_set.scoped(:conditions => ["lft > ? AND rgt < ? AND lft = rgt - 1", lft, rgt])
140
+ rgt - lft == 1 ? [] : nested_set.with_descendants(lft, rgt).with_leaves
136
141
  end
137
142
 
138
- # Move the node to the child of another node
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
- # moves the node to the left of its left sibling if any
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
- # moves the node to the right of its right sibling if any
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
@@ -1,3 +1,3 @@
1
1
  module SimpleNestedSet
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
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-02 00:00:00 +02:00
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