mongoid_nested_set 0.1.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,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).aggregate.all? {|c| c['count'] == 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,100 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{mongoid_nested_set}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brandon Turner"]
12
+ s.date = %q{2010-12-17}
13
+ s.description = %q{Fully featured tree implementation for Mongoid using the nested set model}
14
+ s.email = %q{bturner@bltweb.net}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.markdown"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.markdown",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/mongoid_nested_set.rb",
29
+ "lib/mongoid_nested_set/base.rb",
30
+ "lib/mongoid_nested_set/document.rb",
31
+ "lib/mongoid_nested_set/fields.rb",
32
+ "lib/mongoid_nested_set/outline_number.rb",
33
+ "lib/mongoid_nested_set/rebuild.rb",
34
+ "lib/mongoid_nested_set/relations.rb",
35
+ "lib/mongoid_nested_set/remove_order_by.rb",
36
+ "lib/mongoid_nested_set/update.rb",
37
+ "lib/mongoid_nested_set/validation.rb",
38
+ "mongoid_nested_set.gemspec",
39
+ "spec/matchers/nestedset_pos.rb",
40
+ "spec/models/circle_node.rb",
41
+ "spec/models/node.rb",
42
+ "spec/models/node_without_nested_set.rb",
43
+ "spec/models/numbering_node.rb",
44
+ "spec/models/renamed_fields.rb",
45
+ "spec/models/shape_node.rb",
46
+ "spec/models/square_node.rb",
47
+ "spec/models/test_document.rb",
48
+ "spec/models/unscoped_node.rb",
49
+ "spec/mongoid_nested_set_spec.rb",
50
+ "spec/spec_helper.rb"
51
+ ]
52
+ s.homepage = %q{http://github.com/thinkwell/mongoid_nested_set}
53
+ s.licenses = ["MIT"]
54
+ s.require_paths = ["lib"]
55
+ s.rubygems_version = %q{1.3.7}
56
+ s.summary = %q{Nested set based tree implementation for Mongoid}
57
+ s.test_files = [
58
+ "spec/matchers/nestedset_pos.rb",
59
+ "spec/models/circle_node.rb",
60
+ "spec/models/node.rb",
61
+ "spec/models/node_without_nested_set.rb",
62
+ "spec/models/numbering_node.rb",
63
+ "spec/models/renamed_fields.rb",
64
+ "spec/models/shape_node.rb",
65
+ "spec/models/square_node.rb",
66
+ "spec/models/test_document.rb",
67
+ "spec/models/unscoped_node.rb",
68
+ "spec/mongoid_nested_set_spec.rb",
69
+ "spec/spec_helper.rb"
70
+ ]
71
+
72
+ if s.respond_to? :specification_version then
73
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
74
+ s.specification_version = 3
75
+
76
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
77
+ s.add_runtime_dependency(%q<mongoid>, [">= 2.0.0.beta.20"])
78
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
79
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
80
+ s.add_development_dependency(%q<rcov>, [">= 0"])
81
+ s.add_development_dependency(%q<rspec-core>, [">= 0"])
82
+ s.add_runtime_dependency(%q<mongoid>, [">= 2.0.0.beta.20"])
83
+ else
84
+ s.add_dependency(%q<mongoid>, [">= 2.0.0.beta.20"])
85
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
86
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
87
+ s.add_dependency(%q<rcov>, [">= 0"])
88
+ s.add_dependency(%q<rspec-core>, [">= 0"])
89
+ s.add_dependency(%q<mongoid>, [">= 2.0.0.beta.20"])
90
+ end
91
+ else
92
+ s.add_dependency(%q<mongoid>, [">= 2.0.0.beta.20"])
93
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
94
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
95
+ s.add_dependency(%q<rcov>, [">= 0"])
96
+ s.add_dependency(%q<rspec-core>, [">= 0"])
97
+ s.add_dependency(%q<mongoid>, [">= 2.0.0.beta.20"])
98
+ end
99
+ end
100
+
@@ -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
@@ -0,0 +1,7 @@
1
+
2
+ class RenamedFields
3
+ include Mongoid::Document
4
+ acts_as_nested_set :parent_field => 'mother_id', :left_field => 'red', :right_field => 'black'
5
+
6
+ field :name
7
+ end
@@ -0,0 +1,18 @@
1
+ require "#{File.dirname(__FILE__)}/test_document"
2
+
3
+ class ShapeNode
4
+ include Mongoid::Document
5
+ include Mongoid::Acts::NestedSet::TestDocument
6
+ acts_as_nested_set
7
+
8
+ field :name
9
+
10
+ def test_set_attributes(attrs)
11
+ @attributes.update(attrs)
12
+ self
13
+ end
14
+
15
+ def self.test_set_dependent_option(val)
16
+ self.acts_as_nested_set_options[:dependent] = val
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ require "#{File.dirname(__FILE__)}/shape_node"
2
+
3
+ class SquareNode < ShapeNode
4
+ end
@@ -0,0 +1,35 @@
1
+ module Mongoid::Acts::NestedSet
2
+
3
+ module TestDocument
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.send(:include, InstanceMethods)
8
+ end
9
+
10
+
11
+ module ClassMethods
12
+
13
+ def test_set_dependent_option(val)
14
+ self.acts_as_nested_set_options[:dependent] = val
15
+ end
16
+
17
+ end
18
+
19
+
20
+ module InstanceMethods
21
+
22
+ def test_set_attributes(attrs)
23
+ attrs.each do |key, val|
24
+ if Mongoid.allow_dynamic_fields ||
25
+ fields.keys.any? { |k| k.to_s == key.to_s } ||
26
+ associations.any? { |a| a[0].to_s == key.to_s || a[1].foreign_key.to_s == key.to_s }
27
+ @attributes[key] = val
28
+ end
29
+ end
30
+ self
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,9 @@
1
+ require "#{File.dirname(__FILE__)}/test_document"
2
+
3
+ class UnscopedNode
4
+ include Mongoid::Document
5
+ include Mongoid::Acts::NestedSet::TestDocument
6
+ acts_as_nested_set
7
+
8
+ field :name
9
+ end
@@ -0,0 +1,723 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+
4
+ describe Mongoid::Acts::NestedSet do
5
+
6
+ it "provides the acts_as_nested_set method" do
7
+ Node.should respond_to('acts_as_nested_set')
8
+ NodeWithoutNestedSet.should respond_to('acts_as_nested_set')
9
+ end
10
+
11
+ end
12
+
13
+
14
+ describe "A Mongoid::Document" do
15
+
16
+ def create_clothing_nodes(klass=Node)
17
+ nodes = {}
18
+ # See Wikipedia for an illustration of the first tree
19
+ # http://en.wikipedia.org/wiki/Nested_set_model#Example
20
+ nodes[:clothing] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Clothing', 'lft' => 1, 'rgt' => 22, 'depth' => 0, 'number' => nil, 'parent_id' => nil)
21
+ nodes[:mens] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Men\'s', 'lft' => 2, 'rgt' => 9, 'depth' => 1, 'number' => '1', 'parent_id' => nodes[:clothing].id)
22
+ nodes[:suits] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Suits', 'lft' => 3, 'rgt' => 8, 'depth' => 2, 'number' => '1.1', 'parent_id' => nodes[:mens].id)
23
+ nodes[:slacks] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Slacks', 'lft' => 4, 'rgt' => 5, 'depth' => 3, 'number' => '1.1.1', 'parent_id' => nodes[:suits].id)
24
+ nodes[:jackets] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Jackets', 'lft' => 6, 'rgt' => 7, 'depth' => 3, 'number' => '1.1.2', 'parent_id' => nodes[:suits].id)
25
+ nodes[:womens] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Women\'s', 'lft' => 10, 'rgt' => 21, 'depth' => 1, 'number' => '2', 'parent_id' => nodes[:clothing].id)
26
+ nodes[:dresses] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Dresses', 'lft' => 11, 'rgt' => 16, 'depth' => 2, 'number' => '2.1', 'parent_id' => nodes[:womens].id)
27
+ nodes[:skirts] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Skirts', 'lft' => 17, 'rgt' => 18, 'depth' => 2, 'number' => '2.2', 'parent_id' => nodes[:womens].id)
28
+ nodes[:blouses] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Blouses', 'lft' => 19, 'rgt' => 20, 'depth' => 2, 'number' => '2.3', 'parent_id' => nodes[:womens].id)
29
+ nodes[:gowns] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Gowns', 'lft' => 12, 'rgt' => 13, 'depth' => 3, 'number' => '2.1.1', 'parent_id' => nodes[:dresses].id)
30
+ nodes[:sundress] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Sun Dresses', 'lft' => 14, 'rgt' => 15, 'depth' => 3, 'number' => '2.1.2', 'parent_id' => nodes[:dresses].id)
31
+ nodes
32
+ end
33
+
34
+ def create_electronics_nodes(klass=Node)
35
+ nodes = {}
36
+ # See MySQL for an illustration of the second tree
37
+ # http://dev.mysql.com/tech-resources/articles/hierarchical-data.html
38
+ nodes[:electronics] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Electronics', 'lft' => 1, 'rgt' => 20, 'depth' => 0, 'number' => nil, 'parent_id' => nil)
39
+ nodes[:televisions] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Televisions', 'lft' => 2, 'rgt' => 9, 'depth' => 1, 'number' => '1', 'parent_id' => nodes[:electronics].id)
40
+ nodes[:tube] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Tube', 'lft' => 3, 'rgt' => 4, 'depth' => 2, 'number' => '1.1', 'parent_id' => nodes[:televisions].id)
41
+ nodes[:lcd] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'LCD', 'lft' => 5, 'rgt' => 6, 'depth' => 2, 'number' => '1.2', 'parent_id' => nodes[:televisions].id)
42
+ nodes[:plasma] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Plasma', 'lft' => 7, 'rgt' => 8, 'depth' => 2, 'number' => '1.3', 'parent_id' => nodes[:televisions].id)
43
+ nodes[:portable] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Portable', 'lft' => 10, 'rgt' => 19, 'depth' => 1, 'number' => '2', 'parent_id' => nodes[:electronics].id)
44
+ nodes[:mp3] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'MP3', 'lft' => 11, 'rgt' => 14, 'depth' => 2, 'number' => '2.1', 'parent_id' => nodes[:portable].id)
45
+ nodes[:cd] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'CD', 'lft' => 15, 'rgt' => 16, 'depth' => 2, 'number' => '2.2', 'parent_id' => nodes[:portable].id)
46
+ nodes[:radio] = klass.new.test_set_attributes('root_id' => 2, 'name' => '2 Way Radio', 'lft' => 17, 'rgt' => 18, 'depth' => 2, 'number' => '2.3', 'parent_id' => nodes[:portable].id)
47
+ nodes[:flash] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Flash', 'lft' => 12, 'rgt' => 13, 'depth' => 3, 'number' => '2.1.1', 'parent_id' => nodes[:mp3].id)
48
+ nodes
49
+ end
50
+
51
+ def persist_nodes(nodes, collection_name=nil)
52
+ nodes = {:first => nodes} unless nodes.is_a? Hash
53
+ collection_name = nodes.values.first.class.collection_name if collection_name.nil?
54
+ coll = Mongoid.master[collection_name]
55
+
56
+ nodes.each_value do |node|
57
+ # Bypass the ORM (and the nested set callbacks) and save directly with the underlying driver
58
+ coll.update({:_id => node.id}, node.attributes, {:upsert => true})
59
+ node.new_record = false
60
+ end
61
+ nodes
62
+ end
63
+
64
+
65
+
66
+
67
+ context "that does not act as a nested set" do
68
+ it "does not have a left field" do
69
+ NodeWithoutNestedSet.should_not have_field('lft', :type => Integer)
70
+ end
71
+
72
+ it "does not have a right field" do
73
+ NodeWithoutNestedSet.should_not have_field('rgt', :type => Integer)
74
+ end
75
+
76
+ it "does not include NestedSet methods" do
77
+ NodeWithoutNestedSet.should_not respond_to('descendant_of')
78
+ NodeWithoutNestedSet.new.should_not respond_to('left')
79
+ NodeWithoutNestedSet.should_not respond_to('each_with_outline_number')
80
+ end
81
+ end
82
+
83
+
84
+ context "that acts as an un-scoped nested set" do
85
+
86
+ context "in a tree" do
87
+ before(:each) do
88
+ @nodes = persist_nodes(create_clothing_nodes(UnscopedNode))
89
+ end
90
+
91
+ it "can detect if roots are valid" do
92
+ UnscopedNode.should be_all_roots_valid
93
+
94
+ persist_nodes(UnscopedNode.new(:name => 'Test').test_set_attributes(:lft => 20, :rgt => 30, :parent_id=>nil))
95
+ UnscopedNode.should_not be_all_roots_valid
96
+ end
97
+
98
+ it "can detect if left and rights are valid" do
99
+ UnscopedNode.should be_left_and_rights_valid
100
+
101
+ # left > right
102
+ n = UnscopedNode.new(:name => 'Test').test_set_attributes(:lft => 6, :rgt => 5, :parent_id=>@nodes[:suits].id)
103
+ persist_nodes(n)
104
+ UnscopedNode.should_not be_left_and_rights_valid
105
+
106
+ # left == right
107
+ persist_nodes(n.test_set_attributes(:rgt => 6))
108
+ UnscopedNode.should_not be_left_and_rights_valid
109
+
110
+ # Overlaps parent
111
+ persist_nodes(n.test_set_attributes(:rgt => 8))
112
+ UnscopedNode.should_not be_left_and_rights_valid
113
+ end
114
+
115
+ it "can detect duplicate left and right values" do
116
+ UnscopedNode.should be_no_duplicates_for_fields
117
+
118
+ n = UnscopedNode.new(:name => 'Test').test_set_attributes(:lft => 6, :rgt => 25, :parent_id=>@nodes[:suits].id)
119
+ persist_nodes(n)
120
+ UnscopedNode.should_not be_no_duplicates_for_fields
121
+
122
+ persist_nodes(n.test_set_attributes(:lft => 5, :rgt => 7, :parent_id=>@nodes[:suits].id))
123
+ UnscopedNode.should_not be_no_duplicates_for_fields
124
+ end
125
+ end
126
+ end
127
+
128
+
129
+ context "that acts as a scoped nested set" do
130
+
131
+ it "does not include outline number methods" do
132
+ Node.should_not respond_to('each_with_outline_number')
133
+ end
134
+
135
+ # Adds fields
136
+
137
+ it "has a left field" do
138
+ Node.should have_field('lft', :type => Integer)
139
+ RenamedFields.should have_field('red', :type => Integer)
140
+ RenamedFields.should_not have_field('lft', :type => Integer)
141
+ end
142
+
143
+ it "has a right field" do
144
+ Node.should have_field('rgt', :type => Integer)
145
+ RenamedFields.should have_field('red', :type => Integer)
146
+ RenamedFields.should_not have_field('rgt', :type => Integer)
147
+ end
148
+
149
+ it "has a parent field" do
150
+ Node.should have_field('parent_id', :type => String)
151
+ RenamedFields.should have_field('mother_id', :type => String)
152
+ RenamedFields.should_not have_field('parent_id', :type => String)
153
+ end
154
+
155
+ it "does not have a number field" do
156
+ Node.should_not have_field('number', :type => String)
157
+ end
158
+
159
+ it "has a default left field name" do
160
+ Node.acts_as_nested_set_options[:left_field].should == 'lft'
161
+ end
162
+
163
+ it "has a default right field name" do
164
+ Node.acts_as_nested_set_options[:right_field].should == 'rgt'
165
+ end
166
+
167
+ it "has a default parent field name" do
168
+ Node.acts_as_nested_set_options[:parent_field].should == 'parent_id'
169
+ end
170
+
171
+ it "returns the left field name" do
172
+ Node.left_field_name.should == 'lft'
173
+ Node.new.left_field_name.should == 'lft'
174
+ RenamedFields.left_field_name.should == 'red'
175
+ RenamedFields.new.left_field_name.should == 'red'
176
+ end
177
+
178
+ it "returns the right field name" do
179
+ Node.right_field_name.should == 'rgt'
180
+ Node.new.right_field_name.should == 'rgt'
181
+ RenamedFields.right_field_name.should == 'black'
182
+ RenamedFields.new.right_field_name.should == 'black'
183
+ end
184
+
185
+ it "returns the parent field name" do
186
+ Node.parent_field_name.should == 'parent_id'
187
+ Node.new.parent_field_name.should == 'parent_id'
188
+ RenamedFields.parent_field_name.should == 'mother_id'
189
+ RenamedFields.new.parent_field_name.should == 'mother_id'
190
+ end
191
+
192
+ it "does not allow assigning the left field" do
193
+ expect { Node.new.lft = 1 }.to raise_error(NameError)
194
+ expect { RenamedFields.new.red = 1 }.to raise_error(NameError)
195
+ end
196
+
197
+ it "does not allow assigning the right field" do
198
+ expect { Node.new.rgt = 1 }.to raise_error(NameError)
199
+ expect { RenamedFields.new.black = 1 }.to raise_error(NameError)
200
+ end
201
+
202
+
203
+
204
+
205
+ # No-Database Calculations
206
+
207
+ context "with other nodes" do
208
+ before(:each) do
209
+ @nodes = create_clothing_nodes.merge(create_electronics_nodes)
210
+ end
211
+
212
+ it "determines if it is a root node" do
213
+ @nodes[:mens].should_not be_root
214
+ @nodes[:clothing].should be_root
215
+ end
216
+
217
+ it "determines if it is a leaf node" do
218
+ @nodes[:suits].should_not be_leaf
219
+ @nodes[:jackets].should be_leaf
220
+ end
221
+
222
+ it "determines if it is a child node" do
223
+ @nodes[:mens].should be_child
224
+ @nodes[:clothing].should_not be_child
225
+ end
226
+
227
+ it "determines if it is a descendant of another node" do
228
+ @nodes[:sundress].should be_descendant_of(@nodes[:dresses])
229
+ @nodes[:dresses].should_not be_descendant_of(@nodes[:sundress])
230
+ @nodes[:dresses].should_not be_descendant_of(@nodes[:dresses])
231
+ @nodes[:flash].should_not be_descendant_of(@nodes[:dresses])
232
+ end
233
+
234
+ it "determines if it is a descendant of or equal to another node" do
235
+ @nodes[:sundress].should be_is_or_is_descendant_of(@nodes[:dresses])
236
+ @nodes[:sundress].should be_is_or_is_descendant_of(@nodes[:sundress])
237
+ @nodes[:dresses].should_not be_is_or_is_descendant_of(@nodes[:sundress])
238
+ @nodes[:flash].should_not be_is_or_is_descendant_of(@nodes[:dresses])
239
+ @nodes[:skirts].should_not be_is_or_is_descendant_of(@nodes[:radio])
240
+ end
241
+
242
+ it "determines if it is an ancestor of another node" do
243
+ @nodes[:suits].should be_ancestor_of(@nodes[:jackets])
244
+ @nodes[:jackets].should_not be_ancestor_of(@nodes[:suits])
245
+ @nodes[:suits].should_not be_ancestor_of(@nodes[:suits])
246
+ @nodes[:dresses].should_not be_ancestor_of(@nodes[:flash])
247
+ end
248
+
249
+ it "determines if it is an ancestor of or equal to another node" do
250
+ @nodes[:suits].should be_is_or_is_ancestor_of(@nodes[:jackets])
251
+ @nodes[:suits].should be_is_or_is_ancestor_of(@nodes[:suits])
252
+ @nodes[:jackets].should_not be_is_or_is_ancestor_of(@nodes[:suits])
253
+ @nodes[:dresses].should_not be_is_or_is_ancestor_of(@nodes[:flash])
254
+ @nodes[:radio].should_not be_is_or_is_ancestor_of(@nodes[:skirts])
255
+ end
256
+
257
+ end
258
+
259
+
260
+ context "in an empty tree" do
261
+
262
+ it "can create a root node" do
263
+ root = Node.create(:name => 'Root Category')
264
+ root.should have_nestedset_pos(1, 2)
265
+ root.depth.should == 0
266
+ end
267
+
268
+ it "can create a child node via children.create" do
269
+ root = Node.create(:name => 'Root Category')
270
+ child = root.children.create(:name => 'Child Category')
271
+ child.should have_nestedset_pos(2, 3)
272
+ child.parent_id.should == root.id
273
+ child.depth.should == 1
274
+ root.reload.should have_nestedset_pos(1, 4)
275
+ end
276
+
277
+ it "can create a child node via children<<" do
278
+ root = Node.create(:name => 'Root Category')
279
+ child = Node.create(:name => 'Child Category')
280
+ root.children << child
281
+ child.parent_id.should == root.id
282
+ child.should have_nestedset_pos(2, 3)
283
+ child.depth.should == 1
284
+ root.reload.should have_nestedset_pos(1, 4)
285
+ end
286
+
287
+ it "can create a child node with parent pre-assigned" do
288
+ root = Node.create(:name => 'Root Category')
289
+ child = Node.create(:name => 'Child Category', :parent => root)
290
+ child.should have_nestedset_pos(2, 3)
291
+ child.parent_id.should == root.id
292
+ child.depth.should == 1
293
+ root.reload.should have_nestedset_pos(1, 4)
294
+ end
295
+
296
+ it "can create a child node with parent id pre-assigned" do
297
+ root = Node.create(:name => 'Root Category')
298
+ child = Node.create(:name => 'Child Category', :parent_id => root.id)
299
+ child.should have_nestedset_pos(2, 3)
300
+ child.parent_id.should == root.id
301
+ child.depth.should == 1
302
+ root.reload.should have_nestedset_pos(1, 4)
303
+ end
304
+
305
+ it "can change a new node's parent before saving" do
306
+ root = Node.create(:name => 'Root Category')
307
+ child = Node.new(:name => 'Child Category')
308
+ child.parent = root
309
+ child.save
310
+ child.should have_nestedset_pos(2, 3)
311
+ child.parent_id.should == root.id
312
+ child.depth.should == 1
313
+ root.reload.should have_nestedset_pos(1, 4)
314
+ end
315
+
316
+ it "can change a new node's parent id before saving" do
317
+ root = Node.create(:name => 'Root Category')
318
+ child = Node.new(:name => 'Child Category')
319
+ child.parent_id = root.id
320
+ child.save
321
+ child.should have_nestedset_pos(2, 3)
322
+ child.parent_id.should == root.id
323
+ child.depth.should == 1
324
+ root.reload.should have_nestedset_pos(1, 4)
325
+ end
326
+
327
+ end
328
+
329
+
330
+ context "in a tree" do
331
+
332
+ before(:each) do
333
+ @nodes = persist_nodes(create_clothing_nodes.merge(create_electronics_nodes))
334
+ end
335
+
336
+
337
+ # Scopes
338
+
339
+ it "fetches all root nodes" do
340
+ Node.roots.should have(2).entries
341
+ end
342
+
343
+ it "fetches all leaf nodes in order" do
344
+ Node.leaves.where(:root_id=>1).map {|e| e.name}.should == %w[Slacks Jackets Gowns Sun\ Dresses Skirts Blouses]
345
+ end
346
+
347
+ it "fetches all nodes with a given depth in order" do
348
+ Node.with_depth(1).where(:root_id=>1).map {|e| e.name}.should == %w[Men's Women's]
349
+ end
350
+
351
+
352
+ # Queries
353
+
354
+ it "fetches descendants of multiple parents" do
355
+ parents = Node.any_in(:name => %w[Men's Dresses])
356
+ Node.where(:root_id=>1).descendants_of(parents).should have(5).entries
357
+ end
358
+
359
+ it "fetches self and ancestors in order" do
360
+ @nodes[:dresses].self_and_ancestors.map {|e| e.name}.should == %w[Clothing Women's Dresses]
361
+ end
362
+
363
+ it "fetches ancestors in order" do
364
+ @nodes[:dresses].ancestors.map {|e| e.name}.should == %w[Clothing Women's]
365
+ end
366
+
367
+ it "fetches its root" do
368
+ @nodes[:dresses].root.name.should == 'Clothing'
369
+ end
370
+
371
+ it "fetches self and siblings in order" do
372
+ @nodes[:skirts].self_and_siblings.map {|e| e.name}.should == %w[Dresses Skirts Blouses]
373
+ end
374
+
375
+ it "fetches siblings in order" do
376
+ @nodes[:skirts].siblings.map {|e| e.name}.should == %w[Dresses Blouses]
377
+ end
378
+
379
+ it "fetches leaves in order" do
380
+ @nodes[:womens].leaves.map {|e| e.name}.should == %w[Gowns Sun\ Dresses Skirts Blouses]
381
+ end
382
+
383
+ it "fetches its current level" do
384
+ @nodes[:suits].level.should == 2
385
+ end
386
+
387
+ it "fetches self and descendants in order" do
388
+ @nodes[:womens].self_and_descendants.map {|e| e.name}.should == %w[Women's Dresses Gowns Sun\ Dresses Skirts Blouses]
389
+ end
390
+
391
+ it "fetches descendants in order" do
392
+ @nodes[:womens].descendants.map {|e| e.name}.should == %w[Dresses Gowns Sun\ Dresses Skirts Blouses]
393
+ end
394
+
395
+ it "fetches its first sibling to the left" do
396
+ @nodes[:skirts].left_sibling.name.should == 'Dresses'
397
+ @nodes[:slacks].left_sibling.should == nil
398
+ end
399
+
400
+ it "fetches its first sibling to the right" do
401
+ @nodes[:skirts].right_sibling.name.should == 'Blouses'
402
+ @nodes[:jackets].right_sibling.should == nil
403
+ end
404
+
405
+ it "can detect if roots are valid" do
406
+ Node.should be_all_roots_valid
407
+
408
+ persist_nodes(Node.new(:name => 'Test').test_set_attributes(:root_id => 1, :lft => 20, :rgt => 30, :parent_id=>nil))
409
+ Node.should_not be_all_roots_valid
410
+ end
411
+
412
+ it "can detect if left and rights are valid" do
413
+ Node.should be_left_and_rights_valid
414
+
415
+ # left > right
416
+ n = Node.new(:name => 'Test').test_set_attributes(:root_id => 1, :lft => 6, :rgt => 5, :parent_id=>@nodes[:suits].id)
417
+ persist_nodes(n)
418
+ Node.should_not be_left_and_rights_valid
419
+
420
+ # left == right
421
+ persist_nodes(n.test_set_attributes(:rgt => 6))
422
+ Node.should_not be_left_and_rights_valid
423
+
424
+ # Overlaps parent
425
+ persist_nodes(n.test_set_attributes(:rgt => 8))
426
+ Node.should_not be_left_and_rights_valid
427
+ end
428
+
429
+ it "can detect duplicate left and right values" do
430
+ Node.should be_no_duplicates_for_fields
431
+
432
+ n = Node.new(:name => 'Test').test_set_attributes(:root_id => 1, :lft => 6, :rgt => 25, :parent_id=>@nodes[:suits].id)
433
+ persist_nodes(n)
434
+ Node.should_not be_no_duplicates_for_fields
435
+
436
+ persist_nodes(n.test_set_attributes(:lft => 5, :rgt => 7, :parent_id=>@nodes[:suits].id))
437
+ Node.should_not be_no_duplicates_for_fields
438
+ end
439
+
440
+
441
+ # Moves
442
+
443
+ it "cannot move a new node" do
444
+ n = Node.new(:name => 'Test', :root_id => 1)
445
+ expect {
446
+ n.move_to_right_of(Node.where(:name => 'Jackets').first)
447
+ }.to raise_error(Mongoid::Errors::MongoidError, /move.*new node/)
448
+ end
449
+
450
+ it "cannot move a node inside its tree" do
451
+ n = Node.where(:name => 'Men\'s').first
452
+ expect {
453
+ n.move_to_right_of(Node.where(:name => 'Suits').first)
454
+ }.to raise_error(Mongoid::Errors::MongoidError, /possible/)
455
+ end
456
+
457
+ it "cannot move a node to a non-existent target" do
458
+ @nodes[:mens].parent_id = BSON::ObjectId.new
459
+ expect {
460
+ @nodes[:mens].save
461
+ }.to raise_error(Mongoid::Errors::MongoidError, /possible.*(exist|found)/)
462
+ end
463
+
464
+ it "adds newly created nodes to the end of the tree" do
465
+ Node.create(:name => 'Vests', :root_id => 1).should have_nestedset_pos(23, 24)
466
+
467
+ n = Node.new(:name => 'Test', :root_id => 1)
468
+ n.save
469
+ n.should have_nestedset_pos(25, 26)
470
+ end
471
+
472
+ it "can move left" do
473
+ @nodes[:jackets].move_left
474
+ @nodes[:jackets] .should have_nestedset_pos( 4, 5)
475
+ @nodes[:slacks].reload.should have_nestedset_pos( 6, 7)
476
+ @nodes[:suits] .reload.should have_nestedset_pos( 3, 8)
477
+ @nodes[:jackets].depth.should == 3
478
+ end
479
+
480
+ it "can move right" do
481
+ @nodes[:slacks].move_right
482
+ @nodes[:slacks] .should have_nestedset_pos( 6, 7)
483
+ @nodes[:jackets].reload.should have_nestedset_pos( 4, 5)
484
+ @nodes[:suits] .reload.should have_nestedset_pos( 3, 8)
485
+ @nodes[:slacks].depth.should == 3
486
+ end
487
+
488
+ it "can move left of another node" do
489
+ @nodes[:slacks].move_to_left_of(@nodes[:skirts])
490
+ @nodes[:slacks] .should have_nestedset_pos(15, 16)
491
+ @nodes[:skirts] .should have_nestedset_pos(17, 18)
492
+ @nodes[:skirts] .reload.should have_nestedset_pos(17, 18)
493
+ @nodes[:dresses].reload.should have_nestedset_pos( 9, 14)
494
+ @nodes[:womens] .reload.should have_nestedset_pos( 8, 21)
495
+ @nodes[:slacks].depth.should == 2
496
+ end
497
+
498
+ it "can move right of another node" do
499
+ @nodes[:slacks].move_to_right_of(@nodes[:skirts])
500
+ @nodes[:slacks] .should have_nestedset_pos(17, 18)
501
+ @nodes[:skirts] .should have_nestedset_pos(15, 16)
502
+ @nodes[:skirts] .reload.should have_nestedset_pos(15, 16)
503
+ @nodes[:blouses].reload.should have_nestedset_pos(19, 20)
504
+ @nodes[:womens] .reload.should have_nestedset_pos( 8, 21)
505
+ @nodes[:slacks].depth.should == 2
506
+ end
507
+
508
+ it "can move as a child of another node" do
509
+ @nodes[:slacks].move_to_child_of(@nodes[:dresses])
510
+ @nodes[:slacks] .should have_nestedset_pos(14, 15)
511
+ @nodes[:dresses] .should have_nestedset_pos( 9, 16)
512
+ @nodes[:dresses].reload.should have_nestedset_pos( 9, 16)
513
+ @nodes[:gowns] .reload.should have_nestedset_pos(10, 11)
514
+ @nodes[:mens] .reload.should have_nestedset_pos( 2, 7)
515
+ @nodes[:slacks].depth.should == 3
516
+ end
517
+
518
+ it "can change it's parent id" do
519
+ @nodes[:slacks].parent_id = @nodes[:dresses].id
520
+ @nodes[:slacks].save
521
+ @nodes[:slacks] .reload.should have_nestedset_pos(14, 15)
522
+ @nodes[:dresses].reload.should have_nestedset_pos( 9, 16)
523
+ @nodes[:gowns] .reload.should have_nestedset_pos(10, 11)
524
+ @nodes[:mens] .reload.should have_nestedset_pos( 2, 7)
525
+ @nodes[:slacks].depth.should == 3
526
+ end
527
+
528
+ it "can move to the root position" do
529
+ @nodes[:suits].move_to_root
530
+ @nodes[:suits] .should be_root
531
+ @nodes[:suits] .should have_nestedset_pos( 1, 6)
532
+ @nodes[:jackets] .reload.should have_nestedset_pos( 4, 5)
533
+ @nodes[:clothing].reload.should have_nestedset_pos( 7, 22)
534
+ @nodes[:mens] .reload.should have_nestedset_pos( 8, 9)
535
+ @nodes[:womens] .reload.should have_nestedset_pos(10, 21)
536
+ end
537
+
538
+ it "can move to the left of root" do
539
+ @nodes[:suits].move_to_left_of(@nodes[:clothing])
540
+ @nodes[:suits] .should be_root
541
+ @nodes[:suits] .should have_nestedset_pos( 1, 6)
542
+ @nodes[:jackets] .reload.should have_nestedset_pos( 4, 5)
543
+ @nodes[:clothing].reload.should have_nestedset_pos( 7, 22)
544
+ @nodes[:mens] .reload.should have_nestedset_pos( 8, 9)
545
+ @nodes[:womens] .reload.should have_nestedset_pos(10, 21)
546
+ end
547
+
548
+ it "can move to the right of root" do
549
+ @nodes[:suits].move_to_right_of(@nodes[:clothing])
550
+ @nodes[:suits] .should be_root
551
+ @nodes[:suits] .should have_nestedset_pos(17, 22)
552
+ @nodes[:jackets] .reload.should have_nestedset_pos(20, 21)
553
+ @nodes[:clothing].reload.should have_nestedset_pos( 1, 16)
554
+ @nodes[:mens] .reload.should have_nestedset_pos( 2, 3)
555
+ @nodes[:womens] .reload.should have_nestedset_pos( 4, 15)
556
+ end
557
+
558
+ it "can move node with children" do
559
+ @nodes[:suits].move_to_child_of(@nodes[:dresses])
560
+ @nodes[:suits] .should have_nestedset_pos(10, 15)
561
+ @nodes[:dresses] .should have_nestedset_pos( 5, 16)
562
+ @nodes[:mens] .reload.should have_nestedset_pos( 2, 3)
563
+ @nodes[:womens] .reload.should have_nestedset_pos( 4, 21)
564
+ @nodes[:sundress].reload.should have_nestedset_pos( 8, 9)
565
+ @nodes[:jackets] .reload.should have_nestedset_pos(13, 14)
566
+ @nodes[:suits].depth.should == 3
567
+ @nodes[:jackets].depth.should == 4
568
+ end
569
+
570
+ context "with dependent=delete_all" do
571
+ it "deletes descendants when destroyed" do
572
+ @nodes[:mens].destroy
573
+ @nodes[:clothing].reload.should have_nestedset_pos( 1, 14)
574
+ @nodes[:womens] .reload.should have_nestedset_pos( 2, 13)
575
+ Node.where(:name => 'Men\'s').count.should == 0
576
+ Node.where(:name => 'Suits').count.should == 0
577
+ Node.where(:name => 'Slacks').count.should == 0
578
+ end
579
+ end
580
+
581
+ context "with dependent=destroy" do
582
+ it "deletes descendants when destroyed" do
583
+ Node.test_set_dependent_option :destroy
584
+ @nodes[:mens].destroy
585
+ @nodes[:clothing].reload.should have_nestedset_pos( 1, 14)
586
+ @nodes[:womens] .reload.should have_nestedset_pos( 2, 13)
587
+ Node.where(:name => 'Men\'s').count.should == 0
588
+ Node.where(:name => 'Suits').count.should == 0
589
+ Node.where(:name => 'Slacks').count.should == 0
590
+ end
591
+ end
592
+
593
+ end
594
+
595
+
596
+ context "in an adjaceny list tree" do
597
+ before(:each) do
598
+ @nodes = create_clothing_nodes(Node)
599
+ @nodes.each_value { |node| node.test_set_attributes(:rgt => nil) }
600
+ persist_nodes(@nodes)
601
+ end
602
+
603
+ it "can rebuild nested set properties" do
604
+ Node.rebuild!
605
+ root = Node.root
606
+ root.should be_a(Node)
607
+ root.name.should == 'Clothing'
608
+
609
+ @nodes[:clothing].reload.should have_nestedset_pos( 1, 22)
610
+ @nodes[:mens] .reload.should have_nestedset_pos( 2, 9)
611
+ @nodes[:womens] .reload.should have_nestedset_pos(10, 21)
612
+ @nodes[:suits] .reload.should have_nestedset_pos( 3, 8)
613
+ @nodes[:skirts] .reload.should have_nestedset_pos(17, 18)
614
+ end
615
+
616
+ end
617
+ end
618
+
619
+
620
+ context "that acts as a nested set with inheritance" do
621
+ def create_shape_nodes
622
+ nodes = {}
623
+ nodes[:root] = SquareNode.new.test_set_attributes('name' => 'Root', 'lft' => 1, 'rgt' => 12, 'depth' => 0, 'parent_id' => nil)
624
+ nodes[:c1] = SquareNode.new.test_set_attributes('name' => '1', 'lft' => 2, 'rgt' => 7, 'depth' => 1, 'parent_id' => nodes[:root].id)
625
+ nodes[:c2] = SquareNode.new.test_set_attributes('name' => '2', 'lft' => 8, 'rgt' => 9, 'depth' => 1, 'parent_id' => nodes[:root].id)
626
+ nodes[:c3] = CircleNode.new.test_set_attributes('name' => '3', 'lft' => 10, 'rgt' => 11, 'depth' => 1, 'parent_id' => nodes[:root].id)
627
+ nodes[:c11] = CircleNode.new.test_set_attributes('name' => '1.1', 'lft' => 3, 'rgt' => 4, 'depth' => 2, 'parent_id' => nodes[:c1].id)
628
+ nodes[:c12] = SquareNode.new.test_set_attributes('name' => '1.2', 'lft' => 5, 'rgt' => 6, 'depth' => 2, 'parent_id' => nodes[:c1].id)
629
+ nodes
630
+ end
631
+
632
+ context "in a tree" do
633
+ before(:each) do
634
+ @nodes = create_shape_nodes
635
+ persist_nodes(@nodes)
636
+ end
637
+
638
+ it "fetches self and descendants in order" do
639
+ @nodes[:root].self_and_descendants.map {|e| e.name}.should == %w[Root 1 1.1 1.2 2 3]
640
+ end
641
+ end
642
+ end
643
+
644
+
645
+ context "that acts as a nested set with outline numbering" do
646
+
647
+ it "includes outline number methods" do
648
+ NumberingNode.should respond_to('each_with_outline_number')
649
+ end
650
+
651
+ it "does not have a number field" do
652
+ NumberingNode.should have_field('number', :type => String)
653
+ end
654
+
655
+ context "in a tree" do
656
+ before(:each) do
657
+ @nodes = persist_nodes(create_clothing_nodes(NumberingNode).merge(create_electronics_nodes(NumberingNode)))
658
+ end
659
+
660
+ it "sets the number for new child nodes" do
661
+ n = NumberingNode.create(:name => 'Vests', :root_id => 1, :parent_id => @nodes[:suits].id)
662
+ n.number.should == '1.1.3'
663
+ end
664
+
665
+ it "updates the number for nodes moved within the same parent" do
666
+ @nodes[:slacks].move_right
667
+ @nodes[:slacks] .number.should == '1.1.2'
668
+ @nodes[:jackets].reload.number.should == '1.1.1'
669
+ end
670
+
671
+ it "updates the number for nodes moved to a new parent" do
672
+ @nodes[:slacks].move_to_child_of(@nodes[:dresses])
673
+ @nodes[:slacks].number.should == '2.1.3'
674
+ end
675
+
676
+ it "updates the number for nodes moved to root" do
677
+ @nodes[:suits].move_to_root
678
+ @nodes[:suits] .number.should be_nil
679
+ @nodes[:suits] .reload.number.should be_nil
680
+ @nodes[:jackets].reload.number.should == '2'
681
+ @nodes[:skirts] .reload.number.should == '2.2'
682
+ end
683
+
684
+ it "updates the number for old siblings of moved nodes" do
685
+ @nodes[:slacks].move_to_child_of(@nodes[:dresses])
686
+ @nodes[:jackets].reload.number.should == '1.1.1'
687
+ end
688
+
689
+ it "updates the number for new siblings of moved nodes" do
690
+ @nodes[:slacks].move_to_left_of(@nodes[:gowns])
691
+ @nodes[:gowns].reload.number.should == '2.1.2'
692
+ end
693
+
694
+ it "updates the number for descendants of moved nodes" do
695
+ @nodes[:suits].move_to_child_of(@nodes[:dresses])
696
+ @nodes[:suits] .number.should == '2.1.3'
697
+ @nodes[:jackets].reload.number.should == '2.1.3.2'
698
+ end
699
+
700
+ it "updates the number for descendants of old siblings of moved nodes" do
701
+ @nodes[:mens].move_to_child_of(@nodes[:womens])
702
+ @nodes[:womens] .reload.number.should == '1'
703
+ @nodes[:dresses].reload.number.should == '1.1'
704
+ end
705
+
706
+ it "updates the number for descendants of new siblings of moved nodes" do
707
+ @nodes[:dresses].move_to_left_of(@nodes[:suits])
708
+ @nodes[:jackets].reload.number == '1.2.2'
709
+ end
710
+
711
+ it "updates the number for a single node" do
712
+ @nodes[:suits].update_attributes(NumberingNode.outline_number_field_name => '3.1')
713
+ @nodes[:suits].number.should == '3.1'
714
+ @nodes[:suits].update_outline_number
715
+ @nodes[:suits].number.should == '1.1'
716
+ end
717
+
718
+
719
+ end
720
+
721
+ end
722
+
723
+ end