mongoid-ancestry-fixes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Yzc5YjFkNWJjNWM2MDZmNGU3NmY4NWY1NmZlZGEwZGE4MjkyYTI2Mg==
5
+ data.tar.gz: !binary |-
6
+ Y2FlMDMyNWZlM2U2ZWZjNGM0ODU4YjIzYjk2Y2M5ZTIzMzdhOTdmOQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OTljNjA5NDUxODZkYTI2YzZhOTAzZTBkZGE3ZDYyZWU0YmFiODk5OTYwNmJm
10
+ OWFhZTRiMWE0MmVlMGEwZThiZmY1YTQ3NDg0MGRkMDUyNjBkNzdkYmNjMjRh
11
+ ZWE4NTk0MzhkNzBkZjY4MzI3MmZlZjRjY2YwZjIwYjc4MTM3YTk=
12
+ data.tar.gz: !binary |-
13
+ ZDBiMDNhYzQ5YzBmZTA2YWVkY2ZhMGFhOTI1ZDMwODFiYWM5ZmZmMjMzOTI4
14
+ YjYxZjM5ZDIyNmVjYzZlYTM4ZjhkZGRiMDEwZGIyMmM4OWIyYTY0NzY5ODhh
15
+ NGMwYTg3MjI2YThkMjRmM2Y1ZmU4N2E5YmE3ZjgxNmIzYWM4MjA=
data/.gitignore ADDED
@@ -0,0 +1,42 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # jeweler generated
15
+ pkg
16
+
17
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
18
+ #
19
+ # * Create a file at ~/.gitignore
20
+ # * Include files you want ignored
21
+ # * Run: git config --global core.excludesfile ~/.gitignore
22
+ #
23
+ # After doing this, these files will be ignored in all your git projects,
24
+ # saving you from having to 'pollute' every project you touch with them
25
+ #
26
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
27
+ #
28
+ # For MacOS:
29
+ #
30
+ #.DS_Store
31
+ #
32
+ # For TextMate
33
+ #*.tmproj
34
+ #tmtags
35
+ #
36
+ # For emacs:
37
+ #*~
38
+ #\#*
39
+ #.\#*
40
+ #
41
+ # For vim:
42
+ #*.swp
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rake'
7
+ gem 'rspec', '~> 2.5'
8
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,47 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mongoid-ancestry (0.2.3)
5
+ bson_ext (>= 1.3)
6
+ mongoid (>= 2.0)
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ activemodel (3.2.5)
12
+ activesupport (= 3.2.5)
13
+ builder (~> 3.0.0)
14
+ activesupport (3.2.5)
15
+ i18n (~> 0.6)
16
+ multi_json (~> 1.0)
17
+ bson (1.6.4)
18
+ bson_ext (1.6.4)
19
+ bson (~> 1.6.4)
20
+ builder (3.0.0)
21
+ diff-lcs (1.1.2)
22
+ i18n (0.6.0)
23
+ mongo (1.6.2)
24
+ bson (~> 1.6.2)
25
+ mongoid (2.4.7)
26
+ activemodel (~> 3.1)
27
+ mongo (~> 1.3)
28
+ tzinfo (~> 0.3.22)
29
+ multi_json (1.3.6)
30
+ rake (0.9.2.2)
31
+ rspec (2.5.0)
32
+ rspec-core (~> 2.5.0)
33
+ rspec-expectations (~> 2.5.0)
34
+ rspec-mocks (~> 2.5.0)
35
+ rspec-core (2.5.1)
36
+ rspec-expectations (2.5.0)
37
+ diff-lcs (~> 1.1.2)
38
+ rspec-mocks (2.5.0)
39
+ tzinfo (0.3.33)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ mongoid-ancestry!
46
+ rake
47
+ 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/lib/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,255 @@
1
+ This is a fork of [mongoid-ancestry-0.2.3](https://github.com/skyeagle/mongoid-ancestry/tree/v0.2.3) with some fixes
2
+ ===================================================================
3
+
4
+ ##fixes in lib/mongoid-ancestry/instance_methods.rb
5
+
6
+ def current_search_scope
7
+ self.embedded? ? self._parent.send(self.base_class.to_s.tableize) : self.base_class
8
+ end
9
+
10
+ def parent_id= parent_id
11
+ self.parent = parent_id.blank? ? nil : current_search_scope.find(parent_id)
12
+ end
13
+
14
+ def parent
15
+ parent_id.blank? ? nil : current_search_scope.find(parent_id)
16
+ end
17
+
18
+ def root_id
19
+ (root_id == id) ? self : current_search.find(root_id)
20
+ end
21
+
22
+ def root
23
+ (root_id == id) ? self : current_search_scope.find(root_id)
24
+ end
25
+
26
+ def children
27
+ current_search_scope.where(child_conditions)
28
+ end
29
+
30
+ [![travis](https://secure.travis-ci.org/joe1chen/mongoid-ancestry.png)](http://travis-ci.org/joe1chen/mongoid-ancestry)
31
+
32
+ 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.
33
+
34
+ ## Installation
35
+
36
+ ### It's Rails 3 only.
37
+
38
+ To apply Mongoid-ancestry to any Mongoid model, follow these simple steps:
39
+
40
+ 1. Install
41
+
42
+ * Add to Gemfile: `gem 'mongoid-ancestry', git: 'https://github.com/shilovk/mongoid-ancestry-0.2.3'`
43
+ * Install required gems: `bundle install`
44
+
45
+ 2. Add ancestry to your model
46
+
47
+ include Mongoid::Ancestry
48
+ has_ancestry
49
+
50
+ Your model is now a tree!
51
+
52
+ ## Organising records into a tree
53
+ You can use the parent attribute to organise your records into a tree. If you have the id of the record you want
54
+ to use as a parent and don't want to fetch it, you can also use `parent_id`. Like any virtual model attributes,
55
+ parent and `parent_id` can be set using `parent=` and `parent_id=` on a record or by including them in the hash passed
56
+ to new, create, create!, `update_attributes` and `update_attributes!`. For example:
57
+
58
+ TreeNode.create :name => 'Stinky', :parent => TreeNode.create(:name => 'Squeeky')
59
+
60
+ or
61
+
62
+ TreeNode.create :name => 'Stinky', :parent_id => TreeNode.create(:name => 'Squeeky').id
63
+
64
+ #### Note: It doesn't work with `.create!` at the moment(mongoid bug? needs more investigation). But it absolutely will be fixed.
65
+
66
+
67
+ You can also create children through the children relation on a node:
68
+
69
+ node.children.create :name => 'Stinky'
70
+
71
+ ## Navigating your tree
72
+
73
+ To navigate an Ancestry model, use the following methods on any instance / record:
74
+
75
+ parent Returns the parent of the record, nil for a root node
76
+ parent_id Returns the id of the parent of the record, nil for a root node
77
+ root Returns the root of the tree the record is in, self for a root node
78
+ root_id Returns the id of the root of the tree the record is in
79
+ is_root? Returns true if the record is a root node, false otherwise
80
+ ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
81
+ ancestors Scopes the model on ancestors of the record
82
+ path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
83
+ path Scopes model on path records of the record
84
+ children Scopes the model on children of the record
85
+ child_ids Returns a list of child ids
86
+ has_children? Returns true if the record has any children, false otherwise
87
+ is_childless? Returns true is the record has no childen, false otherwise
88
+ siblings Scopes the model on siblings of the record, the record itself is included
89
+ sibling_ids Returns a list of sibling ids
90
+ has_siblings? Returns true if the record's parent has more than one child
91
+ is_only_child? Returns true if the record is the only child of its parent
92
+ descendants Scopes the model on direct and indirect children of the record
93
+ descendant_ids Returns a list of a descendant ids
94
+ subtree Scopes the model on descendants and itself
95
+ subtree_ids Returns a list of all ids in the record's subtree
96
+ depth Return the depth of the node, root nodes are at depth 0
97
+
98
+ ## Options for has_ancestry
99
+
100
+ The `has_ancestry` methods supports the following options:
101
+
102
+ :ancestry_field Pass in a symbol to store ancestry in a different field
103
+ :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
104
+ :destroy All children are destroyed as well (default)
105
+ :rootify The children of the destroyed node become root nodes
106
+ :restrict An Error is raised if any children exist
107
+ :cache_depth Cache the depth of each node in the `ancestry_depth` field (default: false)
108
+ If you turn depth_caching on for an existing model:
109
+ - Mongoid has default configuration attribute `allow_dynamic_fields` as `true`.
110
+ You should manually add this depth field with `Integer` type and `0` as default value
111
+ into your model if `allow_dynamic_fields` is disabled in your configuration.
112
+ - Build cache: TreeNode.rebuild_depth_cache!
113
+ :depth_cache_field Pass in a symbol to store depth cache in a different field
114
+
115
+ ## Scopes
116
+
117
+ 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:
118
+
119
+ node.children.where(:name => 'Mary')
120
+ node.subtree.order_by([:name, :desc]).limit(10).each do; ...; end
121
+ node.descendants.count
122
+
123
+ For convenience, a couple of named scopes are included at the class level:
124
+
125
+ roots Root nodes
126
+ ancestors_of(node) Ancestors of node, node can be either a record or an id
127
+ children_of(node) Children of node, node can be either a record or an id
128
+ descendants_of(node) Descendants of node, node can be either a record or an id
129
+ subtree_of(node) Subtree of node, node can be either a record or an id
130
+ siblings_of(node) Siblings of node, node can be either a record or an id
131
+
132
+ Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
133
+
134
+ node.children.create
135
+ node.siblings.create
136
+ TestNode.children_of(node_id).build
137
+ TestNode.siblings_of(node_id).create
138
+
139
+ ## Selecting nodes by depth
140
+
141
+ When depth caching is enabled (see `has_ancestry` options), five more named scopes can be used to select nodes on their depth:
142
+
143
+ before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
144
+ to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
145
+ at_depth(depth) Return nodes that are at depth (node.depth == depth)
146
+ from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
147
+ after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
148
+
149
+ 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:
150
+
151
+ node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
152
+ node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
153
+ node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
154
+ node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
155
+ node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
156
+ node.path(:from_depth => -2) The node's grandparent, parent and the node itself
157
+
158
+ node.ancestors(:from_depth => -6, :to_depth => -4)
159
+ node.path.from_depth(3).to_depth(4)
160
+ node.descendants(:from_depth => 2, :to_depth => 4)
161
+ node.subtree.from_depth(10).to_depth(12)
162
+
163
+ Please note that depth constraints cannot be passed to `ancestor_ids` and `path_ids`. The reason for this is that
164
+ both these relations can be fetched directly from the ancestry column without performing a database query. It would
165
+ require an entirely different method of applying the depth constraints which isn't worth the effort of implementing.
166
+ You can use `ancestors(depth_options).map(&:id)` or `ancestor_ids.slice(min_depth..max_depth)` instead.
167
+
168
+ ## STI support
169
+
170
+ 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.
171
+
172
+ ## Arrangement
173
+
174
+ Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
175
+
176
+ { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
177
+ => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
178
+ => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
179
+ => {}
180
+ }
181
+ }
182
+ }
183
+
184
+ The arrange method also works on a scoped class, for example:
185
+
186
+ TreeNode.where(:name => 'Crunchy').first.subtree.arrange
187
+
188
+ 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:
189
+
190
+ TreeNode.where(:name => 'Crunchy').subtree.arrange(:order => [:name, :asc])
191
+
192
+ ## Migrating from plugin that uses parent_id column
193
+
194
+ With Mongoid-ancestry its easy to migrate from any of these plugins, to do so, use the `Model.build_ancestry_from_parent_ids!` method on your model. These steps provide a more detailed explanation:
195
+
196
+ 1. Remove old tree plugin or gem and add in Mongoid-ancestry
197
+ * See 'Installation' for more info on installing and configuring gem
198
+ * Add to app/models/model.rb:
199
+
200
+ include Mongoid::Ancestry
201
+ has_ancestry
202
+
203
+ * Create indexes
204
+
205
+ 2. Change your code
206
+ Most tree calls will probably work fine with ancestry
207
+ Others must be changed or proxied
208
+ Check if all your data is intact and all tests pass
209
+
210
+ 3. Drop `parent_id` field
211
+
212
+ ## Integrity checking and restoration
213
+
214
+ 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.
215
+
216
+ 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!`.
217
+
218
+ For example, from IRB:
219
+
220
+ >> stinky = TreeNode.create :name => 'Stinky'
221
+ $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
222
+ >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
223
+ $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
224
+ >> stinky.update_attribute :parent, squeeky
225
+ $ true
226
+ >> TreeNode.all
227
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
228
+ >> TreeNode.check_ancestry_integrity!
229
+ !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
230
+ >> TreeNode.restore_ancestry_integrity!
231
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
232
+
233
+ Additionally, if you think something is wrong with your depth cache:
234
+
235
+ >> TreeNode.rebuild_depth_cache!
236
+
237
+ ## Tests
238
+
239
+ 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`.
240
+
241
+ ## Internals
242
+
243
+ 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.
244
+
245
+ 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.
246
+
247
+ ## Contact and copyright
248
+
249
+ It's a fork of [original ancestry](https://github.com/stefankroes/ancestry) gem but adopted to work with Mongoid.
250
+
251
+ All thanks should goes to Stefan Kroes for his great work.
252
+
253
+ Bug report? Faulty/incomplete documentation? Feature request? Please post an issue on [issues tracker](http://github.com/skyeagle/mongoid-ancestry/issues).
254
+
255
+ Copyright (c) 2009 Stefan Kroes, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rspec/core'
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec) do |spec|
8
+ spec.pattern = FileList['spec/**/*_spec.rb']
9
+ end
10
+
11
+ task :default => :spec
12
+
13
+ require 'rdoc/task'
14
+ Rake::RDocTask.new do |rdoc|
15
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
16
+
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = "mongoid-ancestry #{version}"
19
+ rdoc.rdoc_files.include('README*')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
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,212 @@
1
+ module Mongoid
2
+ module Ancestry
3
+ module ClassMethods
4
+ def has_ancestry(opts = {})
5
+ defaults = {
6
+ :ancestry_field => :ancestry,
7
+ :cache_depth => false,
8
+ :depth_cache_field => :ancestry_depth,
9
+ :orphan_strategy => :destroy
10
+ }
11
+
12
+ valid_opts = [:ancestry_field, :cache_depth, :depth_cache_field, :orphan_strategy]
13
+ unless opts.is_a?(Hash) && opts.keys.all? {|opt| valid_opts.include?(opt) }
14
+ raise Error.new("Invalid options for has_ancestry. Only hash is allowed.\n Defaults: #{defaults.inspect}")
15
+ end
16
+
17
+ opts.symbolize_keys!
18
+
19
+ opts.reverse_merge!(defaults)
20
+
21
+ # Create ancestry field accessor and set to option or default
22
+ cattr_accessor :ancestry_field
23
+ self.ancestry_field = opts[:ancestry_field]
24
+
25
+ self.field ancestry_field, :type => String
26
+ self.index ancestry_field
27
+
28
+ # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
29
+ cattr_reader :orphan_strategy
30
+ self.orphan_strategy = opts[:orphan_strategy]
31
+
32
+ # Validate format of ancestry column value
33
+ primary_key_format = opts[:primary_key_format] || /[a-z0-9]+/
34
+ validates_format_of ancestry_field, :with => /\A#{primary_key_format.source}(\/#{primary_key_format.source})*\Z/, :allow_nil => true
35
+
36
+ # Validate that the ancestor ids don't include own id
37
+ validate :ancestry_exclude_self
38
+
39
+ # Create ancestry column accessor and set to option or default
40
+ if opts[:cache_depth]
41
+ # Create accessor for column name and set to option or default
42
+ self.cattr_accessor :depth_cache_field
43
+ self.depth_cache_field = opts[:depth_cache_field]
44
+
45
+ # Cache depth in depth cache column before save
46
+ before_validation :cache_depth
47
+
48
+ # Validate depth column
49
+ validates_numericality_of depth_cache_field, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
50
+ end
51
+
52
+ # Create named scopes for depth
53
+ {:before_depth => 'lt', :to_depth => 'lte', :at_depth => nil, :from_depth => 'gte', :after_depth => 'gt'}.each do |scope_name, operator|
54
+ scope scope_name, lambda { |depth|
55
+ raise Error.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless opts[:cache_depth]
56
+ where( (operator ? depth_cache_field.send(operator.to_sym) : depth_cache_field) => depth)
57
+ }
58
+ end
59
+
60
+ scope :roots, where(ancestry_field => nil)
61
+ scope :ancestors_of, lambda { |object| where(to_node(object).ancestor_conditions) }
62
+ scope :children_of, lambda { |object| where(to_node(object).child_conditions) }
63
+ scope :descendants_of, lambda { |object| any_of(to_node(object).descendant_conditions) }
64
+ scope :subtree_of, lambda { |object| any_of(to_node(object).subtree_conditions) }
65
+ scope :siblings_of, lambda { |object| where(to_node(object).sibling_conditions) }
66
+ scope :ordered_by_ancestry, asc(:"#{self.base_class.ancestry_field}")
67
+ scope :ordered_by_ancestry_and, lambda {|by| ordered_by_ancestry.order_by([by]) }
68
+
69
+ # Update descendants with new ancestry before save
70
+ before_save :update_descendants_with_new_ancestry
71
+
72
+ # Apply orphan strategy before destroy
73
+ before_destroy :apply_orphan_strategy
74
+ end
75
+
76
+ # Fetch tree node if necessary
77
+ def to_node object
78
+ object.is_a?(self.base_class) ? object : find(object)
79
+ end
80
+
81
+ # Scope on relative depth options
82
+ def scope_depth depth_options, depth
83
+ depth_options.inject(self.base_class) do |scope, option|
84
+ scope_name, relative_depth = option
85
+ if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
86
+ scope.send scope_name, depth + relative_depth
87
+ else
88
+ raise Error.new("Unknown depth option: #{scope_name}.")
89
+ end
90
+ end
91
+ end
92
+
93
+ # Orphan strategy writer
94
+ def orphan_strategy= orphan_strategy
95
+ # Check value of orphan strategy, only rootify, restrict or destroy is allowed
96
+ if [:rootify, :restrict, :destroy].include? orphan_strategy
97
+ class_variable_set :@@orphan_strategy, orphan_strategy
98
+ else
99
+ raise Error.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
100
+ end
101
+ end
102
+
103
+ # Arrangement
104
+ def arrange options = {}
105
+ scope =
106
+ if options[:order].nil?
107
+ self.base_class.ordered_by_ancestry
108
+ else
109
+ self.base_class.ordered_by_ancestry_and options.delete(:order)
110
+ end
111
+ # Get all nodes ordered by ancestry and start sorting them into an empty hash
112
+ scope.all(options).inject(ActiveSupport::OrderedHash.new) do |arranged_nodes, node|
113
+ # Find the insertion point for that node by going through its ancestors
114
+ node.ancestor_ids.inject(arranged_nodes) do |insertion_point, ancestor_id|
115
+ insertion_point.each do |parent, children|
116
+ # Change the insertion point to children if node is a descendant of this parent
117
+ insertion_point = children if ancestor_id == parent.id
118
+ end; insertion_point
119
+ end[node] = ActiveSupport::OrderedHash.new; arranged_nodes
120
+ end
121
+ end
122
+
123
+ # Integrity checking
124
+ def check_ancestry_integrity! options = {}
125
+ parents = {}
126
+ exceptions = [] if options[:report] == :list
127
+ # For each node ...
128
+ self.base_class.all.each do |node|
129
+ begin
130
+ # ... check validity of ancestry column
131
+ if !node.valid? and !node.errors[node.class.ancestry_field].blank?
132
+ raise IntegrityError.new "Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_field}."
133
+ end
134
+ # ... check that all ancestors exist
135
+ node.ancestor_ids.each do |ancestor_id|
136
+ unless where(:_id => ancestor_id).first
137
+ raise IntegrityError.new "Reference to non-existent node in node #{node.id}: #{ancestor_id}."
138
+ end
139
+ end
140
+ # ... check that all node parents are consistent with values observed earlier
141
+ node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
142
+ parents[node_id] = parent_id unless parents.has_key? node_id
143
+ unless parents[node_id] == parent_id
144
+ raise IntegrityError.new "Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}"
145
+ end
146
+ end
147
+ rescue IntegrityError => integrity_exception
148
+ case options[:report]
149
+ when :list then exceptions << integrity_exception
150
+ when :echo then puts integrity_exception
151
+ else raise integrity_exception
152
+ end
153
+ end
154
+ end
155
+ exceptions if options[:report] == :list
156
+ end
157
+
158
+ # Integrity restoration
159
+ def restore_ancestry_integrity!
160
+ parents = {}
161
+ # For each node ...
162
+ self.base_class.all.each do |node|
163
+ # ... set its ancestry to nil if invalid
164
+ if node.errors[node.class.ancestry_field].blank?
165
+ node.without_ancestry_callbacks do
166
+ node.update_attribute node.ancestry_field, nil
167
+ end
168
+ end
169
+ # ... save parent of this node in parents array if it exists
170
+ parents[node.id] = node.parent_id if exists? node.parent_id
171
+
172
+ # Reset parent id in array to nil if it introduces a cycle
173
+ parent = parents[node.id]
174
+ until parent.nil? || parent == node.id
175
+ parent = parents[parent]
176
+ end
177
+ parents[node.id] = nil if parent == node.id
178
+ end
179
+ # For each node ...
180
+ self.base_class.all.each do |node|
181
+ # ... rebuild ancestry from parents array
182
+ ancestry, parent = nil, parents[node.id]
183
+ until parent.nil?
184
+ ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
185
+ end
186
+ node.without_ancestry_callbacks do
187
+ node.update_attribute node.ancestry_field, ancestry
188
+ end
189
+ end
190
+ end
191
+
192
+ # Build ancestry from parent id's for migration purposes
193
+ def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
194
+ self.base_class.where(:parent_id => parent_id).all.each do |node|
195
+ node.without_ancestry_callbacks do
196
+ node.update_attribute(self.base_class.ancestry_field, ancestry)
197
+ end
198
+ build_ancestry_from_parent_ids! node.id,
199
+ if ancestry.nil? then node.id.to_s else "#{ancestry}/#{node.id}" end
200
+ end
201
+ end
202
+
203
+ # Rebuild depth cache if it got corrupted or if depth caching was just turned on
204
+ def rebuild_depth_cache!
205
+ raise Error.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_field
206
+ self.base_class.all.each do |node|
207
+ node.update_attribute depth_cache_field, node.depth
208
+ end
209
+ end
210
+ end
211
+ end
212
+ 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