simple_nested_set 0.0.1

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.
@@ -0,0 +1,42 @@
1
+ module SimpleNestedSet
2
+ module ActMacro
3
+ def acts_as_nested_set(options = {})
4
+ return if acts_as_nested_set?
5
+
6
+ include SimpleNestedSet::InstanceMethods
7
+ extend SimpleNestedSet::ClassMethods
8
+
9
+ # TODO get callbacks working
10
+ # define_callbacks :move, :terminator => "result == false"
11
+ # before_move :init_as_node
12
+
13
+ before_validation :init_as_node
14
+ before_destroy :prune_branch
15
+ belongs_to :parent, :class_name => self.name
16
+ has_many :children, :foreign_key => :parent_id, :class_name => self.base_class.name
17
+
18
+ default_scope :order => :lft
19
+
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
+ }
36
+ end
37
+
38
+ def acts_as_nested_set?
39
+ included_modules.include?(SimpleNestedSet::InstanceMethods)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+
3
+ module SimpleNestedSet
4
+ module ClassMethods
5
+ NESTED_SET_ATTRIBUTES = [:parent_id, :left_id, :right_id]
6
+
7
+ def create(attributes)
8
+ with_move_by_attributes(attributes) { super }
9
+ end
10
+
11
+ def create!(attributes)
12
+ with_move_by_attributes(attributes) { super }
13
+ end
14
+
15
+ # Returns the single root
16
+ def root(*args)
17
+ nested_set(*args).first(:conditions => { :parent_id => nil })
18
+ end
19
+
20
+ # 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 } )
23
+ end
24
+
25
+ def leaves(*args)
26
+ nested_set(*args).scoped(:conditions => 'lft = rgt - 1' )
27
+ end
28
+
29
+ protected
30
+
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
37
+
38
+ def extract_nested_set_attributes!(attributes)
39
+ result = attributes.slice(*NESTED_SET_ATTRIBUTES)
40
+ attributes.except!(*NESTED_SET_ATTRIBUTES)
41
+ result
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,354 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module SimpleNestedSet
4
+ module InstanceMethods
5
+ def update_attributes(attributes)
6
+ move_by_attributes(attributes)
7
+ super
8
+ end
9
+
10
+ def update_attributes!(attributes)
11
+ move_by_attributes(attributes)
12
+ super
13
+ end
14
+
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
+ end
19
+
20
+ # Returns the level of this object in the tree, root level is 0
21
+ def level
22
+ parent_id.nil? ? 0 : ancestors.count
23
+ end
24
+
25
+ # Returns true if this is a root node.
26
+ def root?
27
+ parent_id.blank?
28
+ end
29
+
30
+ # Returns true is this is a child node
31
+ def child?
32
+ !root?
33
+ end
34
+
35
+ def leaf?
36
+ rgt - lft == 1
37
+ end
38
+
39
+ # compare by left column
40
+ def <=>(other)
41
+ lft <=> other.lft
42
+ end
43
+
44
+ # Returns the root
45
+ def root
46
+ root? ? self : ancestors.first
47
+ end
48
+
49
+ # Returns the parent
50
+ def parent
51
+ nested_set.klass.find(parent_id) unless root?
52
+ end
53
+
54
+ def ancestor_of?(other)
55
+ lft < other.lft && rgt > other.rgt
56
+ end
57
+
58
+ def self_or_ancestor_of?(other)
59
+ self == other || ancestor_of?(other)
60
+ end
61
+
62
+ # Returns an array of all parents
63
+ def ancestors
64
+ nested_set.scoped(:conditions => "lft < #{lft} AND rgt > #{rgt}")
65
+ end
66
+
67
+ # Returns the array of all parents and self
68
+ def self_and_ancestors
69
+ ancestors + [self]
70
+ end
71
+
72
+ def descendent_of?(other)
73
+ lft > other.lft && rgt < other.rgt
74
+ end
75
+
76
+ def self_or_descendent_of?(other)
77
+ self == other || descendent_of?(other)
78
+ end
79
+
80
+ # Returns a set of all of its children and nested children.
81
+ def descendants
82
+ rgt - lft == 1 ? [] : nested_set.scoped(:conditions => ['lft > ? AND rgt < ?', lft, rgt])
83
+ end
84
+
85
+ # Returns a set of itself and all of its nested children.
86
+ def self_and_descendants
87
+ [self] + descendants
88
+ end
89
+
90
+ # Returns the number of descendents
91
+ def descendents_count
92
+ rgt > lft ? (rgt - lft - 1) / 2 : 0
93
+ end
94
+
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
+ # Returns a set of only this entry's immediate children including self
101
+ def self_and_children
102
+ [self] + children
103
+ end
104
+
105
+ # Returns true if the node has any children
106
+ def children?
107
+ descendents_count > 0
108
+ end
109
+ alias has_children? children?
110
+
111
+ # Returns the array of all children of the parent, except self
112
+ def siblings
113
+ without_self(self_and_siblings)
114
+ end
115
+
116
+ # Returns the array of all children of the parent, included self
117
+ def self_and_siblings
118
+ nested_set.scoped(:conditions => { :parent_id => parent_id })
119
+ end
120
+
121
+ # Returns the lefthand sibling
122
+ def previous_sibling
123
+ nested_set.first :conditions => { :rgt => lft - 1 }
124
+ end
125
+ alias left_sibling previous_sibling
126
+
127
+ # Returns the righthand sibling
128
+ def next_sibling
129
+ nested_set.first :conditions => { :lft => rgt + 1 }
130
+ end
131
+ alias right_sibling next_sibling
132
+
133
+ # Returns all descendents that are leaves
134
+ def leaves
135
+ rgt - lft == 1 ? [] : nested_set.scoped(:conditions => ["lft > ? AND rgt < ? AND lft = rgt - 1", lft, rgt])
136
+ end
137
+
138
+ # Move the node to the child of another node
139
+ def move_to_child_of(node)
140
+ node ? move_to(node, :child) : move_to_root
141
+ end
142
+
143
+ def move_to_root
144
+ move_to(nil, :root)
145
+ end
146
+
147
+ # moves the node to the left of its left sibling if any
148
+ def move_left
149
+ move_to_left_of(left_sibling) if left_sibling
150
+ end
151
+
152
+ # moves the node to the right of its right sibling if any
153
+ def move_right
154
+ move_to_right_of(right_sibling) if right_sibling
155
+ end
156
+
157
+ # Move the node to the left of another node
158
+ def move_to_left_of(node)
159
+ move_to(node, :left)
160
+ end
161
+
162
+ # Move the node to the left of another node
163
+ 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
353
+ end
354
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleNestedSet
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,10 @@
1
+ module SimpleNestedSet
2
+ class InconsistentMove < ActiveRecord::ActiveRecordError ; end
3
+ class ImpossibleMove < ActiveRecord::ActiveRecordError ; end
4
+
5
+ autoload :ActMacro, 'simple_nested_set/act_macro'
6
+ autoload :ClassMethods, 'simple_nested_set/class_methods'
7
+ autoload :InstanceMethods, 'simple_nested_set/instance_methods'
8
+ end
9
+
10
+ ActiveRecord::Base.send :extend, SimpleNestedSet::ActMacro
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_nested_set
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Sven Fuchs
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-08-02 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: -1848230024
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 0
34
+ - beta4
35
+ version: 3.0.0.beta4
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: activesupport
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: database_cleaner
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ type: :development
65
+ version_requirements: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ name: test_declarative
68
+ prerelease: false
69
+ requirement: &id004 !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ type: :development
79
+ version_requirements: *id004
80
+ - !ruby/object:Gem::Dependency
81
+ name: pathname_local
82
+ prerelease: false
83
+ requirement: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ type: :development
93
+ version_requirements: *id005
94
+ description: "[description]"
95
+ email: svenfuchs@artweb-design.de
96
+ executables: []
97
+
98
+ extensions: []
99
+
100
+ extra_rdoc_files: []
101
+
102
+ files:
103
+ - lib/simple_nested_set.rb
104
+ - lib/simple_nested_set/act_macro.rb
105
+ - lib/simple_nested_set/class_methods.rb
106
+ - lib/simple_nested_set/instance_methods.rb
107
+ - lib/simple_nested_set/version.rb
108
+ has_rdoc: true
109
+ homepage: http://github.com/svenfuchs/simple_nested_set
110
+ licenses: []
111
+
112
+ post_install_message:
113
+ rdoc_options: []
114
+
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ hash: 3
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ hash: 3
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ requirements: []
136
+
137
+ rubyforge_project: "[none]"
138
+ rubygems_version: 1.3.7
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: "[summary]"
142
+ test_files: []
143
+