glebtv-mongoid_nested_set 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,122 @@
1
+ module Mongoid::Acts::NestedSet
2
+
3
+ module OutlineNumber
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.send(:include, InstanceMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Iterates over tree elements and determines the current outline number in
13
+ # the tree.
14
+ # Only accepts default ordering, ordering by an other field than lft
15
+ # does not work.
16
+ # This method does not used the cached number field.
17
+ #
18
+ # Example:
19
+ # Category.each_with_outline_number(Category.root.self_and_descendants) do |o, level|
20
+ #
21
+ def each_with_outline_number(objects, parent_number=nil)
22
+ objects = Array(objects) unless objects.is_a? Array
23
+
24
+ stack = []
25
+ last_num = parent_number
26
+ objects.each_with_index do |o, i|
27
+ if i == 0 && last_num == nil && !o.root?
28
+ last_num = o.parent.outline_number
29
+ end
30
+
31
+ if stack.last.nil? || o.parent_id != stack.last[:parent_id]
32
+ # we are on a new level, did we descend or ascend?
33
+ if stack.any? { |h| h[:parent_id] == o.parent_id }
34
+ # ascend
35
+ stack.pop while stack.last[:parent_id] != o.parent_id
36
+ else
37
+ # descend
38
+ stack << {:parent_id => o.parent_id, :parent_number => last_num, :siblings => []}
39
+ end
40
+ end
41
+
42
+ if o.root? && !roots_have_outline_numbers?
43
+ num = nil
44
+ else
45
+ num = o.send(:build_outline_number,
46
+ o.root? ? '' : stack.last[:parent_number],
47
+ o.send(:outline_number_sequence, stack.last[:siblings])
48
+ )
49
+ end
50
+ yield(o, num)
51
+
52
+ stack.last[:siblings] << o
53
+ last_num = num
54
+ end
55
+ end
56
+
57
+
58
+ def update_outline_numbers(objects, parent_number=nil)
59
+ each_with_outline_number(objects, parent_number) do |o, num|
60
+ o.update_attributes(outline_number_field_name => num)
61
+ end
62
+ end
63
+
64
+
65
+ # Do root nodes have outline numbers
66
+ def roots_have_outline_numbers?
67
+ false
68
+ end
69
+
70
+ end
71
+
72
+ module InstanceMethods
73
+
74
+ def outline_number
75
+ self[outline_number_field_name]
76
+ end
77
+
78
+
79
+ def update_outline_number
80
+ self.class.update_outline_numbers(self)
81
+ end
82
+
83
+
84
+ def update_self_and_descendants_outline_number
85
+ self.class.update_outline_numbers(self_and_descendants)
86
+ end
87
+
88
+
89
+ def update_descendants_outline_number
90
+ self.class.update_outline_numbers(self.descendants, self.outline_number)
91
+ end
92
+
93
+
94
+ protected
95
+
96
+ # Gets the outline sequence number for this node
97
+ #
98
+ # For example, if the parent's outline number is 1.2 and this is the
99
+ # 3rd sibling this will return 3.
100
+ #
101
+ def outline_number_sequence(prev_siblings)
102
+ prev_siblings.count + 1
103
+ end
104
+
105
+
106
+ # Constructs the full outline number
107
+ #
108
+ def build_outline_number(parent_number, sequence)
109
+ if parent_number && parent_number != ''
110
+ parent_number + outline_number_seperator + sequence.to_s
111
+ else
112
+ sequence.to_s
113
+ end
114
+ end
115
+
116
+ def outline_number_seperator
117
+ '.'
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,40 @@
1
+ module Mongoid::Acts::NestedSet
2
+
3
+ module Rebuild
4
+
5
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
6
+ # Warning: Very expensive!
7
+ def rebuild!(options = {})
8
+ # Don't rebuild a valid tree.
9
+ return true if valid?
10
+
11
+ scope = lambda{ |node| {} }
12
+ if acts_as_nested_set_options[:scope]
13
+ scope = lambda { |node| node.nested_set_scope.options.merge(node.nested_set_scope.selector) }
14
+ end
15
+ indices = {}
16
+
17
+ set_left_and_rights = lambda do |node|
18
+ # set left
19
+ left = (indices[scope.call(node)] += 1)
20
+ # find
21
+ node.nested_set_scope.where(parent_field_name => node.id).asc(left_field_name).asc(right_field_name).each { |n| set_left_and_rights.call(n) }
22
+ # set right
23
+ right = (indices[scope.call(node)] += 1)
24
+
25
+ node.class.collection.find(:_id => node.id).update(
26
+ {"$set" => {left_field_name => left, right_field_name => right}},
27
+ {:safe => true}
28
+ )
29
+ end
30
+
31
+ # Find root node(s)
32
+ root_nodes = self.where(parent_field_name => nil).asc(left_field_name).asc(right_field_name).asc(:_id).each do |root_node|
33
+ # setup index for this scope
34
+ indices[scope.call(root_node)] ||= 0
35
+ set_left_and_rights.call(root_node)
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,100 @@
1
+ module Mongoid::Acts::NestedSet
2
+
3
+ module Relations
4
+
5
+ # Returns root
6
+ def root
7
+ self_and_ancestors.first
8
+ end
9
+
10
+
11
+ # Returns the array of all parents and self
12
+ def self_and_ancestors
13
+ nested_set_scope.where(
14
+ left_field_name => {"$lte" => left},
15
+ right_field_name => {"$gte" => right}
16
+ )
17
+ end
18
+
19
+
20
+ # Returns an array of all parents
21
+ def ancestors
22
+ without_self self_and_ancestors
23
+ end
24
+
25
+
26
+ # Returns the array of all children of the parent, including self
27
+ def self_and_siblings
28
+ nested_set_scope.where(parent_field_name => parent_id)
29
+ end
30
+
31
+
32
+ # Returns the array of all children of the parent, except self
33
+ def siblings
34
+ without_self self_and_siblings
35
+ end
36
+
37
+
38
+ # Returns a set of all of its nested children which do not have children
39
+ def leaves
40
+ descendants.where("this.#{right_field_name} - this.#{left_field_name} == 1")
41
+ end
42
+
43
+
44
+ # Returns the level of this object in the tree
45
+ # root level is 0
46
+ def level
47
+ parent_id.nil? ? 0 : ancestors.count
48
+ end
49
+
50
+
51
+ # Returns a set of itself and all of its nested children
52
+ def self_and_descendants
53
+ nested_set_scope.where(
54
+ left_field_name => {"$gte" => left},
55
+ right_field_name => {"$lte" => right}
56
+ )
57
+ end
58
+
59
+
60
+ # Returns a set of all of its children and nested children
61
+ def descendants
62
+ without_self self_and_descendants
63
+ end
64
+
65
+
66
+ def is_descendant_of?(other)
67
+ other.left < self.left && self.left < other.right && same_scope?(other)
68
+ end
69
+ alias :descendant_of? is_descendant_of?
70
+
71
+
72
+ def is_or_is_descendant_of?(other)
73
+ other.left <= self.left && self.left < other.right && same_scope?(other)
74
+ end
75
+
76
+
77
+ def is_ancestor_of?(other)
78
+ self.left < other.left && other.left < self.right && same_scope?(other)
79
+ end
80
+ alias :ancestor_of? is_ancestor_of?
81
+
82
+
83
+ def is_or_is_ancestor_of?(other)
84
+ self.left <= other.left && other.left < self.right && same_scope?(other)
85
+ end
86
+
87
+
88
+ # Find the first sibling to the left
89
+ def left_sibling
90
+ siblings.where(left_field_name => {"$lt" => left}).remove_order_by.desc(left_field_name).first
91
+ end
92
+
93
+
94
+ # Find the first sibling to the right
95
+ def right_sibling
96
+ siblings.where(left_field_name => {"$gt" => left}).asc(left_field_name).first
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,15 @@
1
+ module Mongoid
2
+ module Criterion
3
+ module Ordering
4
+ def remove_order_by
5
+ @options[:sort] = nil
6
+ self
7
+ end
8
+ end
9
+ end
10
+
11
+ class Criteria
12
+ include Criterion::Ordering
13
+ end
14
+ end
15
+
@@ -0,0 +1,230 @@
1
+ module Mongoid::Acts::NestedSet
2
+
3
+ module Update
4
+
5
+ # Shorthand method for finding the left sibling and moving to the left of it
6
+ def move_left
7
+ move_to_left_of left_sibling
8
+ end
9
+
10
+
11
+ # Shorthand method for finding the right sibling and moving to the right of it
12
+ def move_right
13
+ move_to_right_of right_sibling
14
+ end
15
+
16
+
17
+ # Move the node to the left of another node (you can pass id only)
18
+ def move_to_left_of(node)
19
+ move_to node, :left
20
+ end
21
+
22
+
23
+ # Move the node to the right of another node (you can pass id only)
24
+ def move_to_right_of(node)
25
+ move_to node, :right
26
+ end
27
+
28
+
29
+ # Move the node to the child of another node (you can pass id only)
30
+ def move_to_child_of(node)
31
+ move_to node, :child
32
+ end
33
+
34
+
35
+ # Move the node to root nodes
36
+ def move_to_root
37
+ move_to nil, :root
38
+ end
39
+
40
+
41
+ def move_possible?(target)
42
+ self != target && # Can't target self
43
+ same_scope?(target) && # can't be in different scopes
44
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
45
+ end
46
+
47
+
48
+
49
+ protected
50
+
51
+ def store_new_parent
52
+ @move_to_new_parent_id = ((self.new_record? && parent_id) || send("#{parent_field_name}_changed?")) ? parent_id : false
53
+ true # force callback to return true
54
+ end
55
+
56
+
57
+ def move_to_new_parent
58
+ if @move_to_new_parent_id.nil?
59
+ move_to_root
60
+ elsif @move_to_new_parent_id
61
+ move_to_child_of(@move_to_new_parent_id)
62
+ end
63
+ end
64
+
65
+
66
+ # on creation, set automatically lft and rgt to the end of the tree
67
+ def set_default_left_and_right
68
+ maxright = nested_set_scope.remove_order_by.max(right_field_name) || 0
69
+ self[left_field_name] = maxright + 1
70
+ self[right_field_name] = maxright + 2
71
+ self[:depth] = 0
72
+ end
73
+
74
+
75
+ def move_to(target, position)
76
+ raise Mongoid::Errors::MongoidError, "You cannot move a new node" if self.new_record?
77
+
78
+ res = run_callbacks :move do
79
+
80
+ # No transaction support in MongoDB.
81
+ # ACID is not guaranteed
82
+ # TODO
83
+
84
+ if target.is_a? scope_class
85
+ target.reload_nested_set
86
+ elsif position != :root
87
+ # load object if node is not an object
88
+ target = nested_set_scope.where(:_id => target).first
89
+ end
90
+ self.reload_nested_set
91
+
92
+ unless position == :root || target
93
+ raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be found."
94
+ end
95
+
96
+ unless position == :root || move_possible?(target)
97
+ raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be inside moved tree."
98
+ end
99
+
100
+ bound = case position
101
+ when :child; target[right_field_name]
102
+ when :left; target[left_field_name]
103
+ when :right; target[right_field_name] + 1
104
+ when :root; 1
105
+ else raise Mongoid::Errors::MongoidError, "Position should be :child, :left, :right or :root ('#{position}' received)."
106
+ end
107
+
108
+ old_parent = self[parent_field_name]
109
+ new_parent = case position
110
+ when :child; target.id
111
+ when :root; nil
112
+ else target[parent_field_name]
113
+ end
114
+
115
+ left, right = [self[left_field_name], self[right_field_name]]
116
+ width, distance = [right - left + 1, bound - left]
117
+ edge = bound > right ? bound - 1 : bound
118
+
119
+ # there would be no change
120
+ return self if left == edge || right == edge
121
+
122
+ # moving backwards
123
+ if distance < 0
124
+ distance -= width
125
+ left += width
126
+ end
127
+
128
+ scope_class.mongo_session.with(:safe => true) do |session|
129
+ collection = session[scope_class.collection_name]
130
+ scope = nested_set_scope.remove_order_by
131
+
132
+ # allocate space for new move
133
+ collection.find(
134
+ scope.gte(left_field_name => bound).selector
135
+ ).update_all("$inc" => { left_field_name => width })
136
+
137
+ collection.find(
138
+ scope.gte(right_field_name => bound).selector
139
+ ).update_all("$inc" => { right_field_name => width })
140
+
141
+ # move the nodes
142
+ collection.find(
143
+ scope.and(left_field_name => {"$gte" => left}, right_field_name => {"$lt" => left + width}).selector
144
+ ).update_all("$inc" => { left_field_name => distance, right_field_name => distance })
145
+
146
+ # remove the hole
147
+ collection.find(
148
+ scope.gt(left_field_name => right).selector
149
+ ).update_all("$inc" => { left_field_name => -width })
150
+
151
+ collection.find(
152
+ scope.gt(right_field_name => right).selector
153
+ ).update_all("$inc" => { right_field_name => -width })
154
+ end
155
+
156
+ self.set(parent_field_name, new_parent)
157
+ self.reload_nested_set
158
+ self.update_self_and_descendants_depth
159
+
160
+ if outline_numbering?
161
+ if old_parent && old_parent != new_parent
162
+ scope_class.where(:_id => old_parent).first.update_descendants_outline_number
163
+ end
164
+ if new_parent
165
+ scope_class.where(:_id => new_parent).first.update_descendants_outline_number
166
+ else
167
+ update_self_and_descendants_outline_number
168
+ end
169
+ self.reload_nested_set
170
+ end
171
+
172
+ target.reload_nested_set if target
173
+ end
174
+ self
175
+ end
176
+
177
+
178
+ # Update cached level attribute
179
+ def update_depth
180
+ if depth?
181
+ self.update_attribute(:depth, level)
182
+ end
183
+ self
184
+ end
185
+
186
+
187
+ # Update cached level attribute for self and descendants
188
+ def update_self_and_descendants_depth
189
+ if depth?
190
+ scope_class.each_with_level(self_and_descendants) do |node, level|
191
+ node.with(:safe => true).set(:depth, level) unless node.depth == level
192
+ end
193
+ self.reload
194
+ end
195
+ self
196
+ end
197
+
198
+
199
+ # Prunes a branch off of the tree, shifting all of the elements on the right
200
+ # back to the left so the counts still work
201
+ def destroy_descendants
202
+ return if right.nil? || left.nil? || skip_before_destroy
203
+
204
+ if acts_as_nested_set_options[:dependent] == :destroy
205
+ descendants.each do |model|
206
+ model.skip_before_destroy = true
207
+ model.destroy
208
+ end
209
+ else
210
+ c = nested_set_scope.where(left_field_name.to_sym.gt => left, right_field_name.to_sym.lt => right)
211
+ scope_class.where(c.selector).delete_all
212
+ end
213
+
214
+ # update lefts and rights for remaining nodes
215
+ diff = right - left + 1
216
+
217
+ scope_class.with(:safe => true).where(
218
+ nested_set_scope.where(left_field_name.to_sym.gt => right).selector
219
+ ).inc(left_field_name, -diff)
220
+
221
+ scope_class.with(:safe => true).where(
222
+ nested_set_scope.where(right_field_name.to_sym.gt => right).selector
223
+ ).inc(right_field_name, -diff)
224
+
225
+ # Don't allow multiple calls to destroy to corrupt the set
226
+ self.skip_before_destroy = true
227
+ end
228
+
229
+ end
230
+ end
@@ -0,0 +1,59 @@
1
+ module Mongoid::Acts::NestedSet
2
+
3
+ module Validation
4
+
5
+ # Warning: Very expensive! Do not use unless you know what you are doing.
6
+ # This method is only useful for determining if the entire tree is valid
7
+ def valid?
8
+ left_and_rights_valid? && no_duplicates_for_fields? && all_roots_valid?
9
+ end
10
+
11
+
12
+ # Warning: Very expensive! Do not use unless you know what you are doing.
13
+ def left_and_rights_valid?
14
+ all.detect { |node|
15
+ node.send(left_field_name).nil? ||
16
+ node.send(right_field_name).nil? ||
17
+ node.send(left_field_name) >= node.send(right_field_name) ||
18
+ !node.parent.nil? && (
19
+ node.send(left_field_name) <= node.parent.send(left_field_name) ||
20
+ node.send(right_field_name) >= node.parent.send(right_field_name)
21
+ )
22
+ }.nil?
23
+ end
24
+
25
+
26
+ # Warning: Very expensive! Do not use unless you know what you are doing.
27
+ def no_duplicates_for_fields?
28
+ roots.group_by{|record| scope_field_names.collect{|field| record.send(field.to_sym)}}.all? do |scope, grouped_roots|
29
+ [left_field_name, right_field_name].all? do |field|
30
+ grouped_roots.first.nested_set_scope.only(field).group_by {|doc| doc.send(field)}.all? {|k, v| v.size == 1}
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+ # Wrapper for each_root_valid? that can deal with scope
37
+ # Warning: Very expensive! Do not use unless you know what you are doing.
38
+ def all_roots_valid?
39
+ if acts_as_nested_set_options[:scope]
40
+ roots.group_by{|record| scope_field_names.collect{|field| record.send(field.to_sym)}}.all? do |scope, grouped_roots|
41
+ each_root_valid?(grouped_roots)
42
+ end
43
+ else
44
+ each_root_valid?(roots)
45
+ end
46
+ end
47
+
48
+
49
+ def each_root_valid?(roots_to_validate)
50
+ right = 0
51
+ roots_to_validate.all? do |root|
52
+ (root.left > right && root.right > right).tap do
53
+ right = root.right
54
+ end
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module MongoidNestedSet
2
+ VERSION = "0.3.0"
3
+ end
@@ -0,0 +1,40 @@
1
+
2
+ require 'mongoid_nested_set/remove_order_by'
3
+
4
+ # This acts provides Nested Set functionality. Nested Set is a smart way to implement
5
+ # an _ordered_ tree, with the added feature that you can select the children and all of
6
+ # their descendants with a single query. The drawback is that insertion or move need
7
+ # multiple queries. But everything is done here by this module!
8
+ #
9
+ # Nested sets are appropriate each time you want either an ordered tree (menus,
10
+ # commercial categories) or an efficient way of querying big trees (threaded posts).
11
+ #
12
+ # == API
13
+ #
14
+ # Method names are aligned with acts_as_tree as much as possible to make replacement
15
+ # from one by another easier.
16
+ #
17
+ # item.children.create(:name => 'child1')
18
+ #
19
+ module Mongoid
20
+ module Acts
21
+ module NestedSet
22
+ require 'mongoid_nested_set/base'
23
+ autoload :Document, 'mongoid_nested_set/document'
24
+ autoload :Fields, 'mongoid_nested_set/fields'
25
+ autoload :Rebuild, 'mongoid_nested_set/rebuild'
26
+ autoload :Relations, 'mongoid_nested_set/relations'
27
+ autoload :Update, 'mongoid_nested_set/update'
28
+ autoload :Validation, 'mongoid_nested_set/validation'
29
+ autoload :OutlineNumber, 'mongoid_nested_set/outline_number'
30
+
31
+ def self.included(base)
32
+ base.extend(Base)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+
39
+ # Enable the acts_as_nested_set method
40
+ Mongoid::Document::ClassMethods.send(:include, Mongoid::Acts::NestedSet::Base)
@@ -0,0 +1,46 @@
1
+ module Mongoid::Acts::NestedSet
2
+ module Matchers
3
+
4
+ def have_nestedset_pos(lft, rgt, options = {})
5
+ NestedSetPosition.new(lft, rgt, options)
6
+ end
7
+
8
+ class NestedSetPosition
9
+
10
+ def initialize(lft, rgt, options)
11
+ @lft = lft
12
+ @rgt = rgt
13
+ @options = options
14
+ end
15
+
16
+ def matches?(node)
17
+ @node = node
18
+ !!(
19
+ node.respond_to?('left') && node.respond_to?('right') &&
20
+ node.left == @lft &&
21
+ node.right == @rgt
22
+ )
23
+ end
24
+
25
+ def description
26
+ "have position {left: #{@lft}, right: #{@rgt}}"
27
+ end
28
+
29
+ def failure_message_for_should
30
+ sprintf("expected nested set position: {left: %2s, right: %2s}\n" +
31
+ " got: {left: %2s, right: %2s}",
32
+ @lft,
33
+ @rgt,
34
+ @node.respond_to?('left') ? @node.left : '?',
35
+ @node.respond_to?('right') ? @node.right : '?'
36
+ )
37
+ end
38
+
39
+ def failure_message_for_should_not
40
+ sprintf("expected nested set to not have position: {left: %2s, right: %2s}", @lft, @rgt)
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ require "#{File.dirname(__FILE__)}/shape_node"
2
+
3
+ class CircleNode < ShapeNode
4
+ end
@@ -0,0 +1,10 @@
1
+ require "#{File.dirname(__FILE__)}/test_document"
2
+
3
+ class Node
4
+ include Mongoid::Document
5
+ include Mongoid::Acts::NestedSet::TestDocument
6
+ acts_as_nested_set :scope => :root_id
7
+
8
+ field :name
9
+ field :root_id, :type => Integer
10
+ end
@@ -0,0 +1,6 @@
1
+
2
+ class NodeWithoutNestedSet
3
+ include Mongoid::Document
4
+
5
+ field :name
6
+ end
@@ -0,0 +1,10 @@
1
+ require "#{File.dirname(__FILE__)}/test_document"
2
+
3
+ class NumberingNode
4
+ include Mongoid::Document
5
+ include Mongoid::Acts::NestedSet::TestDocument
6
+ acts_as_nested_set :scope => :root_id, :outline_number_field => 'number'
7
+
8
+ field :name
9
+ field :root_id, :type => Integer
10
+ end