kamal-ancestry 1.2.4

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,305 @@
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, 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
10
+ - <b>Rails 2</b>
11
+ - Add to config/environment.rb: <b>config.gem 'ancestry'</b>
12
+ - Install required gems: <b>sudo rake gems:install</b>
13
+ - <b>Rails 3</b>
14
+ - Add to Gemfile: <b>gem 'ancestry'</b>
15
+ - Install required gems: <b>bundle install</b>
16
+
17
+ 2. Add ancestry column to your table
18
+ - Create migration: <b>rails g migration add_ancestry_to_[table] ancestry:string</b>
19
+ - Add index to migration: <b>add_index [table], :ancestry</b> (UP) / <b>remove_index [table], :ancestry</b> (DOWN)
20
+ - Migrate your database: <b>rake db:migrate</b>
21
+
22
+ 3. Add ancestry to your model
23
+ - Add to app/models/[model].rb: <b>has_ancestry</b>
24
+
25
+ Your model is now a tree!
26
+
27
+ = Using acts_as_tree instead of has_ancestry
28
+
29
+ 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.
30
+
31
+ = Organising records into a tree
32
+
33
+ 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:
34
+
35
+ TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
36
+
37
+ You can also create children through the children relation on a node:
38
+
39
+ node.children.create :name => 'Stinky'
40
+
41
+ = Navigating your tree
42
+
43
+ To navigate an Ancestry model, use the following methods on any instance / record:
44
+
45
+ parent Returns the parent of the record, nil for a root node
46
+ parent_id Returns the id of the parent of the record, nil for a root node
47
+ root Returns the root of the tree the record is in, self for a root node
48
+ root_id Returns the id of the root of the tree the record is in
49
+ is_root? Returns true if the record is a root node, false otherwise
50
+ ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
51
+ ancestors Scopes the model on ancestors of the record
52
+ path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
53
+ path Scopes model on path records of the record
54
+ children Scopes the model on children of the record
55
+ child_ids Returns a list of child ids
56
+ has_children? Returns true if the record has any children, false otherwise
57
+ is_childless? Returns true is the record has no childen, false otherwise
58
+ siblings Scopes the model on siblings of the record, the record itself is included
59
+ sibling_ids Returns a list of sibling ids
60
+ has_siblings? Returns true if the record's parent has more than one child
61
+ is_only_child? Returns true if the record is the only child of its parent
62
+ descendants Scopes the model on direct and indirect children of the record
63
+ descendant_ids Returns a list of a descendant ids
64
+ subtree Scopes the model on descendants and itself
65
+ subtree_ids Returns a list of all ids in the record's subtree
66
+ depth Return the depth of the node, root nodes are at depth 0
67
+
68
+ = Options for has_ancestry
69
+
70
+ The has_ancestry methods supports the following options:
71
+
72
+ :ancestry_column Pass in a symbol to store ancestry in a different column
73
+ :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
74
+ :destroy All children are destroyed as well (default)
75
+ :rootify The children of the destroyed node become root nodes
76
+ :restrict An AncestryException is raised if any children exist
77
+ :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
78
+ If you turn depth_caching on for an existing model:
79
+ - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
80
+ - Build cache: TreeNode.rebuild_depth_cache!
81
+ :depth_cache_column Pass in a symbol to store depth cache in a different column
82
+ :primary_key_format Supply a regular expression that matches the format of your primary key.
83
+ By default, primary keys only match integers ([0-9]+).
84
+
85
+ = (Named) Scopes
86
+
87
+ 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:
88
+
89
+ node.children.exists?(:name => 'Mary')
90
+ node.subtree.all(:order => :name, :limit => 10).each do; ...; end
91
+ node.descendants.count
92
+
93
+ For convenience, a couple of named scopes are included at the class level:
94
+
95
+ roots Root nodes
96
+ ancestors_of(node) Ancestors of node, node can be either a record or an id
97
+ children_of(node) Children of node, node can be either a record or an id
98
+ descendants_of(node) Descendants of node, node can be either a record or an id
99
+ subtree_of(node) Subtree of node, node can be either a record or an id
100
+ siblings_of(node) Siblings of node, node can be either a record or an id
101
+
102
+ Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
103
+
104
+ node.children.create
105
+ node.siblings.create!
106
+ TestNode.children_of(node_id).new
107
+ TestNode.siblings_of(node_id).create
108
+
109
+ = Selecting nodes by depth
110
+
111
+ When depth caching is enabled (see has_ancestry options), five more named scopes can be used to select nodes on their depth:
112
+
113
+ before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
114
+ to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
115
+ at_depth(depth) Return nodes that are at depth (node.depth == depth)
116
+ from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
117
+ after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
118
+
119
+ 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:
120
+
121
+ node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
122
+ node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
123
+ node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
124
+ node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
125
+ node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
126
+ node.path(:from_depth => -2) The node's grandparent, parent and the node itself
127
+
128
+ node.ancestors(:from_depth => -6, :to_depth => -4)
129
+ node.path.from_depth(3).to_depth(4)
130
+ node.descendants(:from_depth => 2, :to_depth => 4)
131
+ node.subtree.from_depth(10).to_depth(12)
132
+
133
+ 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.
134
+
135
+ = STI support
136
+
137
+ 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.
138
+
139
+ = Arrangement
140
+
141
+ Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
142
+
143
+ { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
144
+ => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
145
+ => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
146
+ => {}
147
+ }
148
+ }
149
+ }
150
+
151
+ The arrange method also works on a scoped class, for example:
152
+
153
+ TreeNode.find_by_name('Crunchy').subtree.arrange
154
+
155
+ 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:
156
+
157
+ TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
158
+
159
+ = Migrating from plugin that uses parent_id column
160
+
161
+ 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:
162
+
163
+ 1. Add ancestry column to your table
164
+ - Create migration: <b>rails g migration add_ancestry_to_[table] ancestry:string</b>
165
+ - Add index to migration: <b>add_index [table], :ancestry</b> (UP) / <b>remove_index [table], :ancestry</b> (DOWN)
166
+ - Migrate your database: <b>rake db:migrate</b>
167
+
168
+ 2. Remove old tree plugin or gem and add in Ancestry
169
+ - Remove plugin: rm -Rf vendor/plugins/[old plugin]
170
+ - Remove gem config line from environment.rb: config.gem [old gem]
171
+ - Add Ancestry to environment.rb: config.gem :ancestry
172
+ - See 'Installation' for more info on installing and configuring gems
173
+
174
+ 3. Change your model
175
+ - Remove any macros required by old plugin/gem from app/models/[model].rb
176
+ - Add to app/models/[model].rb: <b>has_ancestry</b>
177
+
178
+ 4. Generate ancestry columns
179
+ - In './script.console': <b>[model].build_ancestry_from_parent_ids!</b>
180
+ - Make sure it worked ok: <b>[model].check_ancestry_integrity!</b>
181
+
182
+ 5. Change your code
183
+ - Most tree calls will probably work fine with ancestry
184
+ - Others must be changed or proxied
185
+ - Check if all your data is intact and all tests pass
186
+
187
+ 6. Drop parent_id column:
188
+ - Create migration: <b>rails g migration remove_parent_id_from_[table]</b>
189
+ - Add to migration: <b>remove_column [table], :parent_id</b> (UP) / <b>add_column [table], :parent_id, :integer</b> (DOWN)
190
+ - Migrate your database: <b>rake db:migrate</b>
191
+
192
+ = Integrity checking and restoration
193
+
194
+ 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.
195
+
196
+ 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. 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!.
197
+
198
+ For example, from IRB:
199
+
200
+ >> stinky = TreeNode.create :name => 'Stinky'
201
+ $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
202
+ >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
203
+ $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
204
+ >> stinky.update_attribute :parent, squeeky
205
+ $ true
206
+ >> TreeNode.all
207
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
208
+ >> TreeNode.check_ancestry_integrity!
209
+ !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
210
+ >> TreeNode.restore_ancestry_integrity!
211
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
212
+
213
+ Additionally, if you think something is wrong with your depth cache:
214
+
215
+ >> TreeNode.rebuild_depth_cache!
216
+
217
+ = Tests
218
+
219
+ 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 check out the repository from GitHub, copy test/database.example.yml to test/database.yml and type 'rake'. You can pass rake style options for ActiveRecord version to test against (e.g. ar=3.0.1) and database to test against (e.g. db=mysql). The test suite is located in test/has_ancestry_test.rb.
220
+
221
+ = Internals
222
+
223
+ 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.
224
+
225
+ 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.
226
+
227
+ 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.
228
+
229
+ = Version history
230
+
231
+ The latest version of ancestry is recommended. 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.
232
+
233
+ - Version 1.2.3 (2010-10-28)
234
+ - Fixed error with determining ActiveRecord version
235
+ - Added option to specify :primary_key_format (thanks goes to rolftimmermans)
236
+ - Version 1.2.2 (2010-10-24)
237
+ - Fixed all deprecation warnings for rails 3.0.X
238
+ - Added :report option to check_ancestry_integrity!
239
+ - Changed ActiveRecord dependency to 2.2.2
240
+ - Tested and fixed for ruby 1.8.7 and 1.9.2
241
+ - Changed usage of update_attributes to update_attribute to allow ancestry column protection
242
+ - Version 1.2.0 (2009-11-07)
243
+ - Removed some duplication in has_ancestry
244
+ - Cleaned up plugin pattern according to http://yehudakatz.com/2009/11/12/better-ruby-idioms/
245
+ - Moved parts of ancestry into seperate files
246
+ - Made it possible to pass options into the arrange method
247
+ - Renamed acts_as_tree to has_ancestry
248
+ - Aliased has_ancestry as acts_as_tree if acts_as_tree is available
249
+ - Added subtree_of scope
250
+ - Updated ordered_by_ancestry scope to support Microsoft SQL Server
251
+ - Added empty hash as parameter to exists? calls for older ActiveRecord versions
252
+ - Version 1.1.4 (2009-11-07)
253
+ - Thanks to a patch from tom taylor, Ancestry now works with different primary keys
254
+ - Version 1.1.3 (2009-11-01)
255
+ - Fixed a pretty bad bug where several operations took far too many queries
256
+ - Version 1.1.2 (2009-10-29)
257
+ - Added validation for depth cache column
258
+ - Added STI support (reported broken)
259
+ - Version 1.1.1 (2009-10-28)
260
+ - Fixed some parentheses warnings that where reported
261
+ - Fixed a reported issue with arrangement
262
+ - Fixed issues with ancestors and path order on postgres
263
+ - Added ordered_by_ancestry scope (needed to fix issues)
264
+ - Version 1.1.0 (2009-10-22)
265
+ - Depth caching (and cache rebuilding)
266
+ - Depth method for nodes
267
+ - Named scopes for selecting by depth
268
+ - Relative depth options for tree navigation methods:
269
+ - ancestors
270
+ - path
271
+ - descendants
272
+ - descendant_ids
273
+ - subtree
274
+ - subtree_ids
275
+ - Updated README
276
+ - Easy migration from existing plugins/gems
277
+ - acts_as_tree checks unknown options
278
+ - acts_as_tree checks that options are hash
279
+ - Added a bang (!) to the integrity functions
280
+ - 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.
281
+ - Updated install script to point to documentation
282
+ - Removed rails specific init
283
+ - Removed uninstall script
284
+ - Version 1.0.0 (2009-10-16)
285
+ - Initial version
286
+ - Tree building
287
+ - Tree navigation
288
+ - Integrity checking / restoration
289
+ - Arrangement
290
+ - Orphan strategies
291
+ - Subtree movement
292
+ - Named scopes
293
+ - Validations
294
+
295
+ = Future work
296
+
297
+ 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.
298
+
299
+ = Contact and copyright
300
+
301
+ 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.
302
+
303
+ Question? Contact me at s.a.kroes[at]gmail.com, make sure you read the documentation.
304
+
305
+ Copyright (c) 2009 Stefan Kroes, released under the MIT license
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'kamal-ancestry'
3
+ s.description = 'Organise ActiveRecord model into a tree structure'
4
+ s.summary = '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.'
5
+
6
+ s.version = '1.2.4'
7
+
8
+ s.author = 'Stefan Kroes'
9
+ s.email = 's.a.kroes@gmail.com'
10
+ s.homepage = 'http://github.com/stefankroes/ancestry'
11
+
12
+ s.files = [
13
+ 'ancestry.gemspec',
14
+ 'init.rb',
15
+ 'install.rb',
16
+ 'lib/ancestry.rb',
17
+ 'lib/ancestry/has_ancestry.rb',
18
+ 'lib/ancestry/exceptions.rb',
19
+ 'lib/ancestry/class_methods.rb',
20
+ 'lib/ancestry/instance_methods.rb',
21
+ 'MIT-LICENSE',
22
+ 'README.rdoc'
23
+ ]
24
+
25
+ s.add_dependency 'activerecord', '>= 2.2.2'
26
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'ancestry'
@@ -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 @@
1
+ require 'ancestry/has_ancestry'
@@ -0,0 +1,137 @@
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(ActiveSupport::OrderedHash.new) 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] = ActiveSupport::OrderedHash.new; arranged_nodes
47
+ end
48
+ end
49
+
50
+ # Integrity checking
51
+ def check_ancestry_integrity! options = {}
52
+ parents = {}
53
+ exceptions = [] if options[:report] == :list
54
+ # For each node ...
55
+ self.base_class.all.each do |node|
56
+ begin
57
+ # ... check validity of ancestry column
58
+ if !node.valid? and !node.errors[node.class.ancestry_column].blank?
59
+ raise Ancestry::AncestryIntegrityException.new "Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}."
60
+ end
61
+ # ... check that all ancestors exist
62
+ node.ancestor_ids.each do |ancestor_id|
63
+ unless exists? ancestor_id
64
+ raise Ancestry::AncestryIntegrityException.new "Reference to non-existent node in node #{node.id}: #{ancestor_id}."
65
+ end
66
+ end
67
+ # ... check that all node parents are consistent with values observed earlier
68
+ node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
69
+ parents[node_id] = parent_id unless parents.has_key? node_id
70
+ unless parents[node_id] == parent_id
71
+ raise Ancestry::AncestryIntegrityException.new "Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}"
72
+ end
73
+ end
74
+ rescue Ancestry::AncestryIntegrityException => integrity_exception
75
+ case options[:report]
76
+ when :list then exceptions << integrity_exception
77
+ when :echo then puts integrity_exception
78
+ else raise integrity_exception
79
+ end
80
+ end
81
+ end
82
+ exceptions if options[:report] == :list
83
+ end
84
+
85
+ # Integrity restoration
86
+ def restore_ancestry_integrity!
87
+ parents = {}
88
+ # For each node ...
89
+ self.base_class.all.each do |node|
90
+ # ... set its ancestry to nil if invalid
91
+ if node.errors[node.class.ancestry_column].blank?
92
+ node.without_ancestry_callbacks do
93
+ node.update_attribute node.ancestry_column, nil
94
+ end
95
+ end
96
+ # ... save parent of this node in parents array if it exists
97
+ parents[node.id] = node.parent_id if exists? node.parent_id
98
+
99
+ # Reset parent id in array to nil if it introduces a cycle
100
+ parent = parents[node.id]
101
+ until parent.nil? || parent == node.id
102
+ parent = parents[parent]
103
+ end
104
+ parents[node.id] = nil if parent == node.id
105
+ end
106
+ # For each node ...
107
+ self.base_class.all.each do |node|
108
+ # ... rebuild ancestry from parents array
109
+ ancestry, parent = nil, parents[node.id]
110
+ until parent.nil?
111
+ ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
112
+ end
113
+ node.without_ancestry_callbacks do
114
+ node.update_attribute node.ancestry_column, ancestry
115
+ end
116
+ end
117
+ end
118
+
119
+ # Build ancestry from parent id's for migration purposes
120
+ def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
121
+ self.base_class.all(:conditions => {self.base_class.parent_column => parent_id}).each do |node|
122
+ node.without_ancestry_callbacks do
123
+ node.update_attribute ancestry_column, ancestry
124
+ end
125
+ build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
126
+ end
127
+ end
128
+
129
+ # Rebuild depth cache if it got corrupted or if depth caching was just turned on
130
+ def rebuild_depth_cache!
131
+ raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
132
+ self.base_class.all.each do |node|
133
+ node.update_attribute depth_cache_column, node.depth
134
+ end
135
+ end
136
+ end
137
+ 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,93 @@
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, :parent_column, :orphan_strategy, :cache_depth, :depth_cache_column, :primary_key_format].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 parent column accessor and set to option or default
26
+ cattr_accessor :parent_column
27
+ self.parent_column = options[:parent_column] || :parent_id
28
+
29
+ # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
30
+ cattr_reader :orphan_strategy
31
+ self.orphan_strategy = options[:orphan_strategy] || :destroy
32
+
33
+ # Save self as base class (for STI)
34
+ cattr_accessor :base_class
35
+ self.base_class = self
36
+
37
+ # Validate format of ancestry column value
38
+ primary_key_format = options[:primary_key_format] || /[0-9]+/
39
+ validates_format_of ancestry_column, :with => /\A#{primary_key_format.source}(\/#{primary_key_format.source})*\Z/, :allow_nil => true
40
+
41
+ # Validate that the ancestor ids don't include own id
42
+ validate :ancestry_exclude_self
43
+
44
+ # Save ActiveRecord version
45
+ self.cattr_accessor :rails_3
46
+ self.rails_3 = defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 3
47
+
48
+ # Workaround to support Rails 2
49
+ scope_method = if rails_3 then :scope else :named_scope end
50
+
51
+ # Named scopes
52
+ send scope_method, :roots, :conditions => {ancestry_column => nil}
53
+ send scope_method, :ancestors_of, lambda { |object| {:conditions => to_node(object).ancestor_conditions} }
54
+ send scope_method, :children_of, lambda { |object| {:conditions => to_node(object).child_conditions} }
55
+ send scope_method, :descendants_of, lambda { |object| {:conditions => to_node(object).descendant_conditions} }
56
+ send scope_method, :subtree_of, lambda { |object| {:conditions => to_node(object).subtree_conditions} }
57
+ send scope_method, :siblings_of, lambda { |object| {:conditions => to_node(object).sibling_conditions} }
58
+ send scope_method, :ordered_by_ancestry, :order => "(case when #{ancestry_column} is null then 0 else 1 end), #{ancestry_column}"
59
+ send scope_method, :ordered_by_ancestry_and, lambda { |order| {:order => "(case when #{ancestry_column} is null then 0 else 1 end), #{ancestry_column}, #{order}"} }
60
+
61
+ # Update descendants with new ancestry before save
62
+ before_save :update_descendants_with_new_ancestry
63
+
64
+ # Apply orphan strategy before destroy
65
+ before_destroy :apply_orphan_strategy
66
+
67
+ # Create ancestry column accessor and set to option or default
68
+ if options[:cache_depth]
69
+ # Create accessor for column name and set to option or default
70
+ self.cattr_accessor :depth_cache_column
71
+ self.depth_cache_column = options[:depth_cache_column] || :ancestry_depth
72
+
73
+ # Cache depth in depth cache column before save
74
+ before_validation :cache_depth
75
+
76
+ # Validate depth column
77
+ validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
78
+ end
79
+
80
+ # Create named scopes for depth
81
+ {:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
82
+ send scope_method, scope_name, lambda { |depth|
83
+ raise Ancestry::AncestryException.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless options[:cache_depth]
84
+ {:conditions => ["#{depth_cache_column} #{operator} ?", depth]}
85
+ }
86
+ end
87
+ end
88
+
89
+ # Alias has_ancestry with acts_as_tree, if it's available.
90
+ if !respond_to?(:acts_as_tree)
91
+ alias_method :acts_as_tree, :has_ancestry
92
+ end
93
+ end
@@ -0,0 +1,232 @@
1
+ module Ancestry
2
+ module InstanceMethods
3
+ # Validate that the ancestors don't include itself
4
+ def ancestry_exclude_self
5
+ add_error_to_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.base_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(
19
+ self.base_class.ancestry_column,
20
+ descendant.read_attribute(descendant.class.ancestry_column).gsub(
21
+ /^#{self.child_ancestry}/,
22
+ if read_attribute(self.class.ancestry_column).blank? then id.to_s else "#{read_attribute self.class.ancestry_column }/#{id}" end
23
+ )
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ # Apply orphan strategy
32
+ def apply_orphan_strategy
33
+ # Skip this if callbacks are disabled
34
+ unless ancestry_callbacks_disabled?
35
+ # If this isn't a new record ...
36
+ unless new_record?
37
+ # ... make al children root if orphan strategy is rootify
38
+ if self.base_class.orphan_strategy == :rootify
39
+ descendants.each do |descendant|
40
+ descendant.without_ancestry_callbacks do
41
+ descendant.update_attribute descendant.class.ancestry_column, (if descendant.ancestry == child_ancestry then nil else descendant.ancestry.gsub(/^#{child_ancestry}\//, '') end)
42
+ end
43
+ end
44
+ # ... destroy all descendants if orphan strategy is destroy
45
+ elsif self.base_class.orphan_strategy == :destroy
46
+ descendants.all.each do |descendant|
47
+ descendant.without_ancestry_callbacks do
48
+ descendant.destroy
49
+ end
50
+ end
51
+ # ... throw an exception if it has children and orphan strategy is restrict
52
+ elsif self.base_class.orphan_strategy == :restrict
53
+ raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ # The ancestry value for this record's children
60
+ def child_ancestry
61
+ # New records cannot have children
62
+ raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
63
+
64
+ 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
65
+ end
66
+
67
+ # Ancestors
68
+ def ancestor_ids
69
+ read_attribute(self.base_class.ancestry_column).to_s.split('/').map { |id| cast_primary_key(id) }
70
+ end
71
+
72
+ def ancestor_conditions
73
+ {self.base_class.primary_key => ancestor_ids}
74
+ end
75
+
76
+ def ancestors depth_options = {}
77
+ self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => ancestor_conditions
78
+ end
79
+
80
+ def path_ids
81
+ ancestor_ids + [id]
82
+ end
83
+
84
+ def path_conditions
85
+ {self.base_class.primary_key => path_ids}
86
+ end
87
+
88
+ def path depth_options = {}
89
+ self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => path_conditions
90
+ end
91
+
92
+ def depth
93
+ ancestor_ids.size
94
+ end
95
+
96
+ def cache_depth
97
+ write_attribute self.base_class.depth_cache_column, depth
98
+ end
99
+
100
+ # Parent
101
+ def parent= parent
102
+ write_attribute(self.base_class.ancestry_column, if parent.blank? then nil else parent.child_ancestry end)
103
+ end
104
+
105
+ def parent_id= parent_id
106
+ self.parent = if parent_id.blank? then nil else self.base_class.find(parent_id) end
107
+ end
108
+
109
+ def parent_id
110
+ if ancestor_ids.empty? then nil else ancestor_ids.last end
111
+ end
112
+
113
+ def parent
114
+ if parent_id.blank? then nil else self.base_class.find(parent_id) end
115
+ end
116
+
117
+ # Root
118
+ def root_id
119
+ if ancestor_ids.empty? then id else ancestor_ids.first end
120
+ end
121
+
122
+ def root
123
+ if root_id == id then self else self.base_class.find(root_id) end
124
+ end
125
+
126
+ def is_root?
127
+ read_attribute(self.base_class.ancestry_column).blank?
128
+ end
129
+
130
+ # Children
131
+ def child_conditions
132
+ {self.base_class.ancestry_column => child_ancestry}
133
+ end
134
+
135
+ def children
136
+ self.base_class.scoped :conditions => child_conditions
137
+ end
138
+
139
+ def child_ids
140
+ children.all(:select => self.base_class.primary_key).map(&self.base_class.primary_key.to_sym)
141
+ end
142
+
143
+ def has_children?
144
+ self.children.exists?({})
145
+ end
146
+
147
+ def is_childless?
148
+ !has_children?
149
+ end
150
+
151
+ # Siblings
152
+ def sibling_conditions
153
+ {self.base_class.ancestry_column => read_attribute(self.base_class.ancestry_column)}
154
+ end
155
+
156
+ def siblings
157
+ self.base_class.scoped :conditions => sibling_conditions
158
+ end
159
+
160
+ def sibling_ids
161
+ siblings.all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
162
+ end
163
+
164
+ def has_siblings?
165
+ self.siblings.count > 1
166
+ end
167
+
168
+ def is_only_child?
169
+ !has_siblings?
170
+ end
171
+
172
+ # Descendants
173
+ def descendant_conditions
174
+ ["#{self.base_class.ancestry_column} like ? or #{self.base_class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
175
+ end
176
+
177
+ def descendants depth_options = {}
178
+ self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).scoped :conditions => descendant_conditions
179
+ end
180
+
181
+ def descendant_ids depth_options = {}
182
+ descendants(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
183
+ end
184
+
185
+ # Subtree
186
+ def subtree_conditions
187
+ ["#{self.base_class.primary_key} = ? or #{self.base_class.ancestry_column} like ? or #{self.base_class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
188
+ end
189
+
190
+ def subtree depth_options = {}
191
+ self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).scoped :conditions => subtree_conditions
192
+ end
193
+
194
+ def subtree_ids depth_options = {}
195
+ subtree(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
196
+ end
197
+
198
+ # Callback disabling
199
+ def without_ancestry_callbacks
200
+ @disable_ancestry_callbacks = true
201
+ yield
202
+ @disable_ancestry_callbacks = false
203
+ end
204
+
205
+ def ancestry_callbacks_disabled?
206
+ !!@disable_ancestry_callbacks
207
+ end
208
+
209
+ private
210
+
211
+ # Workaround to support Rails 2
212
+ def add_error_to_base error
213
+ if rails_3
214
+ errors[:base] << error
215
+ else
216
+ errors.add_to_base error
217
+ end
218
+ end
219
+
220
+ def cast_primary_key(key)
221
+ if primary_key_type == :string
222
+ key
223
+ else
224
+ key.to_i
225
+ end
226
+ end
227
+
228
+ def primary_key_type
229
+ @primary_key_type ||= column_for_attribute(self.class.primary_key).type
230
+ end
231
+ end
232
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kamal-ancestry
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 2
8
+ - 4
9
+ version: 1.2.4
10
+ platform: ruby
11
+ authors:
12
+ - Stefan Kroes
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-03-31 00:00:00 +08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 2
30
+ - 2
31
+ - 2
32
+ version: 2.2.2
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Organise ActiveRecord model into a tree structure
36
+ email: s.a.kroes@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - ancestry.gemspec
45
+ - init.rb
46
+ - install.rb
47
+ - lib/ancestry.rb
48
+ - lib/ancestry/has_ancestry.rb
49
+ - lib/ancestry/exceptions.rb
50
+ - lib/ancestry/class_methods.rb
51
+ - lib/ancestry/instance_methods.rb
52
+ - MIT-LICENSE
53
+ - README.rdoc
54
+ has_rdoc: true
55
+ homepage: http://github.com/stefankroes/ancestry
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.3.7
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: 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.
86
+ test_files: []
87
+