simple_nested_set 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+