mongoid-ancestry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,75 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mongoid-ancestry (0.0.0)
5
+ bson_ext (~> 1.3)
6
+ mongoid (~> 2.0)
7
+ mongoid-ancestry
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ activemodel (3.0.6)
13
+ activesupport (= 3.0.6)
14
+ builder (~> 2.1.2)
15
+ i18n (~> 0.5.0)
16
+ activesupport (3.0.6)
17
+ bson (1.3.0)
18
+ bson_ext (1.3.0)
19
+ builder (2.1.2)
20
+ chalofa_ruby-progressbar (0.0.9.1)
21
+ configuration (1.2.0)
22
+ diff-lcs (1.1.2)
23
+ ffi (1.0.7)
24
+ rake (>= 0.8.7)
25
+ fuubar (0.0.4)
26
+ chalofa_ruby-progressbar (~> 0.0.9)
27
+ rspec (~> 2.0)
28
+ rspec-instafail (~> 0.1.4)
29
+ guard (0.3.0)
30
+ open_gem (~> 1.4.2)
31
+ thor (~> 0.14.6)
32
+ guard-rspec (0.2.0)
33
+ guard (>= 0.2.2)
34
+ i18n (0.5.0)
35
+ launchy (0.3.7)
36
+ configuration (>= 0.0.5)
37
+ rake (>= 0.8.1)
38
+ libnotify (0.3.0)
39
+ ffi (>= 0.6.2)
40
+ mongo (1.3.0)
41
+ bson (>= 1.3.0)
42
+ mongoid (2.0.1)
43
+ activemodel (~> 3.0)
44
+ mongo (~> 1.3)
45
+ tzinfo (~> 0.3.22)
46
+ will_paginate (~> 3.0.pre)
47
+ open_gem (1.4.2)
48
+ launchy (~> 0.3.5)
49
+ rake (0.8.7)
50
+ rb-inotify (0.8.4)
51
+ ffi (>= 0.5.0)
52
+ rspec (2.5.0)
53
+ rspec-core (~> 2.5.0)
54
+ rspec-expectations (~> 2.5.0)
55
+ rspec-mocks (~> 2.5.0)
56
+ rspec-core (2.5.1)
57
+ rspec-expectations (2.5.0)
58
+ diff-lcs (~> 1.1.2)
59
+ rspec-instafail (0.1.7)
60
+ rspec-mocks (2.5.0)
61
+ thor (0.14.6)
62
+ tzinfo (0.3.26)
63
+ will_paginate (3.0.pre2)
64
+
65
+ PLATFORMS
66
+ ruby
67
+
68
+ DEPENDENCIES
69
+ bundler (~> 1.0)
70
+ fuubar (~> 0.0.4)
71
+ guard-rspec (~> 0.2)
72
+ libnotify (~> 0.3)
73
+ mongoid-ancestry!
74
+ rb-inotify (~> 0.8)
75
+ rspec (~> 2.5)
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2, :cli => "--format Fuubar" do
5
+ watch(%r{^spec/.+_spec\.rb})
6
+ watch(%r{^lib/mongoid/ancestry/(.+)\.rb}) { |m| "spec/mongoid/ancestry/#{m[1]}_spec.rb" }
7
+ watch('lib/mongoid/ancestry.rb') { "spec" }
8
+ watch(%r{^spec/support/(.+)\.rb}) { "spec" }
9
+ watch('spec/spec_helper.rb') { "spec" }
10
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Stefan Kroes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ Mongoid-ancestry
2
+ ================
3
+
4
+ Mongoid-ancestry is a gem/plugin that allows the records of a Ruby on Rails Mongoid model to be organised as a tree structure (or hierarchy). It uses a single, intuitively formatted database column, using a variation on the materialised path pattern. 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 STI support, scopes, depth caching, depth constraints, easy migration from older plugins/gems, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ ## It's Rails 3 only.
10
+
11
+ To apply Mongoid-ancestry to any Mongoid model, follow these simple steps:
12
+
13
+ 1. Install
14
+ Add to Gemfile: `gem 'ancestry'`
15
+ Install required gems: `bundle install`
16
+
17
+ 2. Add ancestry to your model
18
+ Add to app/models/[model].rb:
19
+
20
+ include Mongoid::Ancestry
21
+ has_ancestry
22
+
23
+ Your model is now a tree!
24
+
25
+ = Organising records into a tree
26
+ By default MongoDB provide records with an unique id which can't be use to order their. Therefore, Mongoid-Ancestry
27
+ adds to the all records of your's model an autoincremented unique id(by default `uid` attribute). You can use the parent
28
+ attribute to organise your records into a tree. For these purposes you should use `uid` attribute. You have already had
29
+ the `uid` of the record and you want to use it as a parent and don't want to fetch it, you can use parent_id. Like any
30
+ virtual model attributes, parent and parent_id can be set using parent= and parent_id= on a record or by including them
31
+ in the hash passed to new, create, create!, update_attributes and update_attributes!. For example:
32
+
33
+ TreeNode.create :name => 'Stinky', :parent => TreeNode.create(:name => 'Squeeky')
34
+
35
+ or
36
+
37
+ TreeNode.create :name => 'Stinky', :parent_id => TreeNode.create(:name => 'Squeeky').uid
38
+
39
+ ### Note: It doesn't work with `.create!` at the moment(mongoid bug? needs more investigation). But it absolutely will be fixed.
40
+
41
+
42
+ You can also create children through the children relation on a node:
43
+
44
+ node.children.create :name => 'Stinky'
45
+
46
+ ## Navigating your tree
47
+
48
+ To navigate an Ancestry model, use the following methods on any instance / record:
49
+
50
+ parent Returns the parent of the record, nil for a root node
51
+ parent_id Returns the id of the parent of the record, nil for a root node
52
+ root Returns the root of the tree the record is in, self for a root node
53
+ root_id Returns the id of the root of the tree the record is in
54
+ is_root? Returns true if the record is a root node, false otherwise
55
+ ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
56
+ ancestors Scopes the model on ancestors of the record
57
+ path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
58
+ path Scopes model on path records of the record
59
+ children Scopes the model on children of the record
60
+ child_ids Returns a list of child ids
61
+ has_children? Returns true if the record has any children, false otherwise
62
+ is_childless? Returns true is the record has no childen, false otherwise
63
+ siblings Scopes the model on siblings of the record, the record itself is included
64
+ sibling_ids Returns a list of sibling ids
65
+ has_siblings? Returns true if the record's parent has more than one child
66
+ is_only_child? Returns true if the record is the only child of its parent
67
+ descendants Scopes the model on direct and indirect children of the record
68
+ descendant_ids Returns a list of a descendant ids
69
+ subtree Scopes the model on descendants and itself
70
+ subtree_ids Returns a list of all ids in the record's subtree
71
+ depth Return the depth of the node, root nodes are at depth 0
72
+
73
+ ## Options for has_ancestry
74
+
75
+ The has_ancestry methods supports the following options:
76
+
77
+ :ancestry_field Pass in a symbol to store ancestry in a different field
78
+ :uid_field Pass in a symbol to store unique id in a different field
79
+ :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
80
+ :destroy All children are destroyed as well (default)
81
+ :rootify The children of the destroyed node become root nodes
82
+ :restrict An Error is raised if any children exist
83
+ :cache_depth Cache the depth of each node in the 'ancestry_depth' field (default: false)
84
+ If you turn depth_caching on for an existing model:
85
+ - Build cache: TreeNode.rebuild_depth_cache!
86
+ :depth_cache_field Pass in a symbol to store depth cache in a different field
87
+
88
+ ## (Named) Scopes
89
+
90
+ Where possible, the navigation methods return scopes instead of records, this means additional ordering, conditions, limits, etc. can be applied and that the result can be either retrieved, counted or checked for existence. For example:
91
+
92
+ node.children.where(:name => 'Mary')
93
+ node.subtree.order_by([:name, :desc]).limit(10).each do; ...; end
94
+ node.descendants.count
95
+
96
+ For convenience, a couple of named scopes are included at the class level:
97
+
98
+ roots Root nodes
99
+ ancestors_of(node) Ancestors of node, node can be either a record or an id
100
+ children_of(node) Children of node, node can be either a record or an id
101
+ descendants_of(node) Descendants of node, node can be either a record or an id
102
+ subtree_of(node) Subtree of node, node can be either a record or an id
103
+ siblings_of(node) Siblings of node, node can be either a record or an id
104
+
105
+ Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
106
+
107
+ node.children.create
108
+ node.siblings.create
109
+ TestNode.children_of(node_id).build
110
+ TestNode.siblings_of(node_id).create
111
+
112
+ ## Selecting nodes by depth
113
+
114
+ When depth caching is enabled (see has_ancestry options), five more named scopes can be used to select nodes on their depth:
115
+
116
+ before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
117
+ to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
118
+ at_depth(depth) Return nodes that are at depth (node.depth == depth)
119
+ from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
120
+ after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
121
+
122
+ The depth scopes are also available through calls to descendants, descendant_ids, subtree, subtree_ids, path and ancestors. In this case, depth values are interpreted relatively. Some examples:
123
+
124
+ node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
125
+ node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
126
+ node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
127
+ node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
128
+ node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
129
+ node.path(:from_depth => -2) The node's grandparent, parent and the node itself
130
+
131
+ node.ancestors(:from_depth => -6, :to_depth => -4)
132
+ node.path.from_depth(3).to_depth(4)
133
+ node.descendants(:from_depth => 2, :to_depth => 4)
134
+ node.subtree.from_depth(10).to_depth(12)
135
+
136
+ Please note that depth constraints cannot be passed to ancestor_ids and path_ids. The reason for this is that both these relations can be fetched directly from the ancestry column without performing a database query. It would require an entirely different method of applying the depth constraints which isn't worth the effort of implementing. You can use ancestors(depth_options).map(&:id) or ancestor_ids.slice(min_depth..max_depth) instead.
137
+
138
+ ## STI support
139
+
140
+ Ancestry works fine with STI. Just create a STI inheritance hierarchy and build an Ancestry tree from the different classes/models. All Ancestry relations that where described above will return nodes of any model type. If you do only want nodes of a specific subclass you'll have to add a condition on type for that.
141
+
142
+ ## Arrangement
143
+
144
+ Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
145
+
146
+ { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
147
+ => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
148
+ => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
149
+ => {}
150
+ }
151
+ }
152
+ }
153
+
154
+ The arrange method also works on a scoped class, for example:
155
+
156
+ TreeNode.where(:name => 'Crunchy').first.subtree.arrange
157
+
158
+ The arrange method takes Mongoid find options. If you want your hashes to be ordered, you should pass the order to the arrange method instead of to the scope. This only works for Ruby 1.9 and later since before that hashes weren't ordered. For example:
159
+
160
+ TreeNode.where(:name => 'Crunchy').subtree.arrange(:order => [:name, :asc])
161
+
162
+ ## Migrating from plugin that uses parent_id column
163
+
164
+ With Mongoid-ancestry its easy to migrate from any of these plugins, to do so, use the build_ancestry_from_parent_ids! method on your model. These steps provide a more detailed explanation:
165
+
166
+ 1. Remove old tree plugin or gem and add in Mongoid-ancestry
167
+ See 'Installation' for more info on installing and configuring gem
168
+ Add to app/models/[model].rb:
169
+
170
+ include Mongoid::Ancestry
171
+ has_ancestry
172
+
173
+ Create indexes
174
+
175
+ 2. Change your code
176
+ Most tree calls will probably work fine with ancestry
177
+ Others must be changed or proxied
178
+ Check if all your data is intact and all tests pass
179
+
180
+ 3. Drop parent_id field
181
+
182
+ ## Integrity checking and restoration
183
+
184
+ I don't see any way Mongoid-ancestry tree integrity could get compromised without explicitly setting cyclic parents or invalid ancestry and circumventing validation with update_attribute, if you do, please let me know.
185
+
186
+ Mongoid-ancestry includes some methods for detecting integrity problems and restoring integrity just to be sure. To check integrity use: [Model].check_ancestry_integrity!. An Mongoid::Ancestry::Error will be raised if there are any problems. You can also specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages. To restore integrity use: [Model].restore_ancestry_integrity!.
187
+
188
+ For example, from IRB:
189
+
190
+ >> stinky = TreeNode.create :name => 'Stinky'
191
+ $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
192
+ >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
193
+ $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
194
+ >> stinky.update_attribute :parent, squeeky
195
+ $ true
196
+ >> TreeNode.all
197
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
198
+ >> TreeNode.check_ancestry_integrity!
199
+ !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
200
+ >> TreeNode.restore_ancestry_integrity!
201
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
202
+
203
+ Additionally, if you think something is wrong with your depth cache:
204
+
205
+ >> TreeNode.rebuild_depth_cache!
206
+
207
+ ## Tests
208
+
209
+ The Mongoid-ancestry gem comes with rspec and guard(for automatically specs running) suite consisting of about 40 specs. It takes about 10 seconds to run. To run it yourself check out the repository from GitHub, run `bundle install`, run `guard` and press `Ctrl+\` or just `rake spec`.
210
+
211
+ ## Internals
212
+
213
+ As can be seen in the previous section, Mongoid-ancestry stores a path from the root to the parent for every node. This is a variation on the materialised path database pattern. It allows to fetch any relation (siblings, descendants, etc.) in a single query without the complicated algorithms and incomprehensibility associated with left and right values. Additionally, any inserts, deletes and updates only affect nodes within the affected node's own subtree.
214
+
215
+ The materialised path pattern requires Mongoid-ancestry to use a 'regexp' condition in order to fetch descendants. This should not be particularly slow however since the the condition never starts with a wildcard which allows the DBMS to use the column index. If you have any data on performance with a large number of records, please drop me line.
216
+
217
+ ## Contact and copyright
218
+
219
+ It's a fork of [original ancestry](https://github.com/stefankroes/ancestry) gem but adopted to work with Mongoid.
220
+
221
+ All thanks should goes to Stefan Kroes for his great work.
222
+
223
+ Bug report? Faulty/incomplete documentation? Feature request? Please post an issue on 'http://github.com/skyeagle/mongoid-ancestry/issues'.
224
+
225
+ Copyright (c) 2009 Stefan Kroes, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+
3
+ require 'rake'
4
+
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
8
+ gem.name = "mongoid-ancestry"
9
+ gem.homepage = "http://github.com/skyeagle/mongoid-ancestry"
10
+ gem.license = "MIT"
11
+ gem.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.'}
12
+ gem.description = %Q{Organise Mongoid model into a tree structure}
13
+ gem.email = "eagle.anton@gmail.com"
14
+ gem.authors = ["Stefan Kroes", "Anton Orel"]
15
+ gem.add_runtime_dependency('mongoid', '~> 2.0')
16
+ gem.add_runtime_dependency('bson_ext', '~> 1.3')
17
+ gem.add_development_dependency 'rspec', '~> 2.5'
18
+ gem.add_development_dependency 'bundler', '~> 1.0'
19
+ gem.add_development_dependency 'guard-rspec', '~> 0.2'
20
+ gem.add_development_dependency 'libnotify', '~> 0.3'
21
+ gem.add_development_dependency 'rb-inotify', '~> 0.8'
22
+ gem.add_development_dependency 'fuubar', '~> 0.0.4'
23
+ end
24
+ Jeweler::RubygemsDotOrgTasks.new
25
+
26
+ require 'rspec/core'
27
+ require 'rspec/core/rake_task'
28
+ RSpec::Core::RakeTask.new(:spec) do |spec|
29
+ spec.pattern = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ task :default => :spec
33
+
34
+ require 'rake/rdoctask'
35
+ Rake::RDocTask.new do |rdoc|
36
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
37
+
38
+ rdoc.rdoc_dir = 'rdoc'
39
+ rdoc.title = "mongoid-ancestry #{version}"
40
+ rdoc.rdoc_files.include('README*')
41
+ rdoc.rdoc_files.include('lib/**/*.rb')
42
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'ancestry'
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ puts "Thank you for installing Ancestry. You can visit http://github.com/stefankroes/ancestry to read the documentation."
@@ -0,0 +1,233 @@
1
+ module Mongoid
2
+ module Ancestry
3
+ module ClassMethods
4
+ def has_ancestry(opts = {})
5
+ defaults = {
6
+ :uid_field => :uid,
7
+ :ancestry_field => :ancestry,
8
+ :cache_depth => false,
9
+ :depth_cache_field => :ancestry_depth,
10
+ :orphan_strategy => :destroy
11
+ }
12
+
13
+ valid_opts = [:uid_field, :ancestry_field, :cache_depth, :depth_cache_field, :orphan_strategy]
14
+ unless opts.is_a?(Hash) && opts.keys.all? {|opt| valid_opts.include?(opt) }
15
+ raise Error.new("Invalid options for has_ancestry. Only hash is allowed.\n Defaults: #{defaults.inspect}")
16
+ end
17
+
18
+ opts.symbolize_keys!
19
+
20
+ opts.reverse_merge!(defaults)
21
+
22
+ cattr_accessor :uid_field
23
+ self.uid_field = opts[:uid_field]
24
+
25
+ # Create unique id field accessor and set to option or default
26
+ self.field uid_field, :type => Integer
27
+ self.index uid_field, :unique => true
28
+
29
+ # Create ancestry field accessor and set to option or default
30
+ cattr_accessor :ancestry_field
31
+ self.ancestry_field = opts[:ancestry_field]
32
+
33
+ self.field ancestry_field, :type => String
34
+ self.index ancestry_field
35
+
36
+ # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
37
+ cattr_reader :orphan_strategy
38
+ self.orphan_strategy = opts[:orphan_strategy]
39
+
40
+ # Validate format of ancestry column value
41
+ primary_key_format = opts[:primary_key_format] || /[0-9]+/
42
+ validates_format_of ancestry_field, :with => /\A#{primary_key_format.source}(\/#{primary_key_format.source})*\Z/, :allow_nil => true
43
+
44
+ # Validate that the ancestor ids don't include own id
45
+ validate :ancestry_exclude_self
46
+
47
+ # Create ancestry column accessor and set to option or default
48
+ if opts[:cache_depth]
49
+ # Create accessor for column name and set to option or default
50
+ self.cattr_accessor :depth_cache_field
51
+ self.depth_cache_field = opts[:depth_cache_field]
52
+
53
+ # Cache depth in depth cache column before save
54
+ before_validation :cache_depth
55
+
56
+ # Validate depth column
57
+ validates_numericality_of depth_cache_field, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
58
+ end
59
+
60
+ # Create named scopes for depth
61
+ {:before_depth => 'lt', :to_depth => 'lte', :at_depth => nil, :from_depth => 'gte', :after_depth => 'gt'}.each do |scope_name, operator|
62
+ scope scope_name, lambda { |depth|
63
+ raise Error.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless opts[:cache_depth]
64
+ where( (operator ? depth_cache_field.send(operator.to_sym) : depth_cache_field) => depth)
65
+ }
66
+ end
67
+
68
+ scope :roots, where(ancestry_field => nil)
69
+ scope :ancestors_of, lambda { |object| where(to_node(object).ancestor_conditions) }
70
+ scope :children_of, lambda { |object| where(to_node(object).child_conditions) }
71
+ scope :descendants_of, lambda { |object| any_of(to_node(object).descendant_conditions) }
72
+ scope :subtree_of, lambda { |object| any_of(to_node(object).subtree_conditions) }
73
+ scope :siblings_of, lambda { |object| where(to_node(object).sibling_conditions) }
74
+ scope :ordered_by_ancestry, asc(:"#{self.base_class.ancestry_field}")
75
+ scope :ordered_by_ancestry_and, lambda {|by| ordered_by_ancestry.order_by([by]) }
76
+
77
+ before_create :set_uid
78
+
79
+ # Update descendants with new ancestry before save
80
+ before_save :update_descendants_with_new_ancestry
81
+
82
+ # Apply orphan strategy before destroy
83
+ before_destroy :apply_orphan_strategy
84
+ end
85
+
86
+ def find_by_uid!(uid)
87
+ where(self.base_class.uid_field => uid).first || raise(Mongoid::Errors::DocumentNotFound.new(self, uid))
88
+ end
89
+
90
+ # Fetch tree node if necessary
91
+ def to_node object
92
+ object.is_a?(self.base_class) ? object : find(object)
93
+ end
94
+
95
+ # Scope on relative depth options
96
+ def scope_depth depth_options, depth
97
+ depth_options.inject(self.base_class) do |scope, option|
98
+ scope_name, relative_depth = option
99
+ if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
100
+ scope.send scope_name, depth + relative_depth
101
+ else
102
+ raise Error.new("Unknown depth option: #{scope_name}.")
103
+ end
104
+ end
105
+ end
106
+
107
+ # Orphan strategy writer
108
+ def orphan_strategy= orphan_strategy
109
+ # Check value of orphan strategy, only rootify, restrict or destroy is allowed
110
+ if [:rootify, :restrict, :destroy].include? orphan_strategy
111
+ class_variable_set :@@orphan_strategy, orphan_strategy
112
+ else
113
+ raise Error.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
114
+ end
115
+ end
116
+
117
+ # Arrangement
118
+ def arrange options = {}
119
+ scope =
120
+ if options[:order].nil?
121
+ self.base_class.ordered_by_ancestry
122
+ else
123
+ self.base_class.ordered_by_ancestry_and options.delete(:order)
124
+ end
125
+ # Get all nodes ordered by ancestry and start sorting them into an empty hash
126
+ scope.all(options).inject(ActiveSupport::OrderedHash.new) do |arranged_nodes, node|
127
+ # Find the insertion point for that node by going through its ancestors
128
+ node.ancestor_ids.inject(arranged_nodes) do |insertion_point, ancestor_id|
129
+ insertion_point.each do |parent, children|
130
+ # Change the insertion point to children if node is a descendant of this parent
131
+ insertion_point = children if ancestor_id == parent.read_attribute(self.base_class.uid_field)
132
+ end; insertion_point
133
+ end[node] = ActiveSupport::OrderedHash.new; arranged_nodes
134
+ end
135
+ end
136
+
137
+ # Integrity checking
138
+ def check_ancestry_integrity! options = {}
139
+ parents = {}
140
+ exceptions = [] if options[:report] == :list
141
+ # For each node ...
142
+ self.base_class.all.each do |node|
143
+ begin
144
+ # ... check validity of ancestry column
145
+ if !node.valid? and !node.errors[node.class.ancestry_field].blank?
146
+ raise IntegrityError.new "Invalid format for ancestry column of node #{node.read_attribute node.uid_field}: #{node.read_attribute node.ancestry_field}."
147
+ end
148
+ # ... check that all ancestors exist
149
+ node.ancestor_ids.each do |ancestor_id|
150
+ unless where(:uid => ancestor_id).first
151
+ raise IntegrityError.new "Reference to non-existent node in node #{node.read_attribute node.uid_field}: #{ancestor_id}."
152
+ end
153
+ end
154
+ # ... check that all node parents are consistent with values observed earlier
155
+ node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
156
+ parents[node_id] = parent_id unless parents.has_key? node_id
157
+ unless parents[node_id] == parent_id
158
+ raise IntegrityError.new "Conflicting parent id found in node #{node.read_attribute node.uid_field}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}"
159
+ end
160
+ end
161
+ rescue IntegrityError => integrity_exception
162
+ case options[:report]
163
+ when :list then exceptions << integrity_exception
164
+ when :echo then puts integrity_exception
165
+ else raise integrity_exception
166
+ end
167
+ end
168
+ end
169
+ exceptions if options[:report] == :list
170
+ end
171
+
172
+ # Integrity restoration
173
+ def restore_ancestry_integrity!
174
+ parents = {}
175
+ # For each node ...
176
+ self.base_class.all.each do |node|
177
+ # ... set its ancestry to nil if invalid
178
+ if node.errors[node.class.ancestry_field].blank?
179
+ node.without_ancestry_callbacks do
180
+ node.update_attribute node.ancestry_field, nil
181
+ end
182
+ end
183
+ # ... save parent of this node in parents array if it exists
184
+ parents[node.read_attribute(node.uid_field)] = node.parent_id if exists? node.parent_id
185
+
186
+ # Reset parent id in array to nil if it introduces a cycle
187
+ parent = parents[node.read_attribute(node.uid_field)]
188
+ until parent.nil? || parent == node.read_attribute(node.uid_field)
189
+ parent = parents[parent]
190
+ end
191
+ parents[node.read_attribute(node.uid_field)] = nil if parent == node.read_attribute(node.uid_field)
192
+ end
193
+ # For each node ...
194
+ self.base_class.all.each do |node|
195
+ # ... rebuild ancestry from parents array
196
+ ancestry, parent = nil, parents[node.read_attribute(node.uid_field)]
197
+ until parent.nil?
198
+ ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
199
+ end
200
+ node.without_ancestry_callbacks do
201
+ node.update_attribute node.ancestry_field, ancestry
202
+ end
203
+ end
204
+ end
205
+
206
+ # Build ancestry from parent id's for migration purposes
207
+ def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
208
+ self.base_class.where(:parent_id => parent_id).all.each do |node|
209
+ node.without_ancestry_callbacks do
210
+ retries = 3
211
+ begin
212
+ node.set_uid
213
+ node.send(:"#{ancestry_field}=", ancestry)
214
+ node.save!
215
+ rescue Mongo::OperationFailure => e
216
+ (retries -= 1) > 0 && e.to_s =~ %r{duplicate key error.+\$#{self.base_class.uid_field}} ? retry : raise(e)
217
+ end
218
+ end
219
+ build_ancestry_from_parent_ids! node.id,
220
+ if ancestry.nil? then "#{node.read_attribute(self.base_class.uid_field)}" else "#{ancestry}/#{node.read_attribute(self.base_class.uid_field)}" end
221
+ end
222
+ end
223
+
224
+ # Rebuild depth cache if it got corrupted or if depth caching was just turned on
225
+ def rebuild_depth_cache!
226
+ raise Error.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_field
227
+ self.base_class.all.each do |node|
228
+ node.update_attribute depth_cache_field, node.depth
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,6 @@
1
+ module Mongoid
2
+ module Ancestry
3
+ class Error < RuntimeError; end
4
+ class IntegrityError < RuntimeError; end
5
+ end
6
+ end