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.
- data/Gemfile +3 -0
- data/Gemfile.lock +75 -0
- data/Guardfile +10 -0
- data/MIT-LICENSE +20 -0
- data/README.md +225 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/mongoid/ancestry/class_methods.rb +233 -0
- data/lib/mongoid/ancestry/exceptions.rb +6 -0
- data/lib/mongoid/ancestry/instance_methods.rb +246 -0
- data/lib/mongoid/ancestry.rb +14 -0
- data/log/.gitkeep +0 -0
- data/mongoid-ancestry.gemspec +125 -0
- data/spec/mongoid/ancestry/class_methods_spec.rb +305 -0
- data/spec/mongoid/ancestry/instance_methods_spec.rb +266 -0
- data/spec/mongoid/ancestry_spec.rb +117 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/models.rb +44 -0
- metadata +310 -0
@@ -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
|