atd-ancestry 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,295 @@
1
+ = Ancestry
2
+
3
+ Ancestry is a gem/plugin that allows the records of a Ruby on Rails ActiveRecord 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 sql query. Additional features are STI support, named_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.
4
+
5
+ = Installation
6
+
7
+ To apply Ancestry to any ActiveRecord model, follow these simple steps:
8
+
9
+ 1. Install gem
10
+ - Install gemcutter gem: <b>sudo gem install gemcutter</b> (maybe you need: gem update --system)
11
+ - Add gemcutter.org as default gem source: <b>gem tumble</b>
12
+ - Add to config/environment.rb: <b>config.gem 'ancestry'</b>
13
+ - Install required gems: <b>sudo rake gems:install</b>
14
+ - Alternatively: sudo gem install ancestry
15
+ - If you don't want gemcutter: config.gem 'ancestry', :source => 'gemcutter.org'
16
+ - Alternatively: sudo gem install ancestry --source gemcutter.org
17
+
18
+ 2. Add ancestry column to your table
19
+ - Create migration: <b>./script/generate migration add_ancestry_to_[table] ancestry:string</b>
20
+ - Add index to migration: <b>add_index [table], :ancestry</b> (UP) / <b>remove_index [table], :ancestry</b> (DOWN)
21
+ - Migrate your database: <b>rake db:migrate</b>
22
+
23
+ 3. Add ancestry to your model
24
+ - Add to app/models/[model].rb: <b>has_ancestry</b>
25
+
26
+ Your model is now a tree!
27
+
28
+ = Using acts_as_tree instead of has_ancestry
29
+
30
+ In version 1.2.0 the <b>acts_as_tree</b> method was <b>renamed to has_ancestry</b> in order to allow usage of both the acts_as_tree gem and the ancestry gem in a single application. To not break backwards compatibility, the has_ancestry method is aliased with acts_as_tree if ActiveRecord::Base does not respond to acts_as_tree. acts_as_tree will continue to be supported in the future as I personally prefer it.
31
+
32
+ = Organising records into a tree
33
+
34
+ You can use the parent attribute to organise your records into a tree. If you have the id of the record you want to use as a parent and don't want to fetch it, you can also use parent_id. Like any virtual model attributes, parent and parent_id can be set using parent= and parent_id= on a record or by including them in the hash passed to new, create, create!, update_attributes and update_attributes!. For example:
35
+
36
+ TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
37
+
38
+ You can also create children through the children relation on a node:
39
+
40
+ node.children.create :name => 'Stinky'
41
+
42
+ = Navigating your tree
43
+
44
+ To navigate an Ancestry model, use the following methods on any instance / record:
45
+
46
+ parent Returns the parent of the record, nil for a root node
47
+ parent_id Returns the id of the parent of the record, nil for a root node
48
+ root Returns the root of the tree the record is in, self for a root node
49
+ root_id Returns the id of the root of the tree the record is in
50
+ is_root? Returns true if the record is a root node, false otherwise
51
+ ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
52
+ ancestors Scopes the model on ancestors of the record
53
+ path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
54
+ path Scopes model on path records of the record
55
+ children Scopes the model on children of the record
56
+ child_ids Returns a list of child ids
57
+ has_children? Returns true if the record has any children, false otherwise
58
+ is_childless? Returns true is the record has no childen, false otherwise
59
+ siblings Scopes the model on siblings of the record, the record itself is included
60
+ sibling_ids Returns a list of sibling ids
61
+ has_siblings? Returns true if the record's parent has more than one child
62
+ is_only_child? Returns true if the record is the only child of its parent
63
+ descendants Scopes the model on direct and indirect children of the record
64
+ descendant_ids Returns a list of a descendant ids
65
+ subtree Scopes the model on descendants and itself
66
+ subtree_ids Returns a list of all ids in the record's subtree
67
+ depth Return the depth of the node, root nodes are at depth 0
68
+
69
+ = Options for has_ancestry
70
+
71
+ The has_ancestry methods supports the following options:
72
+
73
+ :ancestry_column Pass in a symbol to store ancestry in a different column
74
+ :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
75
+ :destroy All children are destroyed as well (default)
76
+ :rootify The children of the destroyed node become root nodes
77
+ :restrict An AncestryException is raised if any children exist
78
+ :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
79
+ If you turn depth_caching on for an existing model:
80
+ - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
81
+ - Build cache: TreeNode.rebuild_depth_cache!
82
+ :depth_cache_column Pass in a symbol to store depth cache in a different column
83
+
84
+ = (Named) Scopes
85
+
86
+ 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:
87
+
88
+ node.children.exists?(:name => 'Mary')
89
+ node.subtree.all(:order => :name, :limit => 10).each do; ...; end
90
+ node.descendants.count
91
+
92
+ For convenience, a couple of named scopes are included at the class level:
93
+
94
+ roots Root nodes
95
+ ancestors_of(node) Ancestors of node, node can be either a record or an id
96
+ children_of(node) Children of node, node can be either a record or an id
97
+ descendants_of(node) Descendants of node, node can be either a record or an id
98
+ subtree_of(node) Subtree of node, node can be either a record or an id
99
+ siblings_of(node) Siblings of node, node can be either a record or an id
100
+
101
+ Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
102
+
103
+ node.children.create
104
+ node.siblings.create!
105
+ TestNode.children_of(node_id).new
106
+ TestNode.siblings_of(node_id).create
107
+
108
+ = Selecting nodes by depth
109
+
110
+ When depth caching is enabled (see has_ancestry options), five more named scopes can be used to select nodes on their depth:
111
+
112
+ before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
113
+ to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
114
+ at_depth(depth) Return nodes that are at depth (node.depth == depth)
115
+ from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
116
+ after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
117
+
118
+ 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:
119
+
120
+ node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
121
+ node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
122
+ node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
123
+ node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
124
+ node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
125
+ node.path(:from_depth => -2) The node's grandparent, parent and the node itself
126
+
127
+ node.ancestors(:from_depth => -6, :to_depth => -4)
128
+ node.path.from_depth(3).to_depth(4)
129
+ node.descendants(:from_depth => 2, :to_depth => 4)
130
+ node.subtree.from_depth(10).to_depth(12)
131
+
132
+ 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.
133
+
134
+ = STI support
135
+
136
+ 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.
137
+
138
+ = Arrangement
139
+
140
+ Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
141
+
142
+ { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
143
+ => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
144
+ => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
145
+ => {}
146
+ }
147
+ }
148
+ }
149
+
150
+ The arrange method also works on a scoped class, for example:
151
+
152
+ TreeNode.find_by_name('Crunchy').subtree.arrange
153
+
154
+ The arrange method takes ActiveRecord 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:
155
+
156
+ TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
157
+
158
+ = Migrating from plugin that uses parent_id column
159
+
160
+ Most current tree plugins use a parent_id column (has_ancestry, awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry its easy to migrate from any of these plugins, to do so, use the build_ancestry_from_parent_ids! method on your ancestry model. These steps provide a more detailed explanation:
161
+
162
+ 1. Add ancestry column to your table
163
+ - Create migration: <b>./script/generate migration add_ancestry_to_[table] ancestry:string</b>
164
+ - Add index to migration: <b>add_index [table], :ancestry</b> (UP) / <b>remove_index [table], :ancestry</b> (DOWN)
165
+ - Migrate your database: <b>rake db:migrate</b>
166
+
167
+ 2. Remove old tree plugin or gem and add in Ancestry
168
+ - Remove plugin: rm -Rf vendor/plugins/[old plugin]
169
+ - Remove gem config line from environment.rb: config.gem [old gem]
170
+ - Add Ancestry to environment.rb: config.gem :ancestry
171
+ - See 'Installation' for more info on installing and configuring gems
172
+
173
+ 3. Change your model
174
+ - Remove any macros required by old plugin/gem from app/models/[model].rb
175
+ - Add to app/models/[model].rb: <b>has_ancestry</b>
176
+
177
+ 4. Generate ancestry columns
178
+ - In './script.console': <b>[model].build_ancestry_from_parent_ids!</b>
179
+ - Make sure it worked ok: <b>[model].check_ancestry_integrity!</b>
180
+
181
+ 5. Change your code
182
+ - Most tree calls will probably work fine with ancestry
183
+ - Others must be changed or proxied
184
+ - Check if all your data is intact and all tests pass
185
+
186
+ 6. Drop parent_id column:
187
+ - Create migration: <b>./script/generate migration remove_parent_id_from_[table]</b>
188
+ - Add to migration: <b>remove_column [table], :parent_id</b> (UP) / <b>add_column [table], :parent_id, :integer</b> (DOWN)
189
+ - Migrate your database: <b>rake db:migrate</b>
190
+
191
+ = Integrity checking and restoration
192
+
193
+ I don't see any way 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.
194
+
195
+ Ancestry includes some methods for detecting integrity problems and restoring integrity just to be sure. To check integrity use: [Model].check_ancestry_integrity!. An AncestryIntegrityException will be raised if there are any problems. To restore integrity use: [Model].restore_ancestry_integrity!.
196
+
197
+ For example, from IRB:
198
+
199
+ >> stinky = TreeNode.create :name => 'Stinky'
200
+ $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
201
+ >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
202
+ $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
203
+ >> stinky.update_attribute :parent, squeeky
204
+ $ true
205
+ >> TreeNode.all
206
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
207
+ >> TreeNode.check_ancestry_integrity!
208
+ !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
209
+ >> TreeNode.restore_ancestry_integrity!
210
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
211
+
212
+ Additionally, if you think something is wrong with your depth cache:
213
+
214
+ >> TreeNode.rebuild_depth_cache!
215
+
216
+ = Tests
217
+
218
+ The Ancestry gem comes with a unit test suite consisting of about 1800 assertions in about 30 tests. It takes about 10 seconds to run on sqlite. To run it yourself, install Ancestry as a plugin into a Rails application, go to the ancestry folder and type 'rake'. The test suite is located in 'test/has_ancestry_test.rb'.
219
+
220
+ = Internals
221
+
222
+ As can be seen in the previous section, 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 Ancestry to fetch any relation (siblings, descendants, etc.) in a single sql 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.
223
+
224
+ In the example above, the ancestry column is created as a string. This puts a limitation on the depth of the tree of about 40 or 50 levels, which I think may be enough for most users. To increase the maximum depth of the tree, increase the size of the string that is being used or change it to a text to remove the limitation entirely. Changing it to a text will however decrease performance because an index cannot be put on the column in that case.
225
+
226
+ The materialised path pattern requires Ancestry to use a 'like' 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.
227
+
228
+ = Version history
229
+
230
+ The latest and recommended version of ancestry is 1.2.0. The three numbers of each version numbers are respectively the major, minor and patch versions. We started with major version 1 because it looks so much better and ancestry was already quite mature and complete when it was published. The major version is only bumped when backwards compatibility is broken. The minor version is bumped when new features are added. The patch version is bumped when bugs are fixed.
231
+
232
+ - Version 1.2.0 (2009-11-07)
233
+ - Removed some duplication in has_ancestry
234
+ - Cleaned up plugin pattern according to http://yehudakatz.com/2009/11/12/better-ruby-idioms/
235
+ - Moved parts of ancestry into seperate files
236
+ - Made it possible to pass options into the arrange method
237
+ - Renamed acts_as_tree to has_ancestry
238
+ - Aliased has_ancestry as acts_as_tree if acts_as_tree is available
239
+ - Added subtree_of scope
240
+ - Updated ordered_by_ancestry scope to support Microsoft SQL Server
241
+ - Added empty hash as parameter to exists? calls for older ActiveRecord versions
242
+ - Version 1.1.4 (2009-11-07)
243
+ - Thanks to a patch from tom taylor, Ancestry now works with different primary keys
244
+ - Version 1.1.3 (2009-11-01)
245
+ - Fixed a pretty bad bug where several operations took far too many queries
246
+ - Version 1.1.2 (2009-10-29)
247
+ - Added validation for depth cache column
248
+ - Added STI support (reported broken)
249
+ - Version 1.1.1 (2009-10-28)
250
+ - Fixed some parentheses warnings that where reported
251
+ - Fixed a reported issue with arrangement
252
+ - Fixed issues with ancestors and path order on postgres
253
+ - Added ordered_by_ancestry scope (needed to fix issues)
254
+ - Version 1.1.0 (2009-10-22)
255
+ - Depth caching (and cache rebuilding)
256
+ - Depth method for nodes
257
+ - Named scopes for selecting by depth
258
+ - Relative depth options for tree navigation methods:
259
+ - ancestors
260
+ - path
261
+ - descendants
262
+ - descendant_ids
263
+ - subtree
264
+ - subtree_ids
265
+ - Updated README
266
+ - Easy migration from existing plugins/gems
267
+ - acts_as_tree checks unknown options
268
+ - acts_as_tree checks that options are hash
269
+ - Added a bang (!) to the integrity functions
270
+ - Since these functions should only be used from ./script/console and not from your application, this change is not considered as breaking backwards compatibility and the major version wasn't bumped.
271
+ - Updated install script to point to documentation
272
+ - Removed rails specific init
273
+ - Removed uninstall script
274
+ - Version 1.0.0 (2009-10-16)
275
+ - Initial version
276
+ - Tree building
277
+ - Tree navigation
278
+ - Integrity checking / restoration
279
+ - Arrangement
280
+ - Orphan strategies
281
+ - Subtree movement
282
+ - Named scopes
283
+ - Validations
284
+
285
+ = Future work
286
+
287
+ I will try to keep Ancestry up to date with changing versions of Rails and Ruby and also with any bug reports I might receive. I will implement new features on request as I see fit. One thing I definitely want to do soon is some proper performance testing.
288
+
289
+ = Contact and copyright
290
+
291
+ Bug report? Faulty/incomplete documentation? Feature request? Please post an issue on 'http://github.com/stefankroes/ancestry/issues'. Please also contact me at s.a.kroes[at]gmail.com if it's urgent.
292
+
293
+ Question? Contact me at s.a.kroes[at]gmail.com, make sure you read the documentation. You can also join the #ancestry channel on IRC (irc.freenode.net).
294
+
295
+ Copyright (c) 2009 Stefan Kroes, released under the MIT license
@@ -0,0 +1,40 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+
5
+ require 'rake/testtask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test the ancestry plugin.'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = true
15
+ end
16
+
17
+ desc 'Generate documentation for the ancestry plugin.'
18
+ Rake::RDocTask.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = 'doc'
20
+ rdoc.title = 'Ancestry'
21
+ rdoc.options << '--line-numbers' << '--inline-source'
22
+ rdoc.rdoc_files.include('README.rdoc')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
25
+
26
+ spec = Gem::Specification.new do |s|
27
+ s.name = 'atd-ancestry'
28
+ s.summary = 'Organise ActiveRecord model into a tree structure'
29
+ s.description = 'Ancestry allows the records of a ActiveRecord model to be organised in a tree structure, using a single, intuitively formatted database column. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single sql query. Additional features are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.'
30
+ s.files = FileList["[A-Z]*", "lib/**/*"]
31
+ s.version = "1.3.0"
32
+ end
33
+
34
+ Rake::GemPackageTask.new(spec) do |pkg|
35
+ end
36
+
37
+ desc "Install the gem #{spec.name}-#{spec.version}.gem"
38
+ task :install do
39
+ system("gem install pkg/#{spec.name}-#{spec.version}.gem --no-ri --no-rdoc")
40
+ end
@@ -0,0 +1 @@
1
+ require 'ancestry/has_ancestry'
@@ -0,0 +1,127 @@
1
+ module Ancestry
2
+ module ClassMethods
3
+ # Fetch tree node if necessary
4
+ def to_node object
5
+ if object.is_a?(self.base_class) then object else find(object) end
6
+ end
7
+
8
+ # Scope on relative depth options
9
+ def scope_depth depth_options, depth
10
+ depth_options.inject(self.base_class) do |scope, option|
11
+ scope_name, relative_depth = option
12
+ if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
13
+ scope.send scope_name, depth + relative_depth
14
+ else
15
+ raise Ancestry::AncestryException.new("Unknown depth option: #{scope_name}.")
16
+ end
17
+ end
18
+ end
19
+
20
+ # Orphan strategy writer
21
+ def orphan_strategy= orphan_strategy
22
+ # Check value of orphan strategy, only rootify, restrict or destroy is allowed
23
+ if [:rootify, :restrict, :destroy].include? orphan_strategy
24
+ class_variable_set :@@orphan_strategy, orphan_strategy
25
+ else
26
+ raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
27
+ end
28
+ end
29
+
30
+ # Arrangement
31
+ def arrange options = {}
32
+ scope =
33
+ if options[:order].nil?
34
+ self.base_class.ordered_by_ancestry
35
+ else
36
+ self.base_class.ordered_by_ancestry_and options.delete(:order)
37
+ end
38
+ # Get all nodes ordered by ancestry and start sorting them into an empty hash
39
+ scope.all(options).inject({}) do |arranged_nodes, node|
40
+ # Find the insertion point for that node by going through its ancestors
41
+ node.ancestor_ids.inject(arranged_nodes) do |insertion_point, ancestor_id|
42
+ insertion_point.each do |parent, children|
43
+ # Change the insertion point to children if node is a descendant of this parent
44
+ insertion_point = children if ancestor_id == parent.id
45
+ end; insertion_point
46
+ end[node] = {}; arranged_nodes
47
+ end
48
+ end
49
+
50
+ # Integrity checking
51
+ def check_ancestry_integrity!
52
+ parents = {}
53
+ # For each node ...
54
+ self.base_class.all.each do |node|
55
+ # ... check validity of ancestry column
56
+ if !node.valid? and node.errors[node.class.ancestry_column].any?
57
+ raise Ancestry::AncestryIntegrityException.new("Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}.")
58
+ end
59
+ # ... check that all ancestors exist
60
+ node.ancestor_ids.each do |ancestor_id|
61
+ unless exists? ancestor_id
62
+ raise Ancestry::AncestryIntegrityException.new("Reference to non-existent node in node #{node.id}: #{ancestor_id}.")
63
+ end
64
+ end
65
+ # ... check that all node parents are consistent with values observed earlier
66
+ node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
67
+ parents[node_id] = parent_id unless parents.has_key? node_id
68
+ unless parents[node_id] == parent_id
69
+ raise Ancestry::AncestryIntegrityException.new("Conflicting parent id in node #{node.id}: #{parent_id || 'nil'} for node #{node_id}, expecting #{parents[node_id] || 'nil'}")
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Integrity restoration
76
+ def restore_ancestry_integrity!
77
+ parents = {}
78
+ # For each node ...
79
+ self.base_class.all.each do |node|
80
+ # ... set its ancestry to nil if invalid
81
+ if node.errors[node.class.ancestry_column].any?
82
+ node.without_ancestry_callbacks do
83
+ node.update_attribute(node.class.ancestry_column, nil)
84
+ end
85
+ end
86
+ # ... save parent of this node in parents array if it exists
87
+ parents[node.id] = node.parent_id if exists? node.parent_id
88
+
89
+ # Reset parent id in array to nil if it introduces a cycle
90
+ parent = parents[node.id]
91
+ until parent.nil? || parent == node.id
92
+ parent = parents[parent]
93
+ end
94
+ parents[node.id] = nil if parent == node.id
95
+ end
96
+ # For each node ...
97
+ self.base_class.all.each do |node|
98
+ # ... rebuild ancestry from parents array
99
+ ancestry, parent = nil, parents[node.id]
100
+ until parent.nil?
101
+ ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
102
+ end
103
+ node.without_ancestry_callbacks do
104
+ node.update_attribute(node.class.ancestry_column, ancestry)
105
+ end
106
+ end
107
+ end
108
+
109
+ # Build ancestry from parent id's for migration purposes
110
+ def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
111
+ self.base_class.all(:conditions => {:parent_id => parent_id}).each do |node|
112
+ node.without_ancestry_callbacks do
113
+ node.update_attribute ancestry_column, ancestry
114
+ end
115
+ build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
116
+ end
117
+ end
118
+
119
+ # Rebuild depth cache if it got corrupted or if depth caching was just turned on
120
+ def rebuild_depth_cache!
121
+ raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
122
+ self.base_class.all.each do |node|
123
+ node.update_attribute depth_cache_column, node.depth
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,7 @@
1
+ module Ancestry
2
+ class AncestryException < RuntimeError
3
+ end
4
+
5
+ class AncestryIntegrityException < AncestryException
6
+ end
7
+ end
@@ -0,0 +1,81 @@
1
+ require 'ancestry/class_methods'
2
+ require 'ancestry/instance_methods'
3
+ require 'ancestry/exceptions'
4
+
5
+ class << ActiveRecord::Base
6
+ def has_ancestry options = {}
7
+ # Check options
8
+ raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
9
+ options.each do |key, value|
10
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column].include? key
11
+ raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
12
+ end
13
+ end
14
+
15
+ # Include instance methods
16
+ include Ancestry::InstanceMethods
17
+
18
+ # Include dynamic class methods
19
+ extend Ancestry::ClassMethods
20
+
21
+ # Create ancestry column accessor and set to option or default
22
+ cattr_accessor :ancestry_column
23
+ self.ancestry_column = options[:ancestry_column] || :ancestry
24
+
25
+ # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
26
+ cattr_reader :orphan_strategy
27
+ self.orphan_strategy = options[:orphan_strategy] || :destroy
28
+
29
+ # Save self as base class (for STI)
30
+ cattr_accessor :base_class
31
+ self.base_class = self
32
+
33
+ # Validate format of ancestry column value
34
+ validates_format_of ancestry_column, :with => /\A[0-9]+(\/[0-9]+)*\Z/, :allow_nil => true
35
+
36
+ # Validate that the ancestor ids don't include own id
37
+ validate :ancestry_exclude_self
38
+
39
+ # Scopes
40
+ scope :roots, :conditions => {ancestry_column => nil}
41
+ scope :ancestors_of, lambda { |object| {:conditions => to_node(object).ancestor_conditions} }
42
+ scope :children_of, lambda { |object| {:conditions => to_node(object).child_conditions} }
43
+ scope :descendants_of, lambda { |object| {:conditions => to_node(object).descendant_conditions} }
44
+ scope :subtree_of, lambda { |object| {:conditions => to_node(object).subtree_conditions} }
45
+ scope :siblings_of, lambda { |object| {:conditions => to_node(object).sibling_conditions} }
46
+ scope :ordered_by_ancestry, :order => "(case when #{ancestry_column} is null then 0 else 1 end), #{ancestry_column}"
47
+ scope :ordered_by_ancestry_and, lambda { |order| {:order => "(case when #{ancestry_column} is null then 0 else 1 end), #{ancestry_column}, #{order}"} }
48
+
49
+ # Update descendants with new ancestry before save
50
+ before_save :update_descendants_with_new_ancestry
51
+
52
+ # Apply orphan strategy before destroy
53
+ before_destroy :apply_orphan_strategy
54
+
55
+ # Create ancestry column accessor and set to option or default
56
+ if options[:cache_depth]
57
+ # Create accessor for column name and set to option or default
58
+ self.cattr_accessor :depth_cache_column
59
+ self.depth_cache_column = options[:depth_cache_column] || :ancestry_depth
60
+
61
+ # Cache depth in depth cache column before save
62
+ before_validation :cache_depth
63
+
64
+ # Validate depth column
65
+ validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
66
+ end
67
+
68
+ # Create named scopes for depth
69
+ {:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
70
+ scope scope_name, lambda { |depth|
71
+ raise Ancestry::AncestryException.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless options[:cache_depth]
72
+ {:conditions => ["#{depth_cache_column} #{operator} ?", depth]}
73
+ }
74
+ end
75
+ end
76
+
77
+ # Alias has_ancestry with acts_as_tree, if it's available.
78
+ if !respond_to?(:acts_as_tree)
79
+ alias_method :acts_as_tree, :has_ancestry
80
+ end
81
+ end
@@ -0,0 +1,208 @@
1
+ module Ancestry
2
+ module InstanceMethods
3
+ # Validate that the ancestors don't include itself
4
+ def ancestry_exclude_self
5
+ errors[:base] << "#{self.class.name.humanize} cannot be a descendant of itself." if ancestor_ids.include? self.id
6
+ end
7
+
8
+ # Update descendants with new ancestry
9
+ def update_descendants_with_new_ancestry
10
+ # Skip this if callbacks are disabled
11
+ unless ancestry_callbacks_disabled?
12
+ # If node is valid, not a new record and ancestry was updated ...
13
+ if changed.include?(self.class.ancestry_column.to_s) && !new_record? && valid?
14
+ # ... for each descendant ...
15
+ descendants.each do |descendant|
16
+ # ... replace old ancestry with new ancestry
17
+ descendant.without_ancestry_callbacks do
18
+ descendant.update_attribute(self.class.ancestry_column,
19
+ descendant.read_attribute(descendant.class.ancestry_column).gsub(
20
+ /^#{self.child_ancestry}/,
21
+ if read_attribute(self.class.ancestry_column).blank? then id.to_s else "#{read_attribute self.class.ancestry_column }/#{id}" end
22
+ )
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # Apply orphan strategy
31
+ def apply_orphan_strategy
32
+ # Skip this if callbacks are disabled
33
+ unless ancestry_callbacks_disabled?
34
+ # If this isn't a new record ...
35
+ unless new_record?
36
+ # ... make al children root if orphan strategy is rootify
37
+ if self.base_class.orphan_strategy == :rootify
38
+ descendants.each do |descendant|
39
+ descendant.without_ancestry_callbacks do
40
+ descendant.update_attribute(descendant.class.ancestry_column, (if descendant.ancestry == child_ancestry then nil else descendant.ancestry.gsub(/^#{child_ancestry}\//, '') end))
41
+ end
42
+ end
43
+ # ... destroy all descendants if orphan strategy is destroy
44
+ elsif self.base_class.orphan_strategy == :destroy
45
+ descendants.all.each do |descendant|
46
+ descendant.without_ancestry_callbacks do
47
+ descendant.destroy
48
+ end
49
+ end
50
+ # ... throw an exception if it has children and orphan strategy is restrict
51
+ elsif self.base_class.orphan_strategy == :restrict
52
+ raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # The ancestry value for this record's children
59
+ def child_ancestry
60
+ # New records cannot have children
61
+ raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
62
+
63
+ if self.send("#{self.base_class.ancestry_column}_was").blank? then id.to_s else "#{self.send "#{self.base_class.ancestry_column}_was"}/#{id}" end
64
+ end
65
+
66
+ # Ancestors
67
+ def ancestor_ids
68
+ read_attribute(self.base_class.ancestry_column).to_s.split('/').map(&:to_i)
69
+ end
70
+
71
+ def ancestor_conditions
72
+ {self.base_class.primary_key => ancestor_ids}
73
+ end
74
+
75
+ def ancestors depth_options = {}
76
+ self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => ancestor_conditions
77
+ end
78
+
79
+ def path_ids
80
+ ancestor_ids + [id]
81
+ end
82
+
83
+ def path_conditions
84
+ {self.base_class.primary_key => path_ids}
85
+ end
86
+
87
+ def path depth_options = {}
88
+ self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => path_conditions
89
+ end
90
+
91
+ def depth
92
+ ancestor_ids.size
93
+ end
94
+
95
+ def cache_depth
96
+ write_attribute self.base_class.depth_cache_column, depth
97
+ end
98
+
99
+ # Parent
100
+ def parent= parent
101
+ write_attribute(self.base_class.ancestry_column, if parent.blank? then nil else parent.child_ancestry end)
102
+ end
103
+
104
+ def parent_id= parent_id
105
+ self.parent = if parent_id.blank? then nil else self.base_class.find(parent_id) end
106
+ end
107
+
108
+ def parent_id
109
+ if ancestor_ids.empty? then nil else ancestor_ids.last end
110
+ end
111
+
112
+ def parent
113
+ if parent_id.blank? then nil else self.base_class.find(parent_id) end
114
+ end
115
+
116
+ # Root
117
+ def root_id
118
+ if ancestor_ids.empty? then id else ancestor_ids.first end
119
+ end
120
+
121
+ def root
122
+ if root_id == id then self else self.base_class.find(root_id) end
123
+ end
124
+
125
+ def is_root?
126
+ read_attribute(self.base_class.ancestry_column).blank?
127
+ end
128
+
129
+ # Children
130
+ def child_conditions
131
+ {self.base_class.ancestry_column => child_ancestry}
132
+ end
133
+
134
+ def children
135
+ self.base_class.scoped :conditions => child_conditions
136
+ end
137
+
138
+ def child_ids
139
+ children.all(:select => self.base_class.primary_key).map(&self.base_class.primary_key.to_sym)
140
+ end
141
+
142
+ def has_children?
143
+ self.children.exists? {}
144
+ end
145
+
146
+ def is_childless?
147
+ !has_children?
148
+ end
149
+
150
+ # Siblings
151
+ def sibling_conditions
152
+ {self.base_class.ancestry_column => read_attribute(self.base_class.ancestry_column)}
153
+ end
154
+
155
+ def siblings
156
+ self.base_class.scoped :conditions => sibling_conditions
157
+ end
158
+
159
+ def sibling_ids
160
+ siblings.all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
161
+ end
162
+
163
+ def has_siblings?
164
+ self.siblings.count > 1
165
+ end
166
+
167
+ def is_only_child?
168
+ !has_siblings?
169
+ end
170
+
171
+ # Descendants
172
+ def descendant_conditions
173
+ ["#{self.base_class.ancestry_column} like ? or #{self.base_class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
174
+ end
175
+
176
+ def descendants depth_options = {}
177
+ self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).scoped :conditions => descendant_conditions
178
+ end
179
+
180
+ def descendant_ids depth_options = {}
181
+ descendants(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
182
+ end
183
+
184
+ # Subtree
185
+ def subtree_conditions
186
+ ["#{self.base_class.primary_key} = ? or #{self.base_class.ancestry_column} like ? or #{self.base_class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
187
+ end
188
+
189
+ def subtree depth_options = {}
190
+ self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).scoped :conditions => subtree_conditions
191
+ end
192
+
193
+ def subtree_ids depth_options = {}
194
+ subtree(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
195
+ end
196
+
197
+ # Callback disabling
198
+ def without_ancestry_callbacks
199
+ @disable_ancestry_callbacks = true
200
+ yield
201
+ @disable_ancestry_callbacks = false
202
+ end
203
+
204
+ def ancestry_callbacks_disabled?
205
+ !!@disable_ancestry_callbacks
206
+ end
207
+ end
208
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atd-ancestry
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 3
9
+ - 0
10
+ version: 1.3.0
11
+ platform: ruby
12
+ authors: []
13
+
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-09-09 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Ancestry allows the records of a ActiveRecord model to be organised in a tree structure, using a single, intuitively formatted database column. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single sql query. Additional features are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.
23
+ email:
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - MIT-LICENSE
32
+ - Rakefile
33
+ - README.rdoc
34
+ - lib/ancestry.rb
35
+ - lib/ancestry/has_ancestry.rb
36
+ - lib/ancestry/exceptions.rb
37
+ - lib/ancestry/instance_methods.rb
38
+ - lib/ancestry/class_methods.rb
39
+ has_rdoc: true
40
+ homepage:
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ hash: 3
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.7
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Organise ActiveRecord model into a tree structure
73
+ test_files: []
74
+