ancestry 3.0.5 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|