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.
@@ -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