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