glebtv-mongoid_nested_set 0.3.0

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,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