ancestry 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +151 -0
- data/Rakefile +22 -0
- data/ancestry.gemspec +18 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/ancestry.rb +1 -0
- data/lib/ancestry/acts_as_tree.rb +302 -0
- data/rails/init.rb +1 -0
- data/test/acts_as_tree_test.rb +385 -0
- data/test/ancestry_plugin.sqlite3.db +0 -0
- data/test/database.yml +18 -0
- data/test/debug.log +67202 -0
- data/test/schema.rb +9 -0
- data/test/test_helper.rb +31 -0
- data/uninstall.rb +1 -0
- metadata +79 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Stefan Kroes
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
= Ancestry
|
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.
|
4
|
+
|
5
|
+
= Installation
|
6
|
+
|
7
|
+
To apply Ancestry to any ActiveRecord model, follow these simple steps:
|
8
|
+
|
9
|
+
1. Install gem
|
10
|
+
- Install gemcutter gem: sudo gem install gemcutter (maybe you need: gem update --system)
|
11
|
+
- Add gemcutter.org as default gem source: gem tumble
|
12
|
+
- Add to config/environment.rb: config.gem 'ancestry'
|
13
|
+
- Install required gems: sudo rake gems:install
|
14
|
+
- Alternatively: sudo gem install ancestry
|
15
|
+
- If you don't want gemcutter: config.gem 'ancestry', :source => 'gemcutter.org'
|
16
|
+
- Alternatively: sudo gem install ancestry --source gemcutter.org
|
17
|
+
|
18
|
+
2. Add ancestry column to your table
|
19
|
+
- Create migration: ./script/generate migration add_ancestry_to_[table] ancestry:string
|
20
|
+
- Add index to migration: add_index [table], :ancestry / remove_index [table], :ancestry
|
21
|
+
- Migrate your database: rake db:migrate
|
22
|
+
|
23
|
+
3. Add ancestry to your model
|
24
|
+
- Add to app/models/[model].rb: acts_as_tree
|
25
|
+
|
26
|
+
Your model is now a tree!
|
27
|
+
|
28
|
+
= Organising Records Into A Tree
|
29
|
+
|
30
|
+
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:
|
31
|
+
|
32
|
+
TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
|
33
|
+
|
34
|
+
You can also create children through the children relation on a node:
|
35
|
+
|
36
|
+
node.children.create :name => 'Stinky'
|
37
|
+
|
38
|
+
= Navigating Your Tree
|
39
|
+
|
40
|
+
To navigate an Ancestry model, use the following methods on any instance / record:
|
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
|
62
|
+
|
63
|
+
= (Named) Scopes
|
64
|
+
|
65
|
+
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:
|
66
|
+
|
67
|
+
node.children.exists?(:name => 'Mary')
|
68
|
+
node.subtree.all(:order => :name, :limit => 10).each do; ...; end
|
69
|
+
node.descendants.count
|
70
|
+
|
71
|
+
For convenience, a couple of named scopes are included at the class level:
|
72
|
+
|
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
|
78
|
+
|
79
|
+
Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
|
80
|
+
|
81
|
+
node.children.create
|
82
|
+
node.siblings.create!
|
83
|
+
TestNode.children_of(node_id).new
|
84
|
+
TestNode.siblings_of(node_id).create
|
85
|
+
|
86
|
+
= acts_as_tree Options
|
87
|
+
|
88
|
+
The acts_as_tree methods supports two options:
|
89
|
+
|
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
|
95
|
+
|
96
|
+
= Arrangement
|
97
|
+
|
98
|
+
Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
|
99
|
+
|
100
|
+
{ #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
|
101
|
+
=> { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
|
102
|
+
=> { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
|
103
|
+
=> {}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
The arrange method also works on a scoped class, for example:
|
109
|
+
|
110
|
+
TreeNode.find_by_name('Crunchy').subtree.arrange
|
111
|
+
|
112
|
+
= Integrity Checking and Restoration
|
113
|
+
|
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.
|
115
|
+
|
116
|
+
For example, from IRB:
|
117
|
+
|
118
|
+
>> stinky = TreeNode.create :name => 'Stinky'
|
119
|
+
$ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
|
120
|
+
>> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
|
121
|
+
$ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
|
122
|
+
>> stinky.update_attribute :parent, squeeky
|
123
|
+
$ true
|
124
|
+
>> TreeNode.all
|
125
|
+
$ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
|
126
|
+
>> TreeNode.check_ancestry_integrity
|
127
|
+
!! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
|
128
|
+
>> TreeNode.restore_ancestry_integrity
|
129
|
+
$ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
|
130
|
+
|
131
|
+
= Testing
|
132
|
+
|
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'.
|
134
|
+
|
135
|
+
= Internals
|
136
|
+
|
137
|
+
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
|
+
|
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.
|
140
|
+
|
141
|
+
= Future Work
|
142
|
+
|
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
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
Copyright (c) 2009 Stefan Kroes, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the ancestry plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Generate documentation for the ancestry plugin.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'Ancestry'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
data/ancestry.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'ancestry'
|
5
|
+
s.description = 'Organise ActiveRecord model into a tree structure'
|
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
|
+
|
8
|
+
s.version = '1.0.0'
|
9
|
+
s.date = '2009-10-16'
|
10
|
+
|
11
|
+
s.author = 'Stefan Kroes'
|
12
|
+
s.email = 's.a.kroes@gmail.com'
|
13
|
+
s.homepage = 'http://github.com/stefankroes/ancestry'
|
14
|
+
|
15
|
+
s.files = FileList['ancestry.gemspec', '*.rb', 'lib/**/*.rb', 'rails/*', 'test/*', 'Rakefile', 'MIT-LICENSE', 'README.rdoc']
|
16
|
+
|
17
|
+
s.add_dependency 'activerecord', '>= 2.1.0'
|
18
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ancestry'
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
data/lib/ancestry.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ancestry/acts_as_tree'
|
@@ -0,0 +1,302 @@
|
|
1
|
+
module Ancestry
|
2
|
+
class AncestryException < RuntimeError
|
3
|
+
end
|
4
|
+
|
5
|
+
class AncestryIntegrityException < AncestryException
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.included base
|
9
|
+
base.send :extend, ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def acts_as_tree options = {}
|
14
|
+
# Include instance methods
|
15
|
+
send :include, InstanceMethods
|
16
|
+
|
17
|
+
# Include dynamic class methods
|
18
|
+
send :extend, DynamicClassMethods
|
19
|
+
|
20
|
+
# Create ancestry column accessor and set to option or default
|
21
|
+
self.cattr_accessor :ancestry_column
|
22
|
+
self.ancestry_column = options[:ancestry_column] || :ancestry
|
23
|
+
|
24
|
+
# Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
|
25
|
+
self.cattr_reader :orphan_strategy
|
26
|
+
self.orphan_strategy = options[:orphan_strategy] || :destroy
|
27
|
+
|
28
|
+
# Validate format of ancestry column value
|
29
|
+
validates_format_of ancestry_column, :with => /^[0-9]+(\/[0-9]+)*$/, :allow_nil => true
|
30
|
+
|
31
|
+
# Validate that the ancestor ids don't include own id
|
32
|
+
validate :ancestry_exclude_self
|
33
|
+
|
34
|
+
# Named scopes
|
35
|
+
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} }
|
40
|
+
|
41
|
+
# Update descendants with new ancestry before save
|
42
|
+
before_save :update_descendants_with_new_ancestry
|
43
|
+
|
44
|
+
# Apply orphan strategy before destroy
|
45
|
+
before_destroy :apply_orphan_strategy
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module DynamicClassMethods
|
50
|
+
# Fetch tree node if necessary
|
51
|
+
def to_node object
|
52
|
+
object.is_a?(self) ? object : find(object)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Orhpan strategy writer
|
56
|
+
def orphan_strategy= orphan_strategy
|
57
|
+
# Check value of orphan strategy, only rootify, restrict or destroy is allowed
|
58
|
+
if [:rootify, :restrict, :destroy].include? orphan_strategy
|
59
|
+
class_variable_set :@@orphan_strategy, orphan_strategy
|
60
|
+
else
|
61
|
+
raise AncestryException.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Arrangement
|
66
|
+
def arrange
|
67
|
+
# Get all nodes ordered by ancestry and start sorting them into an empty hash
|
68
|
+
all(:order => ancestry_column).inject({}) do |arranged_nodes, node|
|
69
|
+
# Find the insertion point for that node by going through its ancestors
|
70
|
+
node.ancestor_ids.inject(arranged_nodes) do |insertion_point, ancestor_id|
|
71
|
+
insertion_point.each do |parent, children|
|
72
|
+
# Change the insertion point to children if node is a descendant of this parent
|
73
|
+
insertion_point = children if ancestor_id == parent.id
|
74
|
+
end; insertion_point
|
75
|
+
end[node] = {}; arranged_nodes
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Integrity checking
|
80
|
+
def check_ancestry_integrity
|
81
|
+
parents = {}
|
82
|
+
# For each node ...
|
83
|
+
all.each do |node|
|
84
|
+
# ... check validity of ancestry column
|
85
|
+
if node.errors.invalid? node.class.ancestry_column
|
86
|
+
raise AncestryIntegrityException.new "Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}."
|
87
|
+
end
|
88
|
+
# ... check that all ancestors exist
|
89
|
+
node.ancestor_ids.each do |node_id|
|
90
|
+
unless exists? node_id
|
91
|
+
raise AncestryIntegrityException.new "Reference to non-existent node in node #{node.id}: #{node_id}."
|
92
|
+
end
|
93
|
+
end
|
94
|
+
# ... check that all node parents are consistent with values observed earlier
|
95
|
+
node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
|
96
|
+
parents[node_id] = parent_id unless parents.has_key? node_id
|
97
|
+
unless parents[node_id] == parent_id
|
98
|
+
raise AncestryIntegrityException.new "Conflicting parent id in node #{node.id}: #{parent_id || 'nil'} for node #{node_id}, expecting #{parents[node_id] || 'nil'}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Integrity restoration
|
105
|
+
def restore_ancestry_integrity
|
106
|
+
parents = {}
|
107
|
+
# For each node ...
|
108
|
+
all.each do |node|
|
109
|
+
# ... set its ancestry to nil if invalid
|
110
|
+
if node.errors.invalid? node.class.ancestry_column
|
111
|
+
node.update_attributes :ancestry => nil
|
112
|
+
end
|
113
|
+
# ... save parent of this node in parents array if it exists
|
114
|
+
parents[node.id] = node.parent_id if exists? node.parent_id
|
115
|
+
|
116
|
+
# Reset parent id in array to nil if it introduces a cycle
|
117
|
+
parent = parents[node.id]
|
118
|
+
until parent.nil? || parent == node.id
|
119
|
+
parent = parents[parent]
|
120
|
+
end
|
121
|
+
parents[node.id] = nil if parent == node.id
|
122
|
+
end
|
123
|
+
# For each node ...
|
124
|
+
all.each do |node|
|
125
|
+
# ... rebuild ancestry from parents array
|
126
|
+
ancestry, parent = nil, parents[node.id]
|
127
|
+
until parent.nil?
|
128
|
+
ancestry, parent = ancestry.nil? ? parent : "#{parent}/#{ancestry}", parents[parent]
|
129
|
+
end
|
130
|
+
node.update_attributes node.ancestry_column => ancestry
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
module InstanceMethods
|
136
|
+
# Validate that the ancestors don't include itself
|
137
|
+
def ancestry_exclude_self
|
138
|
+
errors.add_to_base "#{self.class.name.humanize} cannot be a descendant of itself." if ancestor_ids.include? self.id
|
139
|
+
end
|
140
|
+
|
141
|
+
# Update descendants with new ancestry
|
142
|
+
def update_descendants_with_new_ancestry
|
143
|
+
# If node is valid, not a new record and ancestry was updated ...
|
144
|
+
if changed.include?(self.class.ancestry_column.to_s) && !new_record? && valid?
|
145
|
+
# ... for each descendant ...
|
146
|
+
descendants.each do |descendant|
|
147
|
+
# ... replace old ancestry with new ancestry
|
148
|
+
descendant.update_attributes(
|
149
|
+
self.class.ancestry_column =>
|
150
|
+
descendant.read_attribute(descendant.class.ancestry_column).gsub(
|
151
|
+
/^#{self.child_ancestry}/,
|
152
|
+
(read_attribute(self.class.ancestry_column).blank? ? id.to_s : "#{read_attribute self.class.ancestry_column }/#{id}")
|
153
|
+
)
|
154
|
+
)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Apply orphan strategy
|
160
|
+
def apply_orphan_strategy
|
161
|
+
# If this isn't a new record ...
|
162
|
+
unless new_record?
|
163
|
+
# ... make al children root if orphan strategy is rootify
|
164
|
+
if self.class.orphan_strategy == :rootify
|
165
|
+
descendants.each do |descendant|
|
166
|
+
descendant.update_attributes descendant.class.ancestry_column => descendant.ancestry == child_ancestry ? nil : descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
|
167
|
+
end
|
168
|
+
# ... destroy all descendants if orphan strategy is destroy
|
169
|
+
elsif self.class.orphan_strategy == :destroy
|
170
|
+
self.class.destroy_all descendant_conditions
|
171
|
+
# ... throw an exception if it has children and orphan strategy is restrict
|
172
|
+
elsif self.class.orphan_strategy == :restrict
|
173
|
+
raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# The ancestry value for this record's children
|
179
|
+
def child_ancestry
|
180
|
+
# New records cannot have children
|
181
|
+
raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
|
182
|
+
|
183
|
+
self.send("#{self.class.ancestry_column}_was").blank? ? id.to_s : "#{self.send "#{self.class.ancestry_column}_was"}/#{id}"
|
184
|
+
end
|
185
|
+
|
186
|
+
# Ancestors
|
187
|
+
def ancestor_ids
|
188
|
+
read_attribute(self.class.ancestry_column).to_s.split('/').map(&:to_i)
|
189
|
+
end
|
190
|
+
|
191
|
+
def ancestor_conditions
|
192
|
+
{:id => ancestor_ids}
|
193
|
+
end
|
194
|
+
|
195
|
+
def ancestors
|
196
|
+
self.class.scoped :conditions => ancestor_conditions
|
197
|
+
end
|
198
|
+
|
199
|
+
def path_ids
|
200
|
+
ancestor_ids + [id]
|
201
|
+
end
|
202
|
+
|
203
|
+
def path
|
204
|
+
ancestors + [self]
|
205
|
+
end
|
206
|
+
|
207
|
+
# Parent
|
208
|
+
def parent= parent
|
209
|
+
write_attribute(self.class.ancestry_column, parent.blank? ? nil : parent.child_ancestry)
|
210
|
+
end
|
211
|
+
|
212
|
+
def parent_id= parent_id
|
213
|
+
self.parent = parent_id.blank? ? nil : self.class.find(parent_id)
|
214
|
+
end
|
215
|
+
|
216
|
+
def parent_id
|
217
|
+
ancestor_ids.empty? ? nil : ancestor_ids.last
|
218
|
+
end
|
219
|
+
|
220
|
+
def parent
|
221
|
+
parent_id.blank? ? nil : self.class.find(parent_id)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Root
|
225
|
+
def root_id
|
226
|
+
ancestor_ids.empty? ? id : ancestor_ids.first
|
227
|
+
end
|
228
|
+
|
229
|
+
def root
|
230
|
+
root_id == id ? self : self.class.find(root_id)
|
231
|
+
end
|
232
|
+
|
233
|
+
def is_root?
|
234
|
+
read_attribute(self.class.ancestry_column).blank?
|
235
|
+
end
|
236
|
+
|
237
|
+
# Children
|
238
|
+
def child_conditions
|
239
|
+
{self.class.ancestry_column => child_ancestry}
|
240
|
+
end
|
241
|
+
|
242
|
+
def children
|
243
|
+
self.class.scoped :conditions => child_conditions
|
244
|
+
end
|
245
|
+
|
246
|
+
def child_ids
|
247
|
+
children.all(:select => :id).map(&:id)
|
248
|
+
end
|
249
|
+
|
250
|
+
def has_children?
|
251
|
+
self.children.exists?
|
252
|
+
end
|
253
|
+
|
254
|
+
def is_childless?
|
255
|
+
!has_children?
|
256
|
+
end
|
257
|
+
|
258
|
+
# Siblings
|
259
|
+
def sibling_conditions
|
260
|
+
{self.class.ancestry_column => read_attribute(self.class.ancestry_column)}
|
261
|
+
end
|
262
|
+
|
263
|
+
def siblings
|
264
|
+
self.class.scoped :conditions => sibling_conditions
|
265
|
+
end
|
266
|
+
|
267
|
+
def sibling_ids
|
268
|
+
siblings.all(:select => :id).collect(&:id)
|
269
|
+
end
|
270
|
+
|
271
|
+
def has_siblings?
|
272
|
+
self.siblings.count > 1
|
273
|
+
end
|
274
|
+
|
275
|
+
def is_only_child?
|
276
|
+
!has_siblings?
|
277
|
+
end
|
278
|
+
|
279
|
+
# Descendants
|
280
|
+
def descendant_conditions
|
281
|
+
["#{self.class.ancestry_column} like ? or #{self.class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
|
282
|
+
end
|
283
|
+
|
284
|
+
def descendants
|
285
|
+
self.class.scoped :conditions => descendant_conditions
|
286
|
+
end
|
287
|
+
|
288
|
+
def descendant_ids
|
289
|
+
descendants.all(:select => :id).collect(&:id)
|
290
|
+
end
|
291
|
+
|
292
|
+
def subtree
|
293
|
+
[self] + descendants
|
294
|
+
end
|
295
|
+
|
296
|
+
def subtree_ids
|
297
|
+
[self.id] + descendant_ids
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
ActiveRecord::Base.send :include, Ancestry
|