ancestry 1.3.0 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +23 -4
- data/ancestry.gemspec +2 -2
- data/lib/ancestry.rb +4 -0
- data/lib/ancestry/class_methods.rb +37 -23
- data/lib/ancestry/has_ancestry.rb +15 -22
- data/lib/ancestry/instance_methods.rb +68 -43
- metadata +40 -51
data/README.rdoc
CHANGED
@@ -8,8 +8,7 @@ To apply Ancestry to any ActiveRecord model, follow these simple steps:
|
|
8
8
|
|
9
9
|
1. Install
|
10
10
|
- <b>Rails 2</b>
|
11
|
-
-
|
12
|
-
- Install required gems: <b>sudo rake gems:install</b>
|
11
|
+
- See 1-3-stable branch
|
13
12
|
- <b>Rails 3</b>
|
14
13
|
- Add to Gemfile: <b>gem 'ancestry'</b>
|
15
14
|
- Install required gems: <b>bundle install</b>
|
@@ -46,7 +45,7 @@ To navigate an Ancestry model, use the following methods on any instance / recor
|
|
46
45
|
parent_id Returns the id of the parent of the record, nil for a root node
|
47
46
|
root Returns the root of the tree the record is in, self for a root node
|
48
47
|
root_id Returns the id of the root of the tree the record is in
|
49
|
-
is_root?
|
48
|
+
root?, is_root? Returns true if the record is a root node, false otherwise
|
50
49
|
ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
|
51
50
|
ancestors Scopes the model on ancestors of the record
|
52
51
|
path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
|
@@ -74,6 +73,7 @@ The has_ancestry methods supports the following options:
|
|
74
73
|
:destroy All children are destroyed as well (default)
|
75
74
|
:rootify The children of the destroyed node become root nodes
|
76
75
|
:restrict An AncestryException is raised if any children exist
|
76
|
+
:adopt The orphan subtree is added to the parent of the deleted node.If the deleted node is Root, then rootify the orphan subtree.
|
77
77
|
:cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
|
78
78
|
If you turn depth_caching on for an existing model:
|
79
79
|
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
|
@@ -224,7 +224,18 @@ Additionally, if you think something is wrong with your depth cache:
|
|
224
224
|
|
225
225
|
= Tests
|
226
226
|
|
227
|
-
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
|
227
|
+
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:
|
228
|
+
- check out the repository from GitHub
|
229
|
+
- copy test/database.example.yml to test/database.yml
|
230
|
+
- run <tt>bundle</tt>
|
231
|
+
- run <tt>rake [test]</tt>
|
232
|
+
|
233
|
+
You can pass an environment variable for the database to test against (e.g. db=mysql). By default, the test suite runs against the latest activerecord version. You can run agains activerecord 3-0 or 3-1 as follows:
|
234
|
+
- run <tt>bundle --gemfile Gemfile.rails-<version></tt>
|
235
|
+
- run <tt>rake [test] BUNDLE_GEMFILE=Gemfile.rails-<version></tt>
|
236
|
+
|
237
|
+
To run the test suite multiple times against several databases and all supported activerecord versions, run <tt>rake test_all</tt>
|
238
|
+
The test suite is located in test/has_ancestry_test.rb.
|
228
239
|
|
229
240
|
= Internals
|
230
241
|
|
@@ -238,6 +249,14 @@ The materialised path pattern requires Ancestry to use a 'like' condition in ord
|
|
238
249
|
|
239
250
|
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.
|
240
251
|
|
252
|
+
- Version 2.0.0.rc1 (2013-05-07)
|
253
|
+
- Removed rails 2 compatibility
|
254
|
+
- Fixed deprecation warnings for rails 4
|
255
|
+
- New adopt strategy
|
256
|
+
- Many more fixes & minor changes
|
257
|
+
- Version 1.3.0 (2012-05-04)
|
258
|
+
- Ancestry now ignores default scopes when moving or destroying nodes, ensuring tree consistency
|
259
|
+
- Changed ActiveRecord dependency to 2.3.14
|
241
260
|
- Version 1.2.5 (2012-03-15)
|
242
261
|
- Fixed warnings: "parenthesize argument(s) for future version"
|
243
262
|
- Fixed a bug in the restore_ancestry_integrity! method (thx Arthur Holstvoogd)
|
data/ancestry.gemspec
CHANGED
@@ -3,7 +3,7 @@ Gem::Specification.new do |s|
|
|
3
3
|
s.description = 'Organise ActiveRecord model into a tree structure'
|
4
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
5
|
|
6
|
-
s.version = '
|
6
|
+
s.version = '2.0.0.rc1'
|
7
7
|
|
8
8
|
s.author = 'Stefan Kroes'
|
9
9
|
s.email = 's.a.kroes@gmail.com'
|
@@ -22,5 +22,5 @@ Gem::Specification.new do |s|
|
|
22
22
|
'README.rdoc'
|
23
23
|
]
|
24
24
|
|
25
|
-
s.add_dependency 'activerecord', '>=
|
25
|
+
s.add_dependency 'activerecord', '>= 3.0.0'
|
26
26
|
end
|
data/lib/ancestry.rb
CHANGED
@@ -2,3 +2,7 @@ require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/class_meth
|
|
2
2
|
require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/instance_methods')
|
3
3
|
require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/exceptions')
|
4
4
|
require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/has_ancestry')
|
5
|
+
|
6
|
+
module Ancestry
|
7
|
+
ANCESTRY_PATTERN = /\A[0-9]+(\/[0-9]+)*\Z/
|
8
|
+
end
|
@@ -2,12 +2,12 @@ module Ancestry
|
|
2
2
|
module ClassMethods
|
3
3
|
# Fetch tree node if necessary
|
4
4
|
def to_node object
|
5
|
-
if object.is_a?(self.
|
5
|
+
if object.is_a?(self.ancestry_base_class) then object else find(object) end
|
6
6
|
end
|
7
7
|
|
8
8
|
# Scope on relative depth options
|
9
9
|
def scope_depth depth_options, depth
|
10
|
-
depth_options.inject(self.
|
10
|
+
depth_options.inject(self.ancestry_base_class) do |scope, option|
|
11
11
|
scope_name, relative_depth = option
|
12
12
|
if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
|
13
13
|
scope.send scope_name, depth + relative_depth
|
@@ -19,11 +19,11 @@ module Ancestry
|
|
19
19
|
|
20
20
|
# Orphan strategy writer
|
21
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
|
22
|
+
# Check value of orphan strategy, only rootify, adopt, restrict or destroy is allowed
|
23
|
+
if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
|
24
24
|
class_variable_set :@@orphan_strategy, orphan_strategy
|
25
25
|
else
|
26
|
-
raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
|
26
|
+
raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify,:adopt, :restrict and :destroy.")
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
@@ -31,12 +31,12 @@ module Ancestry
|
|
31
31
|
def arrange options = {}
|
32
32
|
scope =
|
33
33
|
if options[:order].nil?
|
34
|
-
self.
|
34
|
+
self.ancestry_base_class.ordered_by_ancestry
|
35
35
|
else
|
36
|
-
self.
|
36
|
+
self.ancestry_base_class.ordered_by_ancestry_and options.delete(:order)
|
37
37
|
end
|
38
38
|
# Get all nodes ordered by ancestry and start sorting them into an empty hash
|
39
|
-
arrange_nodes scope.
|
39
|
+
arrange_nodes scope.where(options)
|
40
40
|
end
|
41
41
|
|
42
42
|
# Arrange array of nodes into a nested hash of the form
|
@@ -57,14 +57,28 @@ module Ancestry
|
|
57
57
|
end
|
58
58
|
|
59
59
|
# Pseudo-preordered array of nodes. Children will always follow parents,
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
# for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
|
61
|
+
def sort_by_ancestry(nodes, &block)
|
62
|
+
arranged = nodes if nodes.is_a?(Hash)
|
63
|
+
|
64
|
+
unless arranged
|
65
|
+
presorted_nodes = nodes.sort do |a, b|
|
66
|
+
a_cestry, b_cestry = a.ancestry || '0', b.ancestry || '0'
|
67
|
+
|
68
|
+
if block_given? && a_cestry == b_cestry
|
69
|
+
yield a, b
|
70
|
+
else
|
71
|
+
a_cestry <=> b_cestry
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
arranged = arrange_nodes(presorted_nodes)
|
76
|
+
end
|
77
|
+
|
64
78
|
arranged.inject([]) do |sorted_nodes, pair|
|
65
79
|
node, children = pair
|
66
80
|
sorted_nodes << node
|
67
|
-
sorted_nodes += sort_by_ancestry(children) unless children.blank?
|
81
|
+
sorted_nodes += sort_by_ancestry(children, &block) unless children.blank?
|
68
82
|
sorted_nodes
|
69
83
|
end
|
70
84
|
end
|
@@ -74,9 +88,9 @@ module Ancestry
|
|
74
88
|
parents = {}
|
75
89
|
exceptions = [] if options[:report] == :list
|
76
90
|
|
77
|
-
self.
|
91
|
+
self.ancestry_base_class.unscoped do
|
78
92
|
# For each node ...
|
79
|
-
self.
|
93
|
+
self.ancestry_base_class.find_each do |node|
|
80
94
|
begin
|
81
95
|
# ... check validity of ancestry column
|
82
96
|
if !node.valid? and !node.errors[node.class.ancestry_column].blank?
|
@@ -111,10 +125,10 @@ module Ancestry
|
|
111
125
|
def restore_ancestry_integrity!
|
112
126
|
parents = {}
|
113
127
|
# Wrap the whole thing in a transaction ...
|
114
|
-
self.
|
115
|
-
self.
|
128
|
+
self.ancestry_base_class.transaction do
|
129
|
+
self.ancestry_base_class.unscoped do
|
116
130
|
# For each node ...
|
117
|
-
self.
|
131
|
+
self.ancestry_base_class.find_each do |node|
|
118
132
|
# ... set its ancestry to nil if invalid
|
119
133
|
if !node.valid? and !node.errors[node.class.ancestry_column].blank?
|
120
134
|
node.without_ancestry_callbacks do
|
@@ -133,7 +147,7 @@ module Ancestry
|
|
133
147
|
end
|
134
148
|
|
135
149
|
# For each node ...
|
136
|
-
self.
|
150
|
+
self.ancestry_base_class.find_each do |node|
|
137
151
|
# ... rebuild ancestry from parents array
|
138
152
|
ancestry, parent = nil, parents[node.id]
|
139
153
|
until parent.nil?
|
@@ -149,8 +163,8 @@ module Ancestry
|
|
149
163
|
|
150
164
|
# Build ancestry from parent id's for migration purposes
|
151
165
|
def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
|
152
|
-
self.
|
153
|
-
self.
|
166
|
+
self.ancestry_base_class.unscoped do
|
167
|
+
self.ancestry_base_class.where(:parent_id => parent_id).find_each do |node|
|
154
168
|
node.without_ancestry_callbacks do
|
155
169
|
node.update_attribute ancestry_column, ancestry
|
156
170
|
end
|
@@ -163,8 +177,8 @@ module Ancestry
|
|
163
177
|
def rebuild_depth_cache!
|
164
178
|
raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
|
165
179
|
|
166
|
-
self.
|
167
|
-
self.
|
180
|
+
self.ancestry_base_class.unscoped do
|
181
|
+
self.ancestry_base_class.find_each do |node|
|
168
182
|
node.update_attribute depth_cache_column, node.depth
|
169
183
|
end
|
170
184
|
end
|
@@ -3,7 +3,7 @@ class << ActiveRecord::Base
|
|
3
3
|
# Check options
|
4
4
|
raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
|
5
5
|
options.each do |key, value|
|
6
|
-
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column
|
6
|
+
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column].include? key
|
7
7
|
raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
|
8
8
|
end
|
9
9
|
end
|
@@ -23,32 +23,24 @@ class << ActiveRecord::Base
|
|
23
23
|
self.orphan_strategy = options[:orphan_strategy] || :destroy
|
24
24
|
|
25
25
|
# Save self as base class (for STI)
|
26
|
-
cattr_accessor :
|
27
|
-
self.
|
26
|
+
cattr_accessor :ancestry_base_class
|
27
|
+
self.ancestry_base_class = self
|
28
28
|
|
29
29
|
# Validate format of ancestry column value
|
30
|
-
|
31
|
-
validates_format_of ancestry_column, :with => /\A#{primary_key_format.source}(\/#{primary_key_format.source})*\Z/, :allow_nil => true
|
30
|
+
validates_format_of ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
|
32
31
|
|
33
32
|
# Validate that the ancestor ids don't include own id
|
34
33
|
validate :ancestry_exclude_self
|
35
34
|
|
36
|
-
# Save ActiveRecord version
|
37
|
-
self.cattr_accessor :rails_3
|
38
|
-
self.rails_3 = defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 3
|
39
|
-
|
40
|
-
# Workaround to support Rails 2
|
41
|
-
scope_method = if rails_3 then :scope else :named_scope end
|
42
|
-
|
43
35
|
# Named scopes
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
36
|
+
scope :roots, lambda { where(ancestry_column => nil) }
|
37
|
+
scope :ancestors_of, lambda { |object| where(to_node(object).ancestor_conditions) }
|
38
|
+
scope :children_of, lambda { |object| where(to_node(object).child_conditions) }
|
39
|
+
scope :descendants_of, lambda { |object| where(to_node(object).descendant_conditions) }
|
40
|
+
scope :subtree_of, lambda { |object| where(to_node(object).subtree_conditions) }
|
41
|
+
scope :siblings_of, lambda { |object| where(to_node(object).sibling_conditions) }
|
42
|
+
scope :ordered_by_ancestry, lambda { reorder("(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}") }
|
43
|
+
scope :ordered_by_ancestry_and, lambda { |order| reorder("(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}, #{order}") }
|
52
44
|
|
53
45
|
# Update descendants with new ancestry before save
|
54
46
|
before_save :update_descendants_with_new_ancestry
|
@@ -64,6 +56,7 @@ class << ActiveRecord::Base
|
|
64
56
|
|
65
57
|
# Cache depth in depth cache column before save
|
66
58
|
before_validation :cache_depth
|
59
|
+
before_save :cache_depth
|
67
60
|
|
68
61
|
# Validate depth column
|
69
62
|
validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
|
@@ -71,9 +64,9 @@ class << ActiveRecord::Base
|
|
71
64
|
|
72
65
|
# Create named scopes for depth
|
73
66
|
{:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
|
74
|
-
|
67
|
+
scope scope_name, lambda { |depth|
|
75
68
|
raise Ancestry::AncestryException.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless options[:cache_depth]
|
76
|
-
|
69
|
+
where("#{depth_cache_column} #{operator} ?", depth)
|
77
70
|
}
|
78
71
|
end
|
79
72
|
end
|
@@ -2,21 +2,21 @@ module Ancestry
|
|
2
2
|
module InstanceMethods
|
3
3
|
# Validate that the ancestors don't include itself
|
4
4
|
def ancestry_exclude_self
|
5
|
-
|
5
|
+
errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") if ancestor_ids.include? self.id
|
6
6
|
end
|
7
7
|
|
8
8
|
# Update descendants with new ancestry
|
9
9
|
def update_descendants_with_new_ancestry
|
10
10
|
# Skip this if callbacks are disabled
|
11
11
|
unless ancestry_callbacks_disabled?
|
12
|
-
# If node is
|
13
|
-
if
|
12
|
+
# If node is not a new record and ancestry was updated and the new ancestry is sane ...
|
13
|
+
if ancestry_changed? && !new_record? && sane_ancestry?
|
14
14
|
# ... for each descendant ...
|
15
15
|
unscoped_descendants.each do |descendant|
|
16
16
|
# ... replace old ancestry with new ancestry
|
17
17
|
descendant.without_ancestry_callbacks do
|
18
18
|
descendant.update_attribute(
|
19
|
-
self.
|
19
|
+
self.ancestry_base_class.ancestry_column,
|
20
20
|
descendant.read_attribute(descendant.class.ancestry_column).gsub(
|
21
21
|
/^#{self.child_ancestry}/,
|
22
22
|
if read_attribute(self.class.ancestry_column).blank? then id.to_s else "#{read_attribute self.class.ancestry_column }/#{id}" end
|
@@ -35,21 +35,29 @@ module Ancestry
|
|
35
35
|
# If this isn't a new record ...
|
36
36
|
unless new_record?
|
37
37
|
# ... make all children root if orphan strategy is rootify
|
38
|
-
if self.
|
38
|
+
if self.ancestry_base_class.orphan_strategy == :rootify
|
39
39
|
unscoped_descendants.each do |descendant|
|
40
40
|
descendant.without_ancestry_callbacks do
|
41
41
|
descendant.update_attribute descendant.class.ancestry_column, (if descendant.ancestry == child_ancestry then nil else descendant.ancestry.gsub(/^#{child_ancestry}\//, '') end)
|
42
42
|
end
|
43
43
|
end
|
44
44
|
# ... destroy all descendants if orphan strategy is destroy
|
45
|
-
elsif self.
|
45
|
+
elsif self.ancestry_base_class.orphan_strategy == :destroy
|
46
46
|
unscoped_descendants.each do |descendant|
|
47
47
|
descendant.without_ancestry_callbacks do
|
48
48
|
descendant.destroy
|
49
49
|
end
|
50
50
|
end
|
51
|
+
# ... make child elements of this node, child of its parent if orphan strategy is adopt
|
52
|
+
elsif self.ancestry_base_class.orphan_strategy == :adopt
|
53
|
+
descendants.each do |descendant|
|
54
|
+
descendant.without_ancestry_callbacks do
|
55
|
+
new_ancestry = descendant.ancestor_ids.delete_if { |x| x == self.id }.join("/")
|
56
|
+
descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
|
57
|
+
end
|
58
|
+
end
|
51
59
|
# ... throw an exception if it has children and orphan strategy is restrict
|
52
|
-
elsif self.
|
60
|
+
elsif self.ancestry_base_class.orphan_strategy == :restrict
|
53
61
|
raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
|
54
62
|
end
|
55
63
|
end
|
@@ -61,20 +69,24 @@ module Ancestry
|
|
61
69
|
# New records cannot have children
|
62
70
|
raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
|
63
71
|
|
64
|
-
if self.send("#{self.
|
72
|
+
if self.send("#{self.ancestry_base_class.ancestry_column}_was").blank? then id.to_s else "#{self.send "#{self.ancestry_base_class.ancestry_column}_was"}/#{id}" end
|
65
73
|
end
|
66
74
|
|
67
75
|
# Ancestors
|
76
|
+
def ancestry_changed?
|
77
|
+
changed.include?(self.ancestry_base_class.ancestry_column.to_s)
|
78
|
+
end
|
79
|
+
|
68
80
|
def ancestor_ids
|
69
|
-
read_attribute(self.
|
81
|
+
read_attribute(self.ancestry_base_class.ancestry_column).to_s.split('/').map { |id| cast_primary_key(id) }
|
70
82
|
end
|
71
83
|
|
72
84
|
def ancestor_conditions
|
73
|
-
{
|
85
|
+
{primary_key_with_table => ancestor_ids}
|
74
86
|
end
|
75
87
|
|
76
88
|
def ancestors depth_options = {}
|
77
|
-
self.
|
89
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
|
78
90
|
end
|
79
91
|
|
80
92
|
def path_ids
|
@@ -82,11 +94,11 @@ module Ancestry
|
|
82
94
|
end
|
83
95
|
|
84
96
|
def path_conditions
|
85
|
-
{
|
97
|
+
{primary_key_with_table => path_ids}
|
86
98
|
end
|
87
99
|
|
88
100
|
def path depth_options = {}
|
89
|
-
self.
|
101
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
|
90
102
|
end
|
91
103
|
|
92
104
|
def depth
|
@@ -94,16 +106,16 @@ module Ancestry
|
|
94
106
|
end
|
95
107
|
|
96
108
|
def cache_depth
|
97
|
-
write_attribute self.
|
109
|
+
write_attribute self.ancestry_base_class.depth_cache_column, depth
|
98
110
|
end
|
99
111
|
|
100
112
|
# Parent
|
101
113
|
def parent= parent
|
102
|
-
write_attribute(self.
|
114
|
+
write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
|
103
115
|
end
|
104
116
|
|
105
117
|
def parent_id= parent_id
|
106
|
-
self.parent = if parent_id.blank? then nil else
|
118
|
+
self.parent = if parent_id.blank? then nil else unscoped_find(parent_id) end
|
107
119
|
end
|
108
120
|
|
109
121
|
def parent_id
|
@@ -111,7 +123,11 @@ module Ancestry
|
|
111
123
|
end
|
112
124
|
|
113
125
|
def parent
|
114
|
-
if parent_id.blank? then nil else
|
126
|
+
if parent_id.blank? then nil else unscoped_find(parent_id) end
|
127
|
+
end
|
128
|
+
|
129
|
+
def parent_id?
|
130
|
+
parent_id.present?
|
115
131
|
end
|
116
132
|
|
117
133
|
# Root
|
@@ -120,24 +136,25 @@ module Ancestry
|
|
120
136
|
end
|
121
137
|
|
122
138
|
def root
|
123
|
-
if root_id == id then self else
|
139
|
+
if root_id == id then self else unscoped_find(root_id) end
|
124
140
|
end
|
125
141
|
|
126
142
|
def is_root?
|
127
|
-
read_attribute(self.
|
143
|
+
read_attribute(self.ancestry_base_class.ancestry_column).blank?
|
128
144
|
end
|
145
|
+
alias :root? :is_root?
|
129
146
|
|
130
147
|
# Children
|
131
148
|
def child_conditions
|
132
|
-
{
|
149
|
+
{ancestry_column_with_table => child_ancestry}
|
133
150
|
end
|
134
151
|
|
135
152
|
def children
|
136
|
-
self.
|
153
|
+
self.ancestry_base_class.where child_conditions
|
137
154
|
end
|
138
155
|
|
139
156
|
def child_ids
|
140
|
-
children.
|
157
|
+
children.select(self.ancestry_base_class.primary_key).map(&self.ancestry_base_class.primary_key.to_sym)
|
141
158
|
end
|
142
159
|
|
143
160
|
def has_children?
|
@@ -150,15 +167,15 @@ module Ancestry
|
|
150
167
|
|
151
168
|
# Siblings
|
152
169
|
def sibling_conditions
|
153
|
-
{
|
170
|
+
{ancestry_column_with_table => read_attribute(self.ancestry_base_class.ancestry_column)}
|
154
171
|
end
|
155
172
|
|
156
173
|
def siblings
|
157
|
-
self.
|
174
|
+
self.ancestry_base_class.where sibling_conditions
|
158
175
|
end
|
159
176
|
|
160
177
|
def sibling_ids
|
161
|
-
|
178
|
+
siblings.select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
|
162
179
|
end
|
163
180
|
|
164
181
|
def has_siblings?
|
@@ -171,28 +188,28 @@ module Ancestry
|
|
171
188
|
|
172
189
|
# Descendants
|
173
190
|
def descendant_conditions
|
174
|
-
["#{
|
191
|
+
["#{ancestry_column_with_table} like ? or #{ancestry_column_with_table} = ?", "#{child_ancestry}/%", child_ancestry]
|
175
192
|
end
|
176
193
|
|
177
194
|
def descendants depth_options = {}
|
178
|
-
self.
|
195
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
|
179
196
|
end
|
180
197
|
|
181
198
|
def descendant_ids depth_options = {}
|
182
|
-
descendants(depth_options).
|
199
|
+
descendants(depth_options).select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
|
183
200
|
end
|
184
201
|
|
185
202
|
# Subtree
|
186
203
|
def subtree_conditions
|
187
|
-
["#{
|
204
|
+
["#{primary_key_with_table} = ? or #{ancestry_column_with_table} like ? or #{ancestry_column_with_table} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
|
188
205
|
end
|
189
206
|
|
190
207
|
def subtree depth_options = {}
|
191
|
-
self.
|
208
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
|
192
209
|
end
|
193
210
|
|
194
211
|
def subtree_ids depth_options = {}
|
195
|
-
subtree(depth_options).
|
212
|
+
subtree(depth_options).select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
|
196
213
|
end
|
197
214
|
|
198
215
|
# Callback disabling
|
@@ -208,15 +225,6 @@ module Ancestry
|
|
208
225
|
|
209
226
|
private
|
210
227
|
|
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
228
|
def cast_primary_key(key)
|
221
229
|
if primary_key_type == :string
|
222
230
|
key
|
@@ -228,11 +236,28 @@ module Ancestry
|
|
228
236
|
def primary_key_type
|
229
237
|
@primary_key_type ||= column_for_attribute(self.class.primary_key).type
|
230
238
|
end
|
231
|
-
|
232
239
|
def unscoped_descendants
|
233
|
-
self.
|
234
|
-
self.
|
240
|
+
self.ancestry_base_class.unscoped do
|
241
|
+
self.ancestry_base_class.where descendant_conditions
|
235
242
|
end
|
236
243
|
end
|
244
|
+
|
245
|
+
# basically validates the ancestry, but also applied if validation is
|
246
|
+
# bypassed to determine if chidren should be affected
|
247
|
+
def sane_ancestry?
|
248
|
+
ancestry.nil? || (ancestry.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
|
249
|
+
end
|
250
|
+
|
251
|
+
def unscoped_find id
|
252
|
+
self.ancestry_base_class.unscoped { self.ancestry_base_class.find(id) }
|
253
|
+
end
|
254
|
+
|
255
|
+
def primary_key_with_table
|
256
|
+
"#{self.ancestry_base_class.table_name}.#{self.ancestry_base_class.primary_key}"
|
257
|
+
end
|
258
|
+
|
259
|
+
def ancestry_column_with_table
|
260
|
+
"#{self.ancestry_base_class.table_name}.#{self.ancestry_base_class.ancestry_column}"
|
261
|
+
end
|
237
262
|
end
|
238
263
|
end
|
metadata
CHANGED
@@ -1,46 +1,38 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: ancestry
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
- 1
|
7
|
-
- 3
|
8
|
-
- 0
|
9
|
-
version: 1.3.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0.rc1
|
5
|
+
prerelease: 6
|
10
6
|
platform: ruby
|
11
|
-
authors:
|
7
|
+
authors:
|
12
8
|
- Stefan Kroes
|
13
9
|
autorequire:
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
dependencies:
|
20
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2013-05-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
21
15
|
name: activerecord
|
22
|
-
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
24
17
|
none: false
|
25
|
-
requirements:
|
26
|
-
- -
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
|
29
|
-
- 2
|
30
|
-
- 3
|
31
|
-
- 14
|
32
|
-
version: 2.3.14
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
33
22
|
type: :runtime
|
34
|
-
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
35
30
|
description: Organise ActiveRecord model into a tree structure
|
36
31
|
email: s.a.kroes@gmail.com
|
37
32
|
executables: []
|
38
|
-
|
39
33
|
extensions: []
|
40
|
-
|
41
34
|
extra_rdoc_files: []
|
42
|
-
|
43
|
-
files:
|
35
|
+
files:
|
44
36
|
- ancestry.gemspec
|
45
37
|
- init.rb
|
46
38
|
- install.rb
|
@@ -51,37 +43,34 @@ files:
|
|
51
43
|
- lib/ancestry/instance_methods.rb
|
52
44
|
- MIT-LICENSE
|
53
45
|
- README.rdoc
|
54
|
-
has_rdoc: true
|
55
46
|
homepage: http://github.com/stefankroes/ancestry
|
56
47
|
licenses: []
|
57
|
-
|
58
48
|
post_install_message:
|
59
49
|
rdoc_options: []
|
60
|
-
|
61
|
-
require_paths:
|
50
|
+
require_paths:
|
62
51
|
- lib
|
63
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
53
|
none: false
|
65
|
-
requirements:
|
66
|
-
- -
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
|
69
|
-
|
70
|
-
version: "0"
|
71
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
59
|
none: false
|
73
|
-
requirements:
|
74
|
-
- -
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
|
77
|
-
- 0
|
78
|
-
version: "0"
|
60
|
+
requirements:
|
61
|
+
- - ! '>'
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 1.3.1
|
79
64
|
requirements: []
|
80
|
-
|
81
65
|
rubyforge_project:
|
82
|
-
rubygems_version: 1.
|
66
|
+
rubygems_version: 1.8.25
|
83
67
|
signing_key:
|
84
68
|
specification_version: 3
|
85
|
-
summary: Ancestry allows the records of a ActiveRecord model to be organised in a
|
69
|
+
summary: Ancestry allows the records of a ActiveRecord model to be organised in a
|
70
|
+
tree structure, using a single, intuitively formatted database column. It exposes
|
71
|
+
all the standard tree structure relations (ancestors, parent, root, children, siblings,
|
72
|
+
descendants) and all of them can be fetched in a single sql query. Additional features
|
73
|
+
are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree
|
74
|
+
into hashes and different strategies for dealing with orphaned records.
|
86
75
|
test_files: []
|
87
|
-
|
76
|
+
has_rdoc:
|