ancestry 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  = Ancestry
2
2
 
3
- 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.
3
+ Ancestry allows the records of a ActiveRecord model to be organised as a tree structure, using 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 named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.
4
4
 
5
5
  = Installation
6
6
 
@@ -17,7 +17,7 @@ To apply Ancestry to any ActiveRecord model, follow these simple steps:
17
17
 
18
18
  2. Add ancestry column to your table
19
19
  - Create migration: ./script/generate migration add_ancestry_to_[table] ancestry:string
20
- - Add index to migration: add_index [table], :ancestry / remove_index [table], :ancestry
20
+ - Add index to migration: add_index [table], :ancestry (UP) / remove_index [table], :ancestry (DOWN)
21
21
  - Migrate your database: rake db:migrate
22
22
 
23
23
  3. Add ancestry to your model
@@ -39,26 +39,41 @@ You can also create children through the children relation on a node:
39
39
 
40
40
  To navigate an Ancestry model, use the following methods on any instance / record:
41
41
 
42
- parent Returns the parent of the record
43
- root Returns the root of the tree the record is in
44
- root_id Returns the id of the root of the tree the record is in
45
- is_root? Returns true if the record is a root node, false otherwise
46
- ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
47
- ancestors Scopes the model on ancestors of the record
48
- path_ids Returns a list the path ids, starting with the root is and ending with the node's own id
49
- path Scopes model on path records of the record
50
- children Scopes the model on children of the record
51
- child_ids Returns a list of child ids
52
- has_children? Returns true if the record has any children, false otherwise
53
- is_childless? Returns true is the record has no childen, false otherwise
54
- siblings Scopes the model on siblings of the record, the record itself is included
55
- sibling_ids Returns a list of sibling ids
56
- has_siblings? Returns true if the record's parent has more than one child
57
- is_only_child? Returns true if the record is the only child of its parent
58
- descendants Scopes the model on direct and indirect children of the record
59
- descendant_ids Returns a list of a descendant ids
60
- subtree Scopes the model on descendants and itself
61
- subtree_ids Returns a list of all ids in the record's subtree
42
+ parent Returns the parent of the record, nil for a root node
43
+ parent_id Returns the id of the parent of the record, nil for a root node
44
+ root Returns the root of the tree the record is in, self for a root node
45
+ root_id Returns the id of the root of the tree the record is in
46
+ is_root? Returns true if the record is a root node, false otherwise
47
+ ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
48
+ ancestors Scopes the model on ancestors of the record
49
+ path_ids Returns a list the path ids, starting with the root is and ending with the node's own id
50
+ path Scopes model on path records of the record
51
+ children Scopes the model on children of the record
52
+ child_ids Returns a list of child ids
53
+ has_children? Returns true if the record has any children, false otherwise
54
+ is_childless? Returns true is the record has no childen, false otherwise
55
+ siblings Scopes the model on siblings of the record, the record itself is included
56
+ sibling_ids Returns a list of sibling ids
57
+ has_siblings? Returns true if the record's parent has more than one child
58
+ is_only_child? Returns true if the record is the only child of its parent
59
+ descendants Scopes the model on direct and indirect children of the record
60
+ descendant_ids Returns a list of a descendant ids
61
+ subtree Scopes the model on descendants and itself
62
+ subtree_ids Returns a list of all ids in the record's subtree
63
+ depth Return the depth of the node, root nodes are at depth 0
64
+
65
+ = acts_as_tree Options
66
+
67
+ The acts_as_tree methods supports two options:
68
+
69
+ :ancestry_column Pass in a symbol to store ancestry in a different column
70
+ :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
71
+ :destroy All children are destroyed as well (default)
72
+ :rootify The children of the destroyed node become root nodes
73
+ :restrict An AncestryException is raised if any children exist
74
+ :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
75
+ If you turn depth_caching on for an existing model, use: TreeNode.rebuild_depth_cache!
76
+ :depth_cache_column Pass in a symbol to store depth cache in a different column
62
77
 
63
78
  = (Named) Scopes
64
79
 
@@ -70,11 +85,11 @@ Where possible, the navigation methods return scopes instead of records, this me
70
85
 
71
86
  For convenience, a couple of named scopes are included at the class level:
72
87
 
73
- roots Only root nodes
74
- ancestors_of(node) Only ancestors of node, node can be either a record or an id
75
- children_of(node) Only children of node, node can be either a record or an id
76
- descendants_of(node) Only descendants of node, node can be either a record or an id
77
- siblings_of(node) Only siblings of node, node can be either a record or an id
88
+ roots Root nodes
89
+ ancestors_of(node) Ancestors of node, node can be either a record or an id
90
+ children_of(node) Children of node, node can be either a record or an id
91
+ descendants_of(node) Descendants of node, node can be either a record or an id
92
+ siblings_of(node) Siblings of node, node can be either a record or an id
78
93
 
79
94
  Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
80
95
 
@@ -83,15 +98,31 @@ Thanks to some convenient rails magic, it is even possible to create nodes throu
83
98
  TestNode.children_of(node_id).new
84
99
  TestNode.siblings_of(node_id).create
85
100
 
86
- = acts_as_tree Options
101
+ = Selecting nodes by depth
87
102
 
88
- The acts_as_tree methods supports two options:
103
+ When depth caching is enabled (see acts_as_tree options), five more named scopes can be used to select nodes on their depth:
89
104
 
90
- ancestry_column Pass in a symbol to instruct Ancestry to use a different column name to store record ancestry
91
- orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
92
- :destroy All children are destroyed as well (default)
93
- :rootify The children of the destroyed node become root nodes
94
- :restrict An AncestryException is raised if any children exist
105
+ before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
106
+ to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
107
+ at_depth(depth) Return nodes that are at depth (node.depth == depth)
108
+ from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
109
+ after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
110
+
111
+ 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:
112
+
113
+ node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
114
+ node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
115
+ node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
116
+ node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
117
+ node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
118
+ node.path(:from_depth => -2) The node's grandparent, parent and the node itself
119
+
120
+ node.ancestors(:from_depth => -6, :to_depth => -4)
121
+ node.path.from_depth(3).to_depth(4)
122
+ node.descendants(:from_depth => 2, :to_depth => 4)
123
+ node.subtree.from_depth(10).to_depth(12)
124
+
125
+ 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.
95
126
 
96
127
  = Arrangement
97
128
 
@@ -109,9 +140,44 @@ The arrange method also works on a scoped class, for example:
109
140
 
110
141
  TreeNode.find_by_name('Crunchy').subtree.arrange
111
142
 
143
+ = Migrating from plugin that uses parent_id column
144
+
145
+ Most current tree plugins use a parent_id column (acts_as_tree, awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry its easy to migrate from any of these plugins, to do so, follow these steps:
146
+
147
+ 1. Add ancestry column to your table
148
+ - Create migration: ./script/generate migration add_ancestry_to_[table] ancestry:string
149
+ - Add index to migration: add_index [table], :ancestry (UP) / remove_index [table], :ancestry (DOWN)
150
+ - Migrate your database: rake db:migrate
151
+
152
+ 2. Remove old tree plugin or gem and add in Ancestry
153
+ - Remove plugin: rm -Rf vendor/plugins/[old plugin]
154
+ - Remove gem config line from environment.rb: config.gem [old gem]
155
+ - Add Ancestry to environment.rb: config.gem :ancestry
156
+ - See 'Installation' for more info on installing and configuring gems
157
+
158
+ 3. Change your model
159
+ - Remove any macros required by old plugin/gem from app/models/[model].rb
160
+ - Add to app/models/[model].rb: acts_as_tree
161
+
162
+ 4. Migrate database
163
+ - In './script.console': [model].build_ancestry_from_parent_ids!
164
+ - Make sure it worked ok: [model].check_ancestry_integrity!
165
+
166
+ 5. Change your code
167
+ - Most tree calls will probably work fine with ancestry
168
+ - Others must be changed or proxied
169
+ - Check if all your data is intact and all tests pass
170
+
171
+ 6. Drop parent_id column:
172
+ - Create migration: ./script/generate migration remove_parent_id_from_[table]
173
+ - Add to migration: remove_column [table], :parent_id (UP) / add_column [table], :parent_id, :integer (DOWN)
174
+ - Migrate your database: rake db:migrate
175
+
112
176
  = Integrity Checking and Restoration
113
177
 
114
- 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. I did include 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.
178
+ 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.
179
+
180
+ 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!.
115
181
 
116
182
  For example, from IRB:
117
183
 
@@ -123,29 +189,68 @@ For example, from IRB:
123
189
  $ true
124
190
  >> TreeNode.all
125
191
  $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
126
- >> TreeNode.check_ancestry_integrity
192
+ >> TreeNode.check_ancestry_integrity!
127
193
  !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
128
- >> TreeNode.restore_ancestry_integrity
194
+ >> TreeNode.restore_ancestry_integrity!
129
195
  $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
130
196
 
131
- = Testing
197
+ Additionally, if you think something is wrong with your depth cache:
198
+
199
+ >> TreeNode.rebuild_depth_cache!
132
200
 
133
- The Ancestry gem comes with a unit test suite consisting of about 1500 assertions in 20 tests. It takes about 4 seconds to run on sqlite. To run it yourself, install Ancestry as a plugin, go to the ancestry folder and type 'rake'. The test suite is located in 'test/acts_as_tree_test.rb'.
201
+ = Tests
202
+
203
+ 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/acts_as_tree_test.rb'.
134
204
 
135
205
  = Internals
136
206
 
137
207
  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.
138
208
 
139
- 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 a index cannot be put on the column in that case.
209
+ 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.
210
+
211
+ 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.
212
+
213
+ = Version history
214
+
215
+ The latest and recommended version of ancestry is 1.1.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.
216
+
217
+ - Version 1.1.0 (2009-10-22)
218
+ - Depth caching (and cache rebuilding)
219
+ - Depth method for nodes
220
+ - Named scopes for selecting by depth
221
+ - Relative depth options for tree navigation methods:
222
+ - ancestors
223
+ - path
224
+ - descendants
225
+ - descendant_ids
226
+ - subtree
227
+ - subtree_ids
228
+ - Updated README
229
+ - Easy migration from existing plugins/gems
230
+ - acts_as_tree checks unknown options
231
+ - acts_as_tree checks that options are hash
232
+ - Added a bang (!) to the integrity functions
233
+ - Since these functions should only be used from ./script/console and not from your appliction, this change is not considered as breaking backwards compatibility and the major version wasn't bumped.
234
+ - Updated install script to point to documentation
235
+ - Removed rails specific init
236
+ - Removed uninstall script
237
+ - Version 1.0.0 (2009-10-16)
238
+ - Initial version
239
+ - Tree building
240
+ - Tree navigation
241
+ - Integrity checking / restoration
242
+ - Arrangement
243
+ - Orphan strategies
244
+ - Subtree movement
245
+ - Named scopes
246
+ - Validations
140
247
 
141
248
  = Future Work
142
249
 
143
- 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. Something that definitely needs to be added in the future is constraints on depth, something like: tree_node.subtree.to_depth(4)
144
-
145
- = Feedback
146
-
147
- Question? Bug report? Faulty/incomplete documentation? Feature request? Please contact me at s.a.kroes[at]gmail.com
250
+ 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.
148
251
 
252
+ = Contact and Copyright
149
253
 
254
+ Question? 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.
150
255
 
151
256
  Copyright (c) 2009 Stefan Kroes, released under the MIT license
data/Rakefile CHANGED
@@ -14,9 +14,9 @@ end
14
14
 
15
15
  desc 'Generate documentation for the ancestry plugin.'
16
16
  Rake::RDocTask.new(:rdoc) do |rdoc|
17
- rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.rdoc_dir = 'doc'
18
18
  rdoc.title = 'Ancestry'
19
19
  rdoc.options << '--line-numbers' << '--inline-source'
20
- rdoc.rdoc_files.include('README')
20
+ rdoc.rdoc_files.include('README.rdoc')
21
21
  rdoc.rdoc_files.include('lib/**/*.rb')
22
22
  end
@@ -5,14 +5,14 @@ Gem::Specification.new do |s|
5
5
  s.description = 'Organise ActiveRecord model into a tree structure'
6
6
  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.'
7
7
 
8
- s.version = '1.0.0'
9
- s.date = '2009-10-16'
8
+ s.version = '1.1.0'
9
+ s.date = '2009-10-22'
10
10
 
11
11
  s.author = 'Stefan Kroes'
12
12
  s.email = 's.a.kroes@gmail.com'
13
13
  s.homepage = 'http://github.com/stefankroes/ancestry'
14
14
 
15
- s.files = FileList['ancestry.gemspec', '*.rb', 'lib/**/*.rb', 'rails/*', 'test/*', 'Rakefile', 'MIT-LICENSE', 'README.rdoc']
15
+ s.files = FileList['ancestry.gemspec', '*.rb', 'lib/**/*.rb', 'test/*', 'Rakefile', 'MIT-LICENSE', 'README.rdoc']
16
16
 
17
17
  s.add_dependency 'activerecord', '>= 2.1.0'
18
18
  end
data/install.rb CHANGED
@@ -1 +1,2 @@
1
1
  # Install hook code here
2
+ puts "Thank you for install Ancestry. You can visit http://github.com/stefankroes/ancestry to read the documentation."
@@ -11,6 +11,14 @@ module Ancestry
11
11
 
12
12
  module ClassMethods
13
13
  def acts_as_tree options = {}
14
+ # Check options
15
+ raise AncestryException.new("Options for acts_as_tree must be in a hash.") unless options.is_a? Hash
16
+ options.each do |key, value|
17
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column].include? key
18
+ raise AncestryException.new("Unknown options for acts_as_tree: #{key.inspect} => #{value.inspect}.")
19
+ end
20
+ end
21
+
14
22
  # Include instance methods
15
23
  send :include, InstanceMethods
16
24
 
@@ -28,15 +36,46 @@ module Ancestry
28
36
  # Validate format of ancestry column value
29
37
  validates_format_of ancestry_column, :with => /^[0-9]+(\/[0-9]+)*$/, :allow_nil => true
30
38
 
39
+ # Create ancestry column accessor and set to option or default
40
+ if options[:cache_depth]
41
+ self.cattr_accessor :depth_cache_column
42
+ self.depth_cache_column = options[:depth_cache_column] || :ancestry_depth
43
+ # Cache depth in depth cache column before save
44
+ before_save :cache_depth
45
+ # Named scopes for depth
46
+ end
47
+
48
+ # Create named scopes for depth
49
+ named_scope :before_depth, lambda { |depth|
50
+ raise AncestryException.new("Named scope 'before_depth' is only available when depth caching is enabled.") unless options[:cache_depth]
51
+ {:conditions => ["#{depth_cache_column} < ?", depth]}
52
+ }
53
+ named_scope :to_depth, lambda { |depth|
54
+ raise AncestryException.new("Named scope 'to_depth' is only available when depth caching is enabled.") unless options[:cache_depth]
55
+ {:conditions => ["#{depth_cache_column} <= ?", depth]}
56
+ }
57
+ named_scope :at_depth, lambda { |depth|
58
+ raise AncestryException.new("Named scope 'at_depth' is only available when depth caching is enabled.") unless options[:cache_depth]
59
+ {:conditions => ["#{depth_cache_column} = ?", depth]}
60
+ }
61
+ named_scope :from_depth, lambda { |depth|
62
+ raise AncestryException.new("Named scope 'from_depth' is only available when depth caching is enabled.") unless options[:cache_depth]
63
+ {:conditions => ["#{depth_cache_column} >= ?", depth]}
64
+ }
65
+ named_scope :after_depth, lambda { |depth|
66
+ raise AncestryException.new("Named scope 'after_depth' is only available when depth caching is enabled.") unless options[:cache_depth]
67
+ {:conditions => ["#{depth_cache_column} > ?", depth]}
68
+ }
69
+
31
70
  # Validate that the ancestor ids don't include own id
32
71
  validate :ancestry_exclude_self
33
72
 
34
73
  # Named scopes
35
74
  named_scope :roots, :conditions => {ancestry_column => nil}
36
- named_scope :ancestors_of, lambda{ |object| {:conditions => to_node(object).ancestor_conditions} }
37
- named_scope :children_of, lambda{ |object| {:conditions => to_node(object).child_conditions} }
38
- named_scope :descendants_of, lambda{ |object| {:conditions => to_node(object).descendant_conditions} }
39
- named_scope :siblings_of, lambda{ |object| {:conditions => to_node(object).sibling_conditions} }
75
+ named_scope :ancestors_of, lambda { |object| {:conditions => to_node(object).ancestor_conditions} }
76
+ named_scope :children_of, lambda { |object| {:conditions => to_node(object).child_conditions} }
77
+ named_scope :descendants_of, lambda { |object| {:conditions => to_node(object).descendant_conditions} }
78
+ named_scope :siblings_of, lambda { |object| {:conditions => to_node(object).sibling_conditions} }
40
79
 
41
80
  # Update descendants with new ancestry before save
42
81
  before_save :update_descendants_with_new_ancestry
@@ -49,10 +88,22 @@ module Ancestry
49
88
  module DynamicClassMethods
50
89
  # Fetch tree node if necessary
51
90
  def to_node object
52
- object.is_a?(self) ? object : find(object)
91
+ if object.is_a?(self) then object else find(object) end
53
92
  end
54
93
 
55
- # Orhpan strategy writer
94
+ # Scope on relative depth options
95
+ def scope_depth depth_options, depth
96
+ depth_options.inject(self) do |scope, option|
97
+ scope_name, relative_depth = option
98
+ if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
99
+ scope.send scope_name, depth + relative_depth
100
+ else
101
+ raise Ancestry::AncestryException.new("Unknown depth option: #{scope_name}.")
102
+ end
103
+ end
104
+ end
105
+
106
+ # Orphan strategy writer
56
107
  def orphan_strategy= orphan_strategy
57
108
  # Check value of orphan strategy, only rootify, restrict or destroy is allowed
58
109
  if [:rootify, :restrict, :destroy].include? orphan_strategy
@@ -77,7 +128,7 @@ module Ancestry
77
128
  end
78
129
 
79
130
  # Integrity checking
80
- def check_ancestry_integrity
131
+ def check_ancestry_integrity!
81
132
  parents = {}
82
133
  # For each node ...
83
134
  all.each do |node|
@@ -102,7 +153,7 @@ module Ancestry
102
153
  end
103
154
 
104
155
  # Integrity restoration
105
- def restore_ancestry_integrity
156
+ def restore_ancestry_integrity!
106
157
  parents = {}
107
158
  # For each node ...
108
159
  all.each do |node|
@@ -125,11 +176,27 @@ module Ancestry
125
176
  # ... rebuild ancestry from parents array
126
177
  ancestry, parent = nil, parents[node.id]
127
178
  until parent.nil?
128
- ancestry, parent = ancestry.nil? ? parent : "#{parent}/#{ancestry}", parents[parent]
179
+ ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
129
180
  end
130
181
  node.update_attributes node.ancestry_column => ancestry
131
182
  end
132
183
  end
184
+
185
+ # Build ancestry from parent id's for migration purposes
186
+ def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
187
+ all(:conditions => {:parent_id => parent_id}).each do |node|
188
+ node.update_attribute ancestry_column, ancestry
189
+ build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
190
+ end
191
+ end
192
+
193
+ # Build ancestry from parent id's for migration purposes
194
+ def rebuild_depth_cache!
195
+ raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
196
+ all.each do |node|
197
+ node.update_attribute depth_cache_column, node.depth
198
+ end
199
+ end
133
200
  end
134
201
 
135
202
  module InstanceMethods
@@ -149,7 +216,7 @@ module Ancestry
149
216
  self.class.ancestry_column =>
150
217
  descendant.read_attribute(descendant.class.ancestry_column).gsub(
151
218
  /^#{self.child_ancestry}/,
152
- (read_attribute(self.class.ancestry_column).blank? ? id.to_s : "#{read_attribute self.class.ancestry_column }/#{id}")
219
+ if read_attribute(self.class.ancestry_column).blank? then id.to_s else "#{read_attribute self.class.ancestry_column }/#{id}" end
153
220
  )
154
221
  )
155
222
  end
@@ -163,7 +230,7 @@ module Ancestry
163
230
  # ... make al children root if orphan strategy is rootify
164
231
  if self.class.orphan_strategy == :rootify
165
232
  descendants.each do |descendant|
166
- descendant.update_attributes descendant.class.ancestry_column => descendant.ancestry == child_ancestry ? nil : descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
233
+ descendant.update_attributes descendant.class.ancestry_column => (if descendant.ancestry == child_ancestry then nil else descendant.ancestry.gsub(/^#{child_ancestry}\//, '') end)
167
234
  end
168
235
  # ... destroy all descendants if orphan strategy is destroy
169
236
  elsif self.class.orphan_strategy == :destroy
@@ -180,7 +247,7 @@ module Ancestry
180
247
  # New records cannot have children
181
248
  raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
182
249
 
183
- self.send("#{self.class.ancestry_column}_was").blank? ? id.to_s : "#{self.send "#{self.class.ancestry_column}_was"}/#{id}"
250
+ if self.send("#{self.class.ancestry_column}_was").blank? then id.to_s else "#{self.send "#{self.class.ancestry_column}_was"}/#{id}" end
184
251
  end
185
252
 
186
253
  # Ancestors
@@ -192,42 +259,54 @@ module Ancestry
192
259
  {:id => ancestor_ids}
193
260
  end
194
261
 
195
- def ancestors
196
- self.class.scoped :conditions => ancestor_conditions
262
+ def ancestors depth_options = {}
263
+ self.class.scope_depth(depth_options, depth).scoped :conditions => ancestor_conditions, :order => self.class.ancestry_column
197
264
  end
198
265
 
199
266
  def path_ids
200
267
  ancestor_ids + [id]
201
268
  end
202
269
 
203
- def path
204
- ancestors + [self]
270
+ def path_conditions
271
+ {:id => path_ids}
272
+ end
273
+
274
+ def path depth_options = {}
275
+ self.class.scope_depth(depth_options, depth).scoped :conditions => path_conditions, :order => self.class.ancestry_column
276
+ end
277
+
278
+ def depth
279
+ ancestor_ids.size
280
+ end
281
+
282
+ def cache_depth
283
+ write_attribute self.class.depth_cache_column, depth
205
284
  end
206
285
 
207
286
  # Parent
208
287
  def parent= parent
209
- write_attribute(self.class.ancestry_column, parent.blank? ? nil : parent.child_ancestry)
288
+ write_attribute(self.class.ancestry_column, if parent.blank? then nil else parent.child_ancestry end)
210
289
  end
211
290
 
212
291
  def parent_id= parent_id
213
- self.parent = parent_id.blank? ? nil : self.class.find(parent_id)
292
+ self.parent = if parent_id.blank? then nil else self.class.find(parent_id) end
214
293
  end
215
294
 
216
295
  def parent_id
217
- ancestor_ids.empty? ? nil : ancestor_ids.last
296
+ if ancestor_ids.empty? then nil else ancestor_ids.last end
218
297
  end
219
298
 
220
299
  def parent
221
- parent_id.blank? ? nil : self.class.find(parent_id)
300
+ if parent_id.blank? then nil else self.class.find(parent_id) end
222
301
  end
223
302
 
224
303
  # Root
225
304
  def root_id
226
- ancestor_ids.empty? ? id : ancestor_ids.first
305
+ if ancestor_ids.empty? then id else ancestor_ids.first end
227
306
  end
228
307
 
229
308
  def root
230
- root_id == id ? self : self.class.find(root_id)
309
+ if root_id == id then self else self.class.find(root_id) end
231
310
  end
232
311
 
233
312
  def is_root?
@@ -281,20 +360,25 @@ module Ancestry
281
360
  ["#{self.class.ancestry_column} like ? or #{self.class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
282
361
  end
283
362
 
284
- def descendants
285
- self.class.scoped :conditions => descendant_conditions
363
+ def descendants depth_options = {}
364
+ self.class.scope_depth(depth_options, depth).scoped :conditions => descendant_conditions
286
365
  end
287
366
 
288
- def descendant_ids
289
- descendants.all(:select => :id).collect(&:id)
367
+ def descendant_ids depth_options = {}
368
+ descendants(depth_options).all(:select => :id).collect(&:id)
290
369
  end
291
370
 
292
- def subtree
293
- [self] + descendants
371
+ # Subtree
372
+ def subtree_conditions
373
+ ["id = ? or #{self.class.ancestry_column} like ? or #{self.class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
374
+ end
375
+
376
+ def subtree depth_options = {}
377
+ self.class.scope_depth(depth_options, depth).scoped :conditions => subtree_conditions
294
378
  end
295
379
 
296
- def subtree_ids
297
- [self.id] + descendant_ids
380
+ def subtree_ids depth_options = {}
381
+ subtree(depth_options).all(:select => :id).collect(&:id)
298
382
  end
299
383
  end
300
384
  end