mongoid-ancestry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,246 @@
1
+ module Mongoid
2
+ module Ancestry
3
+ module InstanceMethods
4
+ def save!(opts = {})
5
+ opts.merge!(:safe => true)
6
+ retries = 3
7
+ begin
8
+ super(opts)
9
+ rescue Mongo::OperationFailure => e
10
+ (retries -= 1) > 0 && e.to_s =~ %r{duplicate key error.+\$#{self.base_class.uid_field}} ? retry : raise(e)
11
+ end
12
+ end
13
+ alias_method :save, :save!
14
+
15
+ def set_uid
16
+ previous = self.class.desc(:"#{self.base_class.uid_field}").first
17
+ uniq_id = previous ? previous.read_attribute(:"#{uid_field}").to_i + 1 : 1
18
+ send :"#{uid_field}=", uniq_id
19
+ end
20
+
21
+ # Validate that the ancestors don't include itself
22
+ def ancestry_exclude_self
23
+ if ancestor_ids.include? read_attribute(self.base_class.uid_field)
24
+ errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.")
25
+ end
26
+ end
27
+
28
+ # Update descendants with new ancestry
29
+ def update_descendants_with_new_ancestry
30
+ # Skip this if callbacks are disabled
31
+ unless ancestry_callbacks_disabled?
32
+ # If node is valid, not a new record and ancestry was updated ...
33
+ if changed.include?(self.base_class.ancestry_field.to_s) && !new_record? && valid?
34
+ # ... for each descendant ...
35
+ descendants.each do |descendant|
36
+ # ... replace old ancestry with new ancestry
37
+ descendant.without_ancestry_callbacks do
38
+ for_replace = \
39
+ if read_attribute(self.class.ancestry_field).blank?
40
+ read_attribute(self.base_class.uid_field).to_s
41
+ else
42
+ "#{read_attribute self.class.ancestry_field }/#{uid}"
43
+ end
44
+ new_ancestry = descendant.read_attribute(descendant.class.ancestry_field).gsub(/^#{self.child_ancestry}/, for_replace)
45
+ descendant.update_attribute(self.base_class.ancestry_field, new_ancestry)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # Apply orphan strategy
53
+ def apply_orphan_strategy
54
+ # Skip this if callbacks are disabled
55
+ unless ancestry_callbacks_disabled?
56
+ # If this isn't a new record ...
57
+ unless new_record?
58
+ # ... make al children root if orphan strategy is rootify
59
+ if self.base_class.orphan_strategy == :rootify
60
+ descendants.each do |descendant|
61
+ descendant.without_ancestry_callbacks do
62
+ val = \
63
+ unless descendant.ancestry == child_ancestry
64
+ descendant.read_attribute(descendant.class.ancestry_field).gsub(/^#{child_ancestry}\//, '')
65
+ end
66
+ descendant.update_attribute descendant.class.ancestry_field, val
67
+ end
68
+ end
69
+ # ... destroy all descendants if orphan strategy is destroy
70
+ elsif self.base_class.orphan_strategy == :destroy
71
+ descendants.all.each do |descendant|
72
+ descendant.without_ancestry_callbacks { descendant.destroy }
73
+ end
74
+ # ... throw an exception if it has children and orphan strategy is restrict
75
+ elsif self.base_class.orphan_strategy == :restrict
76
+ raise Error.new('Cannot delete record because it has descendants.') unless is_childless?
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # The ancestry value for this record's children
83
+ def child_ancestry
84
+ # New records cannot have children
85
+ raise Error.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
86
+
87
+ if self.send("#{self.base_class.ancestry_field}_was").blank?
88
+ read_attribute(self.base_class.uid_field).to_s
89
+ else
90
+ "#{self.send "#{self.base_class.ancestry_field}_was"}/#{read_attribute(self.base_class.uid_field)}"
91
+ end
92
+ end
93
+
94
+ # Ancestors
95
+ def ancestor_ids
96
+ read_attribute(self.base_class.ancestry_field).to_s.split('/').map{ |uid| uid.to_i }
97
+ end
98
+
99
+ def ancestor_conditions
100
+ {self.base_class.uid_field.in => ancestor_ids}
101
+ end
102
+
103
+ def ancestors depth_options = {}
104
+ self.base_class.scope_depth(depth_options, depth).where(ancestor_conditions)
105
+ end
106
+
107
+ def path_ids
108
+ ancestor_ids + [read_attribute(self.base_class.uid_field)]
109
+ end
110
+
111
+ def path_conditions
112
+ {self.base_class.uid_field.in => path_ids}
113
+ end
114
+
115
+ def path depth_options = {}
116
+ self.base_class.scope_depth(depth_options, depth).where(path_conditions)
117
+ end
118
+
119
+ def depth
120
+ ancestor_ids.size
121
+ end
122
+
123
+ def cache_depth
124
+ write_attribute self.base_class.depth_cache_field, depth
125
+ end
126
+
127
+ # Parent
128
+ def parent= parent
129
+ write_attribute(self.base_class.ancestry_field, parent.blank? ? nil : parent.child_ancestry)
130
+ end
131
+
132
+ def parent_id= parent_id
133
+ self.parent = parent_id.blank? ? nil : self.base_class.find_by_uid!(parent_id)
134
+ end
135
+
136
+ def parent_id
137
+ ancestor_ids.empty? ? nil : ancestor_ids.last
138
+ end
139
+
140
+ def parent
141
+ parent_id.blank? ? nil : self.base_class.find_by_uid!(parent_id)
142
+ end
143
+
144
+ # Root
145
+ def root_id
146
+ ancestor_ids.empty? ? read_attribute(self.base_class.uid_field) : ancestor_ids.first
147
+ end
148
+
149
+ def root
150
+ (root_id == read_attribute(self.base_class.uid_field)) ? self : self.base_class.find_by_uid!(root_id)
151
+ end
152
+
153
+ def is_root?
154
+ read_attribute(self.base_class.ancestry_field).blank?
155
+ end
156
+
157
+ # Children
158
+ def child_conditions
159
+ {self.base_class.ancestry_field => child_ancestry}
160
+ end
161
+
162
+ def children
163
+ self.base_class.where(child_conditions)
164
+ end
165
+
166
+ def child_ids
167
+ children.only(self.base_class.uid_field).all.map(&self.base_class.uid_field)
168
+ end
169
+
170
+ def has_children?
171
+ self.children.present?
172
+ end
173
+
174
+ def is_childless?
175
+ !has_children?
176
+ end
177
+
178
+ # Siblings
179
+ def sibling_conditions
180
+ {self.base_class.ancestry_field => read_attribute(self.base_class.ancestry_field)}
181
+ end
182
+
183
+ def siblings
184
+ self.base_class.where sibling_conditions
185
+ end
186
+
187
+ def sibling_ids
188
+ siblings.only(self.base_class.uid_field).all.collect(&self.base_class.uid_field)
189
+ end
190
+
191
+ def has_siblings?
192
+ self.siblings.count > 1
193
+ end
194
+
195
+ def is_only_child?
196
+ !has_siblings?
197
+ end
198
+
199
+ # Descendants
200
+ def descendant_conditions
201
+ #["#{self.base_class.ancestry_field} like ? or #{self.base_class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
202
+ [
203
+ { self.base_class.ancestry_field => /^#{child_ancestry}\// },
204
+ { self.base_class.ancestry_field => child_ancestry }
205
+ ]
206
+ end
207
+
208
+ def descendants depth_options = {}
209
+ self.base_class.scope_depth(depth_options, depth).any_of(descendant_conditions)
210
+ end
211
+
212
+ def descendant_ids depth_options = {}
213
+ descendants(depth_options).only(self.base_class.uid_field).collect(&self.base_class.uid_field)
214
+ end
215
+
216
+ # Subtree
217
+ def subtree_conditions
218
+ #["#{self.base_class.primary_key} = ? or #{self.base_class.ancestry_column} like ? or #{self.base_class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
219
+ [
220
+ { self.base_class.uid_field => read_attribute(self.base_class.uid_field) },
221
+ { self.base_class.ancestry_field => /^#{child_ancestry}\// },
222
+ { self.base_class.ancestry_field => child_ancestry }
223
+ ]
224
+ end
225
+
226
+ def subtree depth_options = {}
227
+ self.base_class.scope_depth(depth_options, depth).any_of(subtree_conditions)
228
+ end
229
+
230
+ def subtree_ids depth_options = {}
231
+ subtree(depth_options).select(self.base_class.uid_field).all.collect(&self.base_class.uid_field)
232
+ end
233
+
234
+ # Callback disabling
235
+ def without_ancestry_callbacks
236
+ @disable_ancestry_callbacks = true
237
+ yield
238
+ @disable_ancestry_callbacks = false
239
+ end
240
+
241
+ def ancestry_callbacks_disabled?
242
+ !!@disable_ancestry_callbacks
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,14 @@
1
+ module Mongoid
2
+ module Ancestry
3
+ extend ActiveSupport::Concern
4
+
5
+ autoload :ClassMethods, 'mongoid/ancestry/class_methods'
6
+ autoload :InstanceMethods, 'mongoid/ancestry/instance_methods'
7
+ autoload :Error, 'mongoid/ancestry/exceptions'
8
+
9
+ included do
10
+ cattr_accessor :base_class
11
+ self.base_class = self
12
+ end
13
+ end
14
+ end
data/log/.gitkeep ADDED
File without changes
@@ -0,0 +1,125 @@
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-ancestry}
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 = ["Stefan Kroes", "Anton Orel"]
12
+ s.date = %q{2011-04-16}
13
+ s.description = %q{Organise Mongoid model into a tree structure}
14
+ s.email = %q{eagle.anton@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ "Gemfile",
20
+ "Gemfile.lock",
21
+ "Guardfile",
22
+ "MIT-LICENSE",
23
+ "README.md",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "init.rb",
27
+ "install.rb",
28
+ "lib/mongoid/ancestry.rb",
29
+ "lib/mongoid/ancestry/class_methods.rb",
30
+ "lib/mongoid/ancestry/exceptions.rb",
31
+ "lib/mongoid/ancestry/instance_methods.rb",
32
+ "log/.gitkeep",
33
+ "mongoid-ancestry.gemspec",
34
+ "spec/mongoid/ancestry/class_methods_spec.rb",
35
+ "spec/mongoid/ancestry/instance_methods_spec.rb",
36
+ "spec/mongoid/ancestry_spec.rb",
37
+ "spec/spec_helper.rb",
38
+ "spec/support/models.rb"
39
+ ]
40
+ s.homepage = %q{http://github.com/skyeagle/mongoid-ancestry}
41
+ s.licenses = ["MIT"]
42
+ s.require_paths = ["lib"]
43
+ s.rubygems_version = %q{1.5.2}
44
+ s.summary = %q{'Ancestry allows the records of a Mongoid model to be organised in a tree structure, using a single, intuitively formatted database field. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single query. Additional features are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.'}
45
+ s.test_files = [
46
+ "spec/mongoid/ancestry/class_methods_spec.rb",
47
+ "spec/mongoid/ancestry/instance_methods_spec.rb",
48
+ "spec/mongoid/ancestry_spec.rb",
49
+ "spec/spec_helper.rb",
50
+ "spec/support/models.rb"
51
+ ]
52
+
53
+ if s.respond_to? :specification_version then
54
+ s.specification_version = 3
55
+
56
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
+ s.add_runtime_dependency(%q<mongoid-ancestry>, [">= 0"])
58
+ s.add_development_dependency(%q<rspec>, ["~> 2.5"])
59
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
60
+ s.add_development_dependency(%q<guard-rspec>, ["~> 0.2"])
61
+ s.add_development_dependency(%q<libnotify>, ["~> 0.3"])
62
+ s.add_development_dependency(%q<rb-inotify>, ["~> 0.8"])
63
+ s.add_development_dependency(%q<fuubar>, ["~> 0.0.4"])
64
+ s.add_development_dependency(%q<rspec>, ["~> 2.5"])
65
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
66
+ s.add_development_dependency(%q<guard-rspec>, ["~> 0.2"])
67
+ s.add_development_dependency(%q<libnotify>, ["~> 0.3"])
68
+ s.add_development_dependency(%q<rb-inotify>, ["~> 0.8"])
69
+ s.add_development_dependency(%q<fuubar>, ["~> 0.0.4"])
70
+ s.add_runtime_dependency(%q<mongoid>, ["~> 2.0"])
71
+ s.add_runtime_dependency(%q<bson_ext>, ["~> 1.3"])
72
+ s.add_development_dependency(%q<rspec>, ["~> 2.5"])
73
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
74
+ s.add_development_dependency(%q<guard-rspec>, ["~> 0.2"])
75
+ s.add_development_dependency(%q<libnotify>, ["~> 0.3"])
76
+ s.add_development_dependency(%q<rb-inotify>, ["~> 0.8"])
77
+ s.add_development_dependency(%q<fuubar>, ["~> 0.0.4"])
78
+ else
79
+ s.add_dependency(%q<mongoid-ancestry>, [">= 0"])
80
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
81
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
82
+ s.add_dependency(%q<guard-rspec>, ["~> 0.2"])
83
+ s.add_dependency(%q<libnotify>, ["~> 0.3"])
84
+ s.add_dependency(%q<rb-inotify>, ["~> 0.8"])
85
+ s.add_dependency(%q<fuubar>, ["~> 0.0.4"])
86
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
87
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
88
+ s.add_dependency(%q<guard-rspec>, ["~> 0.2"])
89
+ s.add_dependency(%q<libnotify>, ["~> 0.3"])
90
+ s.add_dependency(%q<rb-inotify>, ["~> 0.8"])
91
+ s.add_dependency(%q<fuubar>, ["~> 0.0.4"])
92
+ s.add_dependency(%q<mongoid>, ["~> 2.0"])
93
+ s.add_dependency(%q<bson_ext>, ["~> 1.3"])
94
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
95
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
96
+ s.add_dependency(%q<guard-rspec>, ["~> 0.2"])
97
+ s.add_dependency(%q<libnotify>, ["~> 0.3"])
98
+ s.add_dependency(%q<rb-inotify>, ["~> 0.8"])
99
+ s.add_dependency(%q<fuubar>, ["~> 0.0.4"])
100
+ end
101
+ else
102
+ s.add_dependency(%q<mongoid-ancestry>, [">= 0"])
103
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
104
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
105
+ s.add_dependency(%q<guard-rspec>, ["~> 0.2"])
106
+ s.add_dependency(%q<libnotify>, ["~> 0.3"])
107
+ s.add_dependency(%q<rb-inotify>, ["~> 0.8"])
108
+ s.add_dependency(%q<fuubar>, ["~> 0.0.4"])
109
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
110
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
111
+ s.add_dependency(%q<guard-rspec>, ["~> 0.2"])
112
+ s.add_dependency(%q<libnotify>, ["~> 0.3"])
113
+ s.add_dependency(%q<rb-inotify>, ["~> 0.8"])
114
+ s.add_dependency(%q<fuubar>, ["~> 0.0.4"])
115
+ s.add_dependency(%q<mongoid>, ["~> 2.0"])
116
+ s.add_dependency(%q<bson_ext>, ["~> 1.3"])
117
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
118
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
119
+ s.add_dependency(%q<guard-rspec>, ["~> 0.2"])
120
+ s.add_dependency(%q<libnotify>, ["~> 0.3"])
121
+ s.add_dependency(%q<rb-inotify>, ["~> 0.8"])
122
+ s.add_dependency(%q<fuubar>, ["~> 0.0.4"])
123
+ end
124
+ end
125
+
@@ -0,0 +1,305 @@
1
+ require 'spec_helper'
2
+
3
+ describe MongoidAncestry do
4
+
5
+ subject { MongoidAncestry }
6
+
7
+ it "should have scopes" do
8
+ subject.with_model :depth => 3, :width => 3 do |model, roots|
9
+ # Roots assertion
10
+ model.roots.all.to_a.should eql(roots.map(&:first))
11
+
12
+ model.all.each do |test_node|
13
+ # Assertions for ancestors_of named scope
14
+ model.ancestors_of(test_node).all.should == test_node.ancestors.all
15
+ model.ancestors_of(test_node.id).all.to_a.should eql(test_node.ancestors.all.to_a)
16
+ # Assertions for children_of named scope
17
+ model.children_of(test_node).all.to_a.should eql(test_node.children.all.to_a)
18
+ model.children_of(test_node.id).all.to_a.should eql(test_node.children.all.to_a)
19
+ # Assertions for descendants_of named scope
20
+ model.descendants_of(test_node).all.should == (test_node.descendants.all)
21
+ model.descendants_of(test_node.id).all.to_a.should eql(test_node.descendants.all.to_a)
22
+ # Assertions for subtree_of named scope
23
+ model.subtree_of(test_node).all.to_a.should eql(test_node.subtree.all.to_a)
24
+ model.subtree_of(test_node.id).all.to_a.should eql(test_node.subtree.all.to_a)
25
+ # Assertions for siblings_of named scope
26
+ model.siblings_of(test_node).all.to_a.should eql(test_node.siblings.all.to_a)
27
+ model.siblings_of(test_node.id).all.to_a.should eql(test_node.siblings.all.to_a)
28
+ end
29
+ end
30
+ end
31
+
32
+ it "should be arranged" do
33
+ subject.with_model :depth => 3, :width => 3 do |model, roots|
34
+ id_sorter = Proc.new do |a, b|; a.uid <=> b.uid; end
35
+ arranged_nodes = model.arrange
36
+ arranged_nodes.size.should eql(3)
37
+ arranged_nodes.each do |node, children|
38
+ children.keys.sort(&id_sorter).should eql(node.children.sort(&id_sorter))
39
+ children.each do |node, children|
40
+ children.keys.sort(&id_sorter).should eql(node.children.sort(&id_sorter))
41
+ children.each do |node, children|
42
+ children.size.should eql(0)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ it "should have arrange order option" do
50
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
51
+ descending_nodes_lvl0 = model.arrange :order => [:uid, :desc]
52
+ ascending_nodes_lvl0 = model.arrange :order => [:uid, :asc]
53
+
54
+ descending_nodes_lvl0.keys.zip(ascending_nodes_lvl0.keys.reverse).each do |descending_node, ascending_node|
55
+ ascending_node.should eql(descending_node)
56
+ descending_nodes_lvl1 = descending_nodes_lvl0[descending_node]
57
+ ascending_nodes_lvl1 = ascending_nodes_lvl0[ascending_node]
58
+ descending_nodes_lvl1.keys.zip(ascending_nodes_lvl1.keys.reverse).each do |descending_node, ascending_node|
59
+ ascending_node.should eql(descending_node)
60
+ descending_nodes_lvl2 = descending_nodes_lvl1[descending_node]
61
+ ascending_nodes_lvl2 = ascending_nodes_lvl1[ascending_node]
62
+ descending_nodes_lvl2.keys.zip(ascending_nodes_lvl2.keys.reverse).each do |descending_node, ascending_node|
63
+ ascending_node.should eql(descending_node)
64
+ descending_nodes_lvl3 = descending_nodes_lvl2[descending_node]
65
+ ascending_nodes_lvl3 = ascending_nodes_lvl2[ascending_node]
66
+ descending_nodes_lvl3.keys.zip(ascending_nodes_lvl3.keys.reverse).each do |descending_node, ascending_node|
67
+ ascending_node.should eql(descending_node)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ it "should have valid orphan rootify strategy" do
76
+ subject.with_model :depth => 3, :width => 3 do |model, roots|
77
+ model.orphan_strategy = :rootify
78
+ root = roots.first.first
79
+ children = root.children.all
80
+ root.destroy
81
+ children.each do |child|
82
+ child.reload
83
+ child.is_root?.should be_true
84
+ child.children.size.should eql(3)
85
+ end
86
+ end
87
+ end
88
+
89
+ it "should have valid orphan destroy strategy" do
90
+ subject.with_model :depth => 3, :width => 3 do |model, roots|
91
+ model.orphan_strategy = :destroy
92
+ root = roots.first.first
93
+ expect { root.destroy }.to change(model, :count).by(-root.subtree.size)
94
+ node = model.roots.first.children.first
95
+ expect { node.destroy }.to change(model, :count).by(-node.subtree.size)
96
+ end
97
+ end
98
+
99
+ it "should have valid orphan restrict strategy" do
100
+ subject.with_model :depth => 3, :width => 3 do |model, roots|
101
+ model.orphan_strategy = :restrict
102
+ root = roots.first.first
103
+ expect { root.destroy }.to raise_error Mongoid::Ancestry::Error
104
+ expect { root.children.first.children.first.destroy }.to_not raise_error Mongoid::Ancestry::Error
105
+ end
106
+ end
107
+
108
+ it "should find record by uid" do
109
+ subject.with_model do |model|
110
+ expect { model.find_by_uid!(1)}.to raise_error(Mongoid::Errors::DocumentNotFound)
111
+ instance = model.create!
112
+ expect { model.find_by_uid!(1).should eql(instance)}.to_not raise_error(Mongoid::Errors::DocumentNotFound)
113
+ end
114
+ end
115
+
116
+ it "should check that there are no errors on a valid tree" do
117
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
118
+ expect { model.check_ancestry_integrity! }.to_not raise_error(Mongoid::Ancestry::Error)
119
+ model.check_ancestry_integrity!(:report => :list).size.should eql(0)
120
+ end
121
+ end
122
+
123
+ it "should check detection of invalid format for ancestry field" do
124
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
125
+ roots.first.first.update_attribute model.ancestry_field, 'invalid_ancestry'
126
+ expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
127
+ model.check_ancestry_integrity!(:report => :list).size.should eql(1)
128
+ end
129
+ end
130
+
131
+ it "should check detection of non-existent ancestor" do
132
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
133
+ roots.first.first.update_attribute model.ancestry_field, 35
134
+ expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
135
+ model.check_ancestry_integrity!(:report => :list).size.should eql(1)
136
+ end
137
+ end
138
+
139
+ it "should check detection of cyclic ancestry" do
140
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
141
+ node = roots.first.first
142
+ node.update_attribute model.ancestry_field, node.uid
143
+ expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
144
+ model.check_ancestry_integrity!(:report => :list).size.should eql(1)
145
+ end
146
+ end
147
+
148
+ it "should check detection of conflicting parent id" do
149
+ subject.with_model do |model|
150
+ model.destroy_all
151
+ model.create!(model.ancestry_field => model.create!(model.ancestry_field => model.create!(model.ancestry_field => nil).uid).uid)
152
+ expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
153
+ model.check_ancestry_integrity!(:report => :list).size.should eql(1)
154
+ end
155
+ end
156
+
157
+ def assert_integrity_restoration model
158
+ expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
159
+ model.restore_ancestry_integrity!
160
+ expect { model.check_ancestry_integrity! }.to_not raise_error(Mongoid::Ancestry::IntegrityError)
161
+ end
162
+
163
+ it "should check that integrity is restored for invalid format for ancestry field" do
164
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
165
+ roots.first.first.update_attribute model.ancestry_field, 'invalid_ancestry'
166
+ assert_integrity_restoration model
167
+ end
168
+ end
169
+
170
+ it "should check that integrity is restored for non-existent ancestor" do
171
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
172
+ roots.first.first.update_attribute model.ancestry_field, 35
173
+ assert_integrity_restoration model
174
+ end
175
+ end
176
+
177
+ it "should check that integrity is restored for cyclic ancestry" do
178
+ subject.with_model :width => 3, :depth => 3 do |model, roots|
179
+ node = roots.first.first
180
+ node.update_attribute model.ancestry_field, node.uid
181
+ assert_integrity_restoration model
182
+ end
183
+ end
184
+
185
+ it "should check that integrity is restored for conflicting parent id" do
186
+ subject.with_model do |model|
187
+ model.destroy_all
188
+ model.create!(model.ancestry_field => model.create!(model.ancestry_field => model.create!(model.ancestry_field => nil).uid).uid)
189
+ assert_integrity_restoration model
190
+ end
191
+ end
192
+
193
+ it "should create node through scope" do
194
+ subject.with_model do |model|
195
+ node = model.create!
196
+ child = node.children.create # doesn't pass with .create!
197
+ child.parent.should eql(node)
198
+
199
+ other_child = child.siblings.create # doesn't pass with .create!
200
+ other_child.parent.should eql(node)
201
+
202
+ grandchild = model.children_of(child).build # doesn't pass with .new
203
+ grandchild.save
204
+ grandchild.parent.should eql(child)
205
+
206
+ other_grandchild = model.siblings_of(grandchild).build # doesn't pass with .new
207
+ other_grandchild.save!
208
+ other_grandchild.parent.should eql(child)
209
+ end
210
+ end
211
+
212
+ it "should have depth scopes" do
213
+ subject.with_model :depth => 4, :width => 2, :cache_depth => true do |model, roots|
214
+ model.before_depth(2).all? { |node| node.depth < 2 }.should be_true
215
+ model.to_depth(2).all? { |node| node.depth <= 2 }.should be_true
216
+ model.at_depth(2).all? { |node| node.depth == 2 }.should be_true
217
+ model.from_depth(2).all? { |node| node.depth >= 2 }.should be_true
218
+ model.after_depth(2).all? { |node| node.depth > 2 }.should be_true
219
+ end
220
+ end
221
+
222
+ it "should raise error on invalid scopes" do
223
+ subject.with_model do |model|
224
+ expect { model.before_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
225
+ expect { model.to_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
226
+ expect { model.at_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
227
+ expect { model.from_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
228
+ expect { model.after_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
229
+ end
230
+ end
231
+
232
+ it "should raise error on invalid has_ancestry options" do
233
+ subject.with_model do |model|
234
+ expect { model.has_ancestry :this_option_doesnt_exist => 42 }.to raise_error(Mongoid::Ancestry::Error)
235
+ expect { model.has_ancestry :not_a_hash }.to raise_error(Mongoid::Ancestry::Error)
236
+ end
237
+ end
238
+
239
+ it "should build ancestry from parent ids" do
240
+ subject.with_model :skip_ancestry => true, :extra_columns => {:parent_id => :integer} do |model|
241
+ [model.create!].each do |parent1|
242
+ (Array.new(5) { model.create :parent_id => parent1.id }).each do |parent2|
243
+ (Array.new(5) { model.create :parent_id => parent2.id }).each do |parent3|
244
+ (Array.new(5) { model.create :parent_id => parent3.id })
245
+ end
246
+ end
247
+ end
248
+
249
+ # Assert all nodes where created
250
+ model.count.should eql((0..3).map { |n| 5 ** n }.sum)
251
+ end
252
+
253
+ subject.with_model do |model|
254
+
255
+ model.build_ancestry_from_parent_ids!
256
+
257
+ # Assert ancestry integrity
258
+ model.check_ancestry_integrity!
259
+
260
+ roots = model.roots.all
261
+ ## Assert single root node
262
+ roots.size.should eql(1)
263
+
264
+ ## Assert it has 5 children
265
+ roots.each do |parent|
266
+ parent.children.count.should eql(5)
267
+ parent.children.each do |parent|
268
+ parent.children.count.should eql(5)
269
+ parent.children.each do |parent|
270
+ parent.children.count.should eql(5)
271
+ parent.children.each do |parent|
272
+ parent.children.count.should eql(0)
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ it "should rebuild depth cache" do
281
+ subject.with_model :depth => 3, :width => 3, :cache_depth => true, :depth_cache_field => :depth_cache do |model, roots|
282
+ model.update_all(:depth_cache => nil)
283
+
284
+ # Assert cache was emptied correctly
285
+ model.all.each do |test_node|
286
+ test_node.depth_cache.should eql(nil)
287
+ end
288
+
289
+ # Rebuild cache
290
+ model.rebuild_depth_cache!
291
+
292
+ # Assert cache was rebuild correctly
293
+ model.all.each do |test_node|
294
+ test_node.depth_cache.should eql(test_node.depth)
295
+ end
296
+ end
297
+ end
298
+
299
+ it "should raise exception when rebuilding depth cache for model without depth caching" do
300
+ subject.with_model do |model|
301
+ expect { model.rebuild_depth_cache! }.to raise_error(Mongoid::Ancestry::Error)
302
+ end
303
+ end
304
+
305
+ end