ancestry 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +150 -45
- data/Rakefile +2 -2
- data/ancestry.gemspec +3 -3
- data/install.rb +1 -0
- data/lib/ancestry/acts_as_tree.rb +114 -30
- data/test/acts_as_tree_test.rb +176 -9
- data/test/ancestry_plugin.sqlite3.db +0 -0
- data/test/debug.log +211713 -0
- data/test/schema.rb +10 -0
- data/test/test_helper.rb +1 -1
- metadata +2 -4
- data/rails/init.rb +0 -1
- data/uninstall.rb +0 -1
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= Ancestry
|
2
2
|
|
3
|
-
Ancestry allows the records of a ActiveRecord model to be organised
|
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
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
74
|
-
ancestors_of(node)
|
75
|
-
children_of(node)
|
76
|
-
descendants_of(node)
|
77
|
-
siblings_of(node)
|
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
|
-
=
|
101
|
+
= Selecting nodes by depth
|
87
102
|
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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.
|
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
|
-
|
197
|
+
Additionally, if you think something is wrong with your depth cache:
|
198
|
+
|
199
|
+
>> TreeNode.rebuild_depth_cache!
|
132
200
|
|
133
|
-
|
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
|
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.
|
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 = '
|
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
|
data/ancestry.gemspec
CHANGED
@@ -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.
|
9
|
-
s.date = '2009-10-
|
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', '
|
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
@@ -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)
|
91
|
+
if object.is_a?(self) then object else find(object) end
|
53
92
|
end
|
54
93
|
|
55
|
-
#
|
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?
|
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
|
-
|
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
|
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?
|
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
|
204
|
-
|
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?
|
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?
|
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?
|
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?
|
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?
|
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
|
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
|
-
|
293
|
-
|
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
|
-
|
380
|
+
def subtree_ids depth_options = {}
|
381
|
+
subtree(depth_options).all(:select => :id).collect(&:id)
|
298
382
|
end
|
299
383
|
end
|
300
384
|
end
|