mongoid_nested_set 0.1.0

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