ancestry 3.0.5 → 3.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +277 -0
- data/README.md +83 -202
- data/lib/ancestry.rb +28 -1
- data/lib/ancestry/class_methods.rb +55 -31
- data/lib/ancestry/has_ancestry.rb +37 -26
- data/lib/ancestry/instance_methods.rb +67 -102
- data/lib/ancestry/locales/en.yml +16 -0
- data/lib/ancestry/materialized_path.rb +100 -17
- data/lib/ancestry/materialized_path_pg.rb +23 -0
- data/lib/ancestry/version.rb +1 -1
- metadata +25 -52
- data/ancestry.gemspec +0 -53
- data/init.rb +0 -1
- data/install.rb +0 -1
data/lib/ancestry.rb
CHANGED
@@ -4,7 +4,34 @@ require_relative 'ancestry/instance_methods'
|
|
4
4
|
require_relative 'ancestry/exceptions'
|
5
5
|
require_relative 'ancestry/has_ancestry'
|
6
6
|
require_relative 'ancestry/materialized_path'
|
7
|
+
require_relative 'ancestry/materialized_path_pg'
|
8
|
+
|
9
|
+
I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
|
10
|
+
'ancestry', 'locales', '*.{rb,yml}').to_s]
|
7
11
|
|
8
12
|
module Ancestry
|
9
|
-
|
13
|
+
@@default_update_strategy = :ruby
|
14
|
+
|
15
|
+
# @!default_update_strategy
|
16
|
+
# @return [Symbol] the default strategy for updating ancestry
|
17
|
+
#
|
18
|
+
# The value changes the default way that ancestry is updated for associated records
|
19
|
+
#
|
20
|
+
# :ruby (default and legacy value)
|
21
|
+
#
|
22
|
+
# Child records will be loaded into memory and updated. callbacks will get called
|
23
|
+
# The callbacks of interest are those that cache values based upon the ancestry value
|
24
|
+
#
|
25
|
+
# :sql (currently only valid in postgres)
|
26
|
+
#
|
27
|
+
# Child records are updated in sql and callbacks will not get called.
|
28
|
+
# Associated records in memory will have the wrong ancestry value
|
29
|
+
|
30
|
+
def self.default_update_strategy
|
31
|
+
@@default_update_strategy
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.default_update_strategy=(value)
|
35
|
+
@@default_update_strategy = value
|
36
|
+
end
|
10
37
|
end
|
@@ -2,7 +2,11 @@ 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.ancestry_base_class)
|
5
|
+
if object.is_a?(self.ancestry_base_class)
|
6
|
+
object
|
7
|
+
else
|
8
|
+
unscoped_where { |scope| scope.find(object.try(primary_key) || object) }
|
9
|
+
end
|
6
10
|
end
|
7
11
|
|
8
12
|
# Scope on relative depth options
|
@@ -12,7 +16,7 @@ module Ancestry
|
|
12
16
|
if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
|
13
17
|
scope.send scope_name, depth + relative_depth
|
14
18
|
else
|
15
|
-
raise Ancestry::AncestryException.new("
|
19
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_depth_option", {:scope_name => scope_name}))
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -23,11 +27,17 @@ module Ancestry
|
|
23
27
|
if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
|
24
28
|
class_variable_set :@@orphan_strategy, orphan_strategy
|
25
29
|
else
|
26
|
-
raise Ancestry::AncestryException.new("
|
30
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.invalid_orphan_strategy"))
|
27
31
|
end
|
28
32
|
end
|
29
33
|
|
30
|
-
|
34
|
+
|
35
|
+
# these methods arrange an entire subtree into nested hashes for easy navigation after database retrieval
|
36
|
+
# the arrange method also works on a scoped class
|
37
|
+
# the arrange method takes ActiveRecord find options
|
38
|
+
# To order your hashes pass the order to the arrange method instead of to the scope
|
39
|
+
|
40
|
+
# Get all nodes and sort them into an empty hash
|
31
41
|
def arrange options = {}
|
32
42
|
if (order = options.delete(:order))
|
33
43
|
arrange_nodes self.ancestry_base_class.order(order).where(options)
|
@@ -50,7 +60,9 @@ module Ancestry
|
|
50
60
|
end
|
51
61
|
end
|
52
62
|
|
53
|
-
# Arrangement to nested array
|
63
|
+
# Arrangement to nested array for serialization
|
64
|
+
# You can also supply your own serialization logic using blocks
|
65
|
+
# also allows you to pass the order just as you can pass it to the arrange method
|
54
66
|
def arrange_serializable options={}, nodes=nil, &block
|
55
67
|
nodes = arrange(options) if nodes.nil?
|
56
68
|
nodes.map do |parent, children|
|
@@ -63,7 +75,6 @@ module Ancestry
|
|
63
75
|
end
|
64
76
|
|
65
77
|
# Pseudo-preordered array of nodes. Children will always follow parents,
|
66
|
-
# for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
|
67
78
|
def sort_by_ancestry(nodes, &block)
|
68
79
|
arranged = nodes if nodes.is_a?(Hash)
|
69
80
|
|
@@ -90,6 +101,9 @@ module Ancestry
|
|
90
101
|
end
|
91
102
|
|
92
103
|
# Integrity checking
|
104
|
+
# compromised tree integrity is unlikely without explicitly setting cyclic parents or invalid ancestry and circumventing validation
|
105
|
+
# just in case, raise an AncestryIntegrityException if issues are detected
|
106
|
+
# specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages
|
93
107
|
def check_ancestry_integrity! options = {}
|
94
108
|
parents = {}
|
95
109
|
exceptions = [] if options[:report] == :list
|
@@ -99,20 +113,30 @@ module Ancestry
|
|
99
113
|
scope.find_each do |node|
|
100
114
|
begin
|
101
115
|
# ... check validity of ancestry column
|
102
|
-
if !node.
|
103
|
-
raise Ancestry::AncestryIntegrityException.new("
|
116
|
+
if !node.sane_ancestor_ids?
|
117
|
+
raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.invalid_ancestry_column",
|
118
|
+
:node_id => node.id,
|
119
|
+
:ancestry_column => "#{node.read_attribute node.ancestry_column}"
|
120
|
+
))
|
104
121
|
end
|
105
122
|
# ... check that all ancestors exist
|
106
123
|
node.ancestor_ids.each do |ancestor_id|
|
107
124
|
unless exists? ancestor_id
|
108
|
-
raise Ancestry::AncestryIntegrityException.new("
|
125
|
+
raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.reference_nonexistent_node",
|
126
|
+
:node_id => node.id,
|
127
|
+
:ancestor_id => ancestor_id
|
128
|
+
))
|
109
129
|
end
|
110
130
|
end
|
111
131
|
# ... check that all node parents are consistent with values observed earlier
|
112
132
|
node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
|
113
133
|
parents[node_id] = parent_id unless parents.has_key? node_id
|
114
134
|
unless parents[node_id] == parent_id
|
115
|
-
raise Ancestry::AncestryIntegrityException.new("
|
135
|
+
raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.conflicting_parent_id",
|
136
|
+
:node_id => node_id,
|
137
|
+
:parent_id => parent_id || 'nil',
|
138
|
+
:expected => parents[node_id] || 'nil'
|
139
|
+
))
|
116
140
|
end
|
117
141
|
end
|
118
142
|
rescue Ancestry::AncestryIntegrityException => integrity_exception
|
@@ -129,59 +153,59 @@ module Ancestry
|
|
129
153
|
|
130
154
|
# Integrity restoration
|
131
155
|
def restore_ancestry_integrity!
|
132
|
-
|
156
|
+
parent_ids = {}
|
133
157
|
# Wrap the whole thing in a transaction ...
|
134
158
|
self.ancestry_base_class.transaction do
|
135
159
|
unscoped_where do |scope|
|
136
160
|
# For each node ...
|
137
161
|
scope.find_each do |node|
|
138
162
|
# ... set its ancestry to nil if invalid
|
139
|
-
if !node.
|
163
|
+
if !node.sane_ancestor_ids?
|
140
164
|
node.without_ancestry_callbacks do
|
141
|
-
node.update_attribute
|
165
|
+
node.update_attribute :ancestor_ids, []
|
142
166
|
end
|
143
167
|
end
|
144
|
-
# ... save parent of this node in
|
145
|
-
|
168
|
+
# ... save parent id of this node in parent_ids array if it exists
|
169
|
+
parent_ids[node.id] = node.parent_id if exists? node.parent_id
|
146
170
|
|
147
171
|
# Reset parent id in array to nil if it introduces a cycle
|
148
|
-
|
149
|
-
until
|
150
|
-
|
172
|
+
parent_id = parent_ids[node.id]
|
173
|
+
until parent_id.nil? || parent_id == node.id
|
174
|
+
parent_id = parent_ids[parent_id]
|
151
175
|
end
|
152
|
-
|
176
|
+
parent_ids[node.id] = nil if parent_id == node.id
|
153
177
|
end
|
154
178
|
|
155
179
|
# For each node ...
|
156
180
|
scope.find_each do |node|
|
157
|
-
# ... rebuild ancestry from
|
158
|
-
|
159
|
-
until
|
160
|
-
|
181
|
+
# ... rebuild ancestry from parent_ids array
|
182
|
+
ancestor_ids, parent_id = [], parent_ids[node.id]
|
183
|
+
until parent_id.nil?
|
184
|
+
ancestor_ids, parent_id = [parent_id] + ancestor_ids, parent_ids[parent_id]
|
161
185
|
end
|
162
186
|
node.without_ancestry_callbacks do
|
163
|
-
node.update_attribute
|
187
|
+
node.update_attribute :ancestor_ids, ancestor_ids
|
164
188
|
end
|
165
189
|
end
|
166
190
|
end
|
167
191
|
end
|
168
192
|
end
|
169
193
|
|
170
|
-
# Build ancestry from parent
|
171
|
-
def build_ancestry_from_parent_ids! parent_id = nil,
|
194
|
+
# Build ancestry from parent ids for migration purposes
|
195
|
+
def build_ancestry_from_parent_ids! column=:parent_id, parent_id = nil, ancestor_ids = []
|
172
196
|
unscoped_where do |scope|
|
173
|
-
scope.where(
|
197
|
+
scope.where(column => parent_id).find_each do |node|
|
174
198
|
node.without_ancestry_callbacks do
|
175
|
-
node.update_attribute
|
199
|
+
node.update_attribute :ancestor_ids, ancestor_ids
|
176
200
|
end
|
177
|
-
build_ancestry_from_parent_ids! node.id,
|
201
|
+
build_ancestry_from_parent_ids! column, node.id, ancestor_ids + [node.id]
|
178
202
|
end
|
179
203
|
end
|
180
204
|
end
|
181
205
|
|
182
206
|
# Rebuild depth cache if it got corrupted or if depth caching was just turned on
|
183
207
|
def rebuild_depth_cache!
|
184
|
-
raise Ancestry::AncestryException.new("
|
208
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_rebuild_depth_cache")) unless respond_to? :depth_cache_column
|
185
209
|
|
186
210
|
self.ancestry_base_class.transaction do
|
187
211
|
unscoped_where do |scope|
|
@@ -203,7 +227,7 @@ module Ancestry
|
|
203
227
|
end
|
204
228
|
|
205
229
|
ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
|
206
|
-
if ActiveSupport::VERSION::STRING < "4.
|
230
|
+
if ActiveSupport::VERSION::STRING < "4.2"
|
207
231
|
def primary_key_is_an_integer?
|
208
232
|
if defined?(@primary_key_is_an_integer)
|
209
233
|
@primary_key_is_an_integer
|
@@ -2,14 +2,13 @@ module Ancestry
|
|
2
2
|
module HasAncestry
|
3
3
|
def has_ancestry options = {}
|
4
4
|
# Check options
|
5
|
-
raise Ancestry::AncestryException.new("
|
5
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.option_must_be_hash")) unless options.is_a? Hash
|
6
6
|
options.each do |key, value|
|
7
|
-
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch].include? key
|
8
|
-
raise Ancestry::AncestryException.new("
|
7
|
+
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy].include? key
|
8
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_option", {:key => key.inspect, :value => value.inspect}))
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
12
|
# Create ancestry column accessor and set to option or default
|
14
13
|
cattr_accessor :ancestry_column
|
15
14
|
self.ancestry_column = options[:ancestry_column] || :ancestry
|
@@ -28,8 +27,12 @@ module Ancestry
|
|
28
27
|
# Include dynamic class methods
|
29
28
|
extend Ancestry::ClassMethods
|
30
29
|
|
30
|
+
validates_format_of self.ancestry_column, :with => derive_ancestry_pattern(options[:primary_key_format]), :allow_nil => true
|
31
31
|
extend Ancestry::MaterializedPath
|
32
32
|
|
33
|
+
update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
|
34
|
+
include Ancestry::MaterializedPathPg if update_strategy == :sql
|
35
|
+
|
33
36
|
# Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
|
34
37
|
cattr_reader :orphan_strategy
|
35
38
|
self.orphan_strategy = options[:orphan_strategy] || :destroy
|
@@ -37,27 +40,6 @@ module Ancestry
|
|
37
40
|
# Validate that the ancestor ids don't include own id
|
38
41
|
validate :ancestry_exclude_self
|
39
42
|
|
40
|
-
# Named scopes
|
41
|
-
scope :roots, lambda { where(root_conditions) }
|
42
|
-
scope :ancestors_of, lambda { |object| where(ancestor_conditions(object)) }
|
43
|
-
scope :children_of, lambda { |object| where(child_conditions(object)) }
|
44
|
-
scope :indirects_of, lambda { |object| where(indirect_conditions(object)) }
|
45
|
-
scope :descendants_of, lambda { |object| where(descendant_conditions(object)) }
|
46
|
-
scope :subtree_of, lambda { |object| where(subtree_conditions(object)) }
|
47
|
-
scope :siblings_of, lambda { |object| where(sibling_conditions(object)) }
|
48
|
-
scope :ordered_by_ancestry, Proc.new { |order|
|
49
|
-
if %w(mysql mysql2 sqlite sqlite3 postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::MAJOR >= 5
|
50
|
-
reorder(
|
51
|
-
Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
|
52
|
-
order
|
53
|
-
)
|
54
|
-
else
|
55
|
-
reorder(Arel.sql("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}"), order)
|
56
|
-
end
|
57
|
-
}
|
58
|
-
scope :ordered_by_ancestry_and, Proc.new { |order| ordered_by_ancestry(order) }
|
59
|
-
scope :path_of, lambda { |object| to_node(object).path }
|
60
|
-
|
61
43
|
# Update descendants with new ancestry before save
|
62
44
|
before_save :update_descendants_with_new_ancestry
|
63
45
|
|
@@ -78,10 +60,27 @@ module Ancestry
|
|
78
60
|
validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
|
79
61
|
end
|
80
62
|
|
63
|
+
# Create counter cache column accessor and set to option or default
|
64
|
+
if options[:counter_cache]
|
65
|
+
cattr_accessor :counter_cache_column
|
66
|
+
|
67
|
+
if options[:counter_cache] == true
|
68
|
+
self.counter_cache_column = :children_count
|
69
|
+
else
|
70
|
+
self.counter_cache_column = options[:counter_cache]
|
71
|
+
end
|
72
|
+
|
73
|
+
after_create :increase_parent_counter_cache, if: :has_parent?
|
74
|
+
after_destroy :decrease_parent_counter_cache, if: :has_parent?
|
75
|
+
after_update :update_parent_counter_cache
|
76
|
+
end
|
77
|
+
|
81
78
|
# Create named scopes for depth
|
82
79
|
{:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
|
83
80
|
scope scope_name, lambda { |depth|
|
84
|
-
raise Ancestry::AncestryException.new("
|
81
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.named_scope_depth_cache",
|
82
|
+
:scope_name => scope_name
|
83
|
+
)) unless options[:cache_depth]
|
85
84
|
where("#{depth_cache_column} #{operator} ?", depth)
|
86
85
|
}
|
87
86
|
end
|
@@ -100,6 +99,18 @@ module Ancestry
|
|
100
99
|
return super if defined?(super)
|
101
100
|
has_ancestry(*args)
|
102
101
|
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def derive_ancestry_pattern(primary_key_format, delimiter = '/')
|
106
|
+
primary_key_format ||= '[0-9]+'
|
107
|
+
|
108
|
+
if primary_key_format.to_s.include?('\A')
|
109
|
+
primary_key_format
|
110
|
+
else
|
111
|
+
/\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
|
112
|
+
end
|
113
|
+
end
|
103
114
|
end
|
104
115
|
end
|
105
116
|
|
@@ -1,11 +1,8 @@
|
|
1
1
|
module Ancestry
|
2
2
|
module InstanceMethods
|
3
|
-
BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save' : '_was'
|
4
|
-
IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database' : '_was'
|
5
|
-
|
6
3
|
# Validate that the ancestors don't include itself
|
7
4
|
def ancestry_exclude_self
|
8
|
-
errors.add(:base, "
|
5
|
+
errors.add(:base, I18n.t("ancestry.exclude_self", {:class_name => self.class.name.humanize})) if ancestor_ids.include? self.id
|
9
6
|
end
|
10
7
|
|
11
8
|
# Update descendants with new ancestry (before save)
|
@@ -16,15 +13,8 @@ module Ancestry
|
|
16
13
|
unscoped_descendants.each do |descendant|
|
17
14
|
# ... replace old ancestry with new ancestry
|
18
15
|
descendant.without_ancestry_callbacks do
|
19
|
-
descendant.
|
20
|
-
|
21
|
-
descendant.read_attribute(descendant.class.ancestry_column).gsub(
|
22
|
-
# child_ancestry_was
|
23
|
-
/^#{self.child_ancestry}/,
|
24
|
-
# future child_ancestry
|
25
|
-
if ancestors? then "#{read_attribute self.class.ancestry_column }/#{id}" else id.to_s end
|
26
|
-
)
|
27
|
-
)
|
16
|
+
new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_in_database)
|
17
|
+
descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
|
28
18
|
end
|
29
19
|
end
|
30
20
|
end
|
@@ -37,13 +27,7 @@ module Ancestry
|
|
37
27
|
when :rootify # make all children root if orphan strategy is rootify
|
38
28
|
unscoped_descendants.each do |descendant|
|
39
29
|
descendant.without_ancestry_callbacks do
|
40
|
-
|
41
|
-
nil
|
42
|
-
else
|
43
|
-
# child_ancestry did not change so child_ancestry_was will work here
|
44
|
-
descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
|
45
|
-
end
|
46
|
-
descendant.update_attribute descendant.class.ancestry_column, new_ancestry
|
30
|
+
descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
|
47
31
|
end
|
48
32
|
end
|
49
33
|
when :destroy # destroy all descendants if orphan strategy is destroy
|
@@ -55,14 +39,11 @@ module Ancestry
|
|
55
39
|
when :adopt # make child elements of this node, child of its parent
|
56
40
|
descendants.each do |descendant|
|
57
41
|
descendant.without_ancestry_callbacks do
|
58
|
-
|
59
|
-
# check for empty string if it's then set to nil
|
60
|
-
new_ancestry = nil if new_ancestry.empty?
|
61
|
-
descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
|
42
|
+
descendant.update_attribute :ancestor_ids, descendant.ancestor_ids.delete_if { |x| x == self.id }
|
62
43
|
end
|
63
44
|
end
|
64
45
|
when :restrict # throw an exception if it has children
|
65
|
-
raise Ancestry::AncestryException.new(
|
46
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
|
66
47
|
end
|
67
48
|
end
|
68
49
|
end
|
@@ -79,67 +60,81 @@ module Ancestry
|
|
79
60
|
end
|
80
61
|
end
|
81
62
|
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
# New records cannot have children
|
86
|
-
raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
|
87
|
-
|
88
|
-
if self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}").blank?
|
89
|
-
id.to_s
|
90
|
-
else
|
91
|
-
"#{self.send "#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"}/#{id}"
|
92
|
-
end
|
63
|
+
# Counter Cache
|
64
|
+
def increase_parent_counter_cache
|
65
|
+
self.class.increment_counter _counter_cache_column, parent_id
|
93
66
|
end
|
94
67
|
|
95
|
-
|
68
|
+
def decrease_parent_counter_cache
|
69
|
+
# @_trigger_destroy_callback comes from activerecord, which makes sure only once decrement when concurrent deletion.
|
70
|
+
# but @_trigger_destroy_callback began after rails@5.1.0.alpha.
|
71
|
+
# https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L340
|
72
|
+
# https://github.com/rails/rails/pull/14735
|
73
|
+
# https://github.com/rails/rails/pull/27248
|
74
|
+
return if defined?(@_trigger_destroy_callback) && !@_trigger_destroy_callback
|
75
|
+
return if ancestry_callbacks_disabled?
|
96
76
|
|
97
|
-
|
98
|
-
# ancestor_ids.present?
|
99
|
-
read_attribute(self.ancestry_base_class.ancestry_column).present?
|
77
|
+
self.class.decrement_counter _counter_cache_column, parent_id
|
100
78
|
end
|
101
|
-
alias :has_parent? :ancestors?
|
102
79
|
|
103
|
-
def
|
104
|
-
changed
|
105
|
-
|
80
|
+
def update_parent_counter_cache
|
81
|
+
changed =
|
82
|
+
if ActiveRecord::VERSION::STRING >= '5.1.0'
|
83
|
+
saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
|
84
|
+
else
|
85
|
+
ancestry_changed?
|
86
|
+
end
|
106
87
|
|
107
|
-
|
108
|
-
|
88
|
+
return unless changed
|
89
|
+
|
90
|
+
if parent_id_was = parent_id_before_last_save
|
91
|
+
self.class.decrement_counter _counter_cache_column, parent_id_was
|
92
|
+
end
|
93
|
+
|
94
|
+
parent_id && self.class.increment_counter(_counter_cache_column, parent_id)
|
109
95
|
end
|
110
96
|
|
111
|
-
def
|
112
|
-
self.ancestry_base_class.
|
97
|
+
def _counter_cache_column
|
98
|
+
self.ancestry_base_class.counter_cache_column.to_s
|
113
99
|
end
|
114
100
|
|
115
|
-
|
116
|
-
|
101
|
+
# Ancestors
|
102
|
+
|
103
|
+
def ancestors?
|
104
|
+
ancestor_ids.present?
|
117
105
|
end
|
106
|
+
alias :has_parent? :ancestors?
|
118
107
|
|
119
|
-
|
120
|
-
|
121
|
-
|
108
|
+
def ancestry_changed?
|
109
|
+
column = self.ancestry_base_class.ancestry_column.to_s
|
110
|
+
if ActiveRecord::VERSION::STRING >= '5.1.0'
|
111
|
+
# These methods return nil if there are no changes.
|
112
|
+
# This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
|
113
|
+
!!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
|
114
|
+
else
|
115
|
+
changed.include?(column)
|
116
|
+
end
|
122
117
|
end
|
123
118
|
|
124
|
-
|
125
|
-
|
126
|
-
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}_was"))
|
119
|
+
def sane_ancestor_ids?
|
120
|
+
valid? || errors[self.ancestry_base_class.ancestry_column].blank?
|
127
121
|
end
|
128
122
|
|
129
|
-
def
|
130
|
-
|
123
|
+
def ancestors depth_options = {}
|
124
|
+
return self.ancestry_base_class.none unless ancestors?
|
125
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
|
131
126
|
end
|
132
127
|
|
133
128
|
def path_ids
|
134
129
|
ancestor_ids + [id]
|
135
130
|
end
|
136
131
|
|
137
|
-
def
|
138
|
-
|
132
|
+
def path_ids_in_database
|
133
|
+
ancestor_ids_in_database + [id]
|
139
134
|
end
|
140
135
|
|
141
136
|
def path depth_options = {}
|
142
|
-
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.
|
137
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
|
143
138
|
end
|
144
139
|
|
145
140
|
def depth
|
@@ -159,7 +154,7 @@ module Ancestry
|
|
159
154
|
# currently parent= does not work in after save callbacks
|
160
155
|
# assuming that parent hasn't changed
|
161
156
|
def parent= parent
|
162
|
-
|
157
|
+
self.ancestor_ids = parent ? parent.path_ids : []
|
163
158
|
end
|
164
159
|
|
165
160
|
def parent_id= new_parent_id
|
@@ -169,15 +164,12 @@ module Ancestry
|
|
169
164
|
def parent_id
|
170
165
|
ancestor_ids.last if ancestors?
|
171
166
|
end
|
167
|
+
alias :parent_id? :ancestors?
|
172
168
|
|
173
169
|
def parent
|
174
170
|
unscoped_find(parent_id) if ancestors?
|
175
171
|
end
|
176
172
|
|
177
|
-
def parent_id?
|
178
|
-
ancestors?
|
179
|
-
end
|
180
|
-
|
181
173
|
def parent_of?(node)
|
182
174
|
self.id == node.parent_id
|
183
175
|
end
|
@@ -193,7 +185,7 @@ module Ancestry
|
|
193
185
|
end
|
194
186
|
|
195
187
|
def is_root?
|
196
|
-
|
188
|
+
!ancestors?
|
197
189
|
end
|
198
190
|
alias :root? :is_root?
|
199
191
|
|
@@ -203,12 +195,8 @@ module Ancestry
|
|
203
195
|
|
204
196
|
# Children
|
205
197
|
|
206
|
-
def child_conditions
|
207
|
-
self.ancestry_base_class.child_conditions(self)
|
208
|
-
end
|
209
|
-
|
210
198
|
def children
|
211
|
-
self.ancestry_base_class.
|
199
|
+
self.ancestry_base_class.children_of(self)
|
212
200
|
end
|
213
201
|
|
214
202
|
def child_ids
|
@@ -231,14 +219,11 @@ module Ancestry
|
|
231
219
|
|
232
220
|
# Siblings
|
233
221
|
|
234
|
-
def sibling_conditions
|
235
|
-
self.ancestry_base_class.sibling_conditions(self)
|
236
|
-
end
|
237
|
-
|
238
222
|
def siblings
|
239
|
-
self.ancestry_base_class.
|
223
|
+
self.ancestry_base_class.siblings_of(self)
|
240
224
|
end
|
241
225
|
|
226
|
+
# NOTE: includes self
|
242
227
|
def sibling_ids
|
243
228
|
siblings.pluck(self.ancestry_base_class.primary_key)
|
244
229
|
end
|
@@ -254,17 +239,13 @@ module Ancestry
|
|
254
239
|
alias_method :only_child?, :is_only_child?
|
255
240
|
|
256
241
|
def sibling_of?(node)
|
257
|
-
self.
|
242
|
+
self.ancestor_ids == node.ancestor_ids
|
258
243
|
end
|
259
244
|
|
260
245
|
# Descendants
|
261
246
|
|
262
|
-
def descendant_conditions
|
263
|
-
self.ancestry_base_class.descendant_conditions(self)
|
264
|
-
end
|
265
|
-
|
266
247
|
def descendants depth_options = {}
|
267
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
248
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
|
268
249
|
end
|
269
250
|
|
270
251
|
def descendant_ids depth_options = {}
|
@@ -277,12 +258,8 @@ module Ancestry
|
|
277
258
|
|
278
259
|
# Indirects
|
279
260
|
|
280
|
-
def indirect_conditions
|
281
|
-
self.ancestry_base_class.indirect_conditions(self)
|
282
|
-
end
|
283
|
-
|
284
261
|
def indirects depth_options = {}
|
285
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
262
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
|
286
263
|
end
|
287
264
|
|
288
265
|
def indirect_ids depth_options = {}
|
@@ -295,12 +272,8 @@ module Ancestry
|
|
295
272
|
|
296
273
|
# Subtree
|
297
274
|
|
298
|
-
def subtree_conditions
|
299
|
-
self.ancestry_base_class.subtree_conditions(self)
|
300
|
-
end
|
301
|
-
|
302
275
|
def subtree depth_options = {}
|
303
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
276
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
|
304
277
|
end
|
305
278
|
|
306
279
|
def subtree_ids depth_options = {}
|
@@ -320,17 +293,9 @@ module Ancestry
|
|
320
293
|
end
|
321
294
|
|
322
295
|
private
|
323
|
-
ANCESTRY_DELIMITER = '/'.freeze
|
324
|
-
|
325
|
-
def parse_ancestry_column obj
|
326
|
-
return [] unless obj
|
327
|
-
obj_ids = obj.split(ANCESTRY_DELIMITER)
|
328
|
-
self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
|
329
|
-
end
|
330
|
-
|
331
296
|
def unscoped_descendants
|
332
297
|
unscoped_where do |scope|
|
333
|
-
scope.where descendant_conditions
|
298
|
+
scope.where self.ancestry_base_class.descendant_conditions(self)
|
334
299
|
end
|
335
300
|
end
|
336
301
|
|