ancestry 3.0.6 → 4.0.0
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 +4 -4
- data/CHANGELOG.md +286 -0
- data/README.md +86 -202
- data/lib/ancestry.rb +28 -1
- data/lib/ancestry/class_methods.rb +60 -52
- data/lib/ancestry/has_ancestry.rb +39 -33
- data/lib/ancestry/instance_methods.rb +62 -106
- data/lib/ancestry/locales/en.yml +16 -0
- data/lib/ancestry/materialized_path.rb +100 -27
- data/lib/ancestry/materialized_path_pg.rb +23 -0
- data/lib/ancestry/version.rb +1 -1
- metadata +24 -51
- 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|
|
@@ -193,31 +217,15 @@ module Ancestry
|
|
193
217
|
end
|
194
218
|
|
195
219
|
def unscoped_where
|
196
|
-
|
197
|
-
self.ancestry_base_class.unscoped do
|
198
|
-
yield self.ancestry_base_class
|
199
|
-
end
|
200
|
-
else
|
201
|
-
yield self.ancestry_base_class.unscope(:where)
|
202
|
-
end
|
220
|
+
yield self.ancestry_base_class.default_scoped.unscope(:where)
|
203
221
|
end
|
204
222
|
|
205
223
|
ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
@primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(columns_hash[primary_key.to_s].type)
|
212
|
-
end
|
213
|
-
end
|
214
|
-
else
|
215
|
-
def primary_key_is_an_integer?
|
216
|
-
if defined?(@primary_key_is_an_integer)
|
217
|
-
@primary_key_is_an_integer
|
218
|
-
else
|
219
|
-
@primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(type_for_attribute(primary_key).type)
|
220
|
-
end
|
224
|
+
def primary_key_is_an_integer?
|
225
|
+
if defined?(@primary_key_is_an_integer)
|
226
|
+
@primary_key_is_an_integer
|
227
|
+
else
|
228
|
+
@primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(type_for_attribute(primary_key).type)
|
221
229
|
end
|
222
230
|
end
|
223
231
|
end
|
@@ -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,31 +60,55 @@ 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
|
88
87
|
|
89
88
|
after_touch :touch_ancestors_callback
|
90
89
|
after_destroy :touch_ancestors_callback
|
91
|
-
|
92
|
-
if ActiveRecord::VERSION::STRING >= '5.1.0'
|
93
|
-
after_save :touch_ancestors_callback, if: :saved_changes?
|
94
|
-
else
|
95
|
-
after_save :touch_ancestors_callback, if: :changed?
|
96
|
-
end
|
90
|
+
after_save :touch_ancestors_callback, if: :saved_changes?
|
97
91
|
end
|
98
92
|
|
99
93
|
def acts_as_tree(*args)
|
100
94
|
return super if defined?(super)
|
101
95
|
has_ancestry(*args)
|
102
96
|
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def derive_ancestry_pattern(primary_key_format, delimiter = '/')
|
101
|
+
primary_key_format ||= '[0-9]+'
|
102
|
+
|
103
|
+
if primary_key_format.to_s.include?('\A')
|
104
|
+
primary_key_format
|
105
|
+
else
|
106
|
+
/\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
|
107
|
+
end
|
108
|
+
end
|
103
109
|
end
|
104
110
|
end
|
105
111
|
|
106
112
|
ActiveSupport.on_load :active_record do
|
107
|
-
|
113
|
+
extend Ancestry::HasAncestry
|
108
114
|
end
|
@@ -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,72 @@ 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
|
-
end
|
80
|
+
def update_parent_counter_cache
|
81
|
+
changed = saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
|
106
82
|
|
107
|
-
|
108
|
-
|
83
|
+
return unless changed
|
84
|
+
|
85
|
+
if parent_id_was = parent_id_before_last_save
|
86
|
+
self.class.decrement_counter _counter_cache_column, parent_id_was
|
87
|
+
end
|
88
|
+
|
89
|
+
parent_id && self.class.increment_counter(_counter_cache_column, parent_id)
|
109
90
|
end
|
110
91
|
|
111
|
-
def
|
112
|
-
self.ancestry_base_class.
|
92
|
+
def _counter_cache_column
|
93
|
+
self.ancestry_base_class.counter_cache_column.to_s
|
113
94
|
end
|
114
95
|
|
115
|
-
|
116
|
-
|
96
|
+
# Ancestors
|
97
|
+
|
98
|
+
def has_parent?
|
99
|
+
ancestor_ids.present?
|
117
100
|
end
|
101
|
+
alias :ancestors? :has_parent?
|
118
102
|
|
119
|
-
|
120
|
-
|
121
|
-
|
103
|
+
def ancestry_changed?
|
104
|
+
column = self.ancestry_base_class.ancestry_column.to_s
|
105
|
+
# These methods return nil if there are no changes.
|
106
|
+
# This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
|
107
|
+
!!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
|
122
108
|
end
|
123
109
|
|
124
|
-
|
125
|
-
|
126
|
-
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}_was"))
|
110
|
+
def sane_ancestor_ids?
|
111
|
+
valid? || errors[self.ancestry_base_class.ancestry_column].blank?
|
127
112
|
end
|
128
113
|
|
129
|
-
def
|
130
|
-
|
114
|
+
def ancestors depth_options = {}
|
115
|
+
return self.ancestry_base_class.none unless has_parent?
|
116
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
|
131
117
|
end
|
132
118
|
|
133
119
|
def path_ids
|
134
120
|
ancestor_ids + [id]
|
135
121
|
end
|
136
122
|
|
137
|
-
def
|
138
|
-
|
123
|
+
def path_ids_in_database
|
124
|
+
ancestor_ids_in_database + [id]
|
139
125
|
end
|
140
126
|
|
141
127
|
def path depth_options = {}
|
142
|
-
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.
|
128
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
|
143
129
|
end
|
144
130
|
|
145
131
|
def depth
|
@@ -159,7 +145,7 @@ module Ancestry
|
|
159
145
|
# currently parent= does not work in after save callbacks
|
160
146
|
# assuming that parent hasn't changed
|
161
147
|
def parent= parent
|
162
|
-
|
148
|
+
self.ancestor_ids = parent ? parent.path_ids : []
|
163
149
|
end
|
164
150
|
|
165
151
|
def parent_id= new_parent_id
|
@@ -167,15 +153,12 @@ module Ancestry
|
|
167
153
|
end
|
168
154
|
|
169
155
|
def parent_id
|
170
|
-
ancestor_ids.last if
|
156
|
+
ancestor_ids.last if has_parent?
|
171
157
|
end
|
158
|
+
alias :parent_id? :ancestors?
|
172
159
|
|
173
160
|
def parent
|
174
|
-
unscoped_find(parent_id) if
|
175
|
-
end
|
176
|
-
|
177
|
-
def parent_id?
|
178
|
-
ancestors?
|
161
|
+
unscoped_find(parent_id) if has_parent?
|
179
162
|
end
|
180
163
|
|
181
164
|
def parent_of?(node)
|
@@ -185,15 +168,15 @@ module Ancestry
|
|
185
168
|
# Root
|
186
169
|
|
187
170
|
def root_id
|
188
|
-
|
171
|
+
has_parent? ? ancestor_ids.first : id
|
189
172
|
end
|
190
173
|
|
191
174
|
def root
|
192
|
-
|
175
|
+
has_parent? ? unscoped_find(root_id) : self
|
193
176
|
end
|
194
177
|
|
195
178
|
def is_root?
|
196
|
-
|
179
|
+
!has_parent?
|
197
180
|
end
|
198
181
|
alias :root? :is_root?
|
199
182
|
|
@@ -203,12 +186,8 @@ module Ancestry
|
|
203
186
|
|
204
187
|
# Children
|
205
188
|
|
206
|
-
def child_conditions
|
207
|
-
self.ancestry_base_class.child_conditions(self)
|
208
|
-
end
|
209
|
-
|
210
189
|
def children
|
211
|
-
self.ancestry_base_class.
|
190
|
+
self.ancestry_base_class.children_of(self)
|
212
191
|
end
|
213
192
|
|
214
193
|
def child_ids
|
@@ -231,14 +210,11 @@ module Ancestry
|
|
231
210
|
|
232
211
|
# Siblings
|
233
212
|
|
234
|
-
def sibling_conditions
|
235
|
-
self.ancestry_base_class.sibling_conditions(self)
|
236
|
-
end
|
237
|
-
|
238
213
|
def siblings
|
239
|
-
self.ancestry_base_class.
|
214
|
+
self.ancestry_base_class.siblings_of(self)
|
240
215
|
end
|
241
216
|
|
217
|
+
# NOTE: includes self
|
242
218
|
def sibling_ids
|
243
219
|
siblings.pluck(self.ancestry_base_class.primary_key)
|
244
220
|
end
|
@@ -254,17 +230,13 @@ module Ancestry
|
|
254
230
|
alias_method :only_child?, :is_only_child?
|
255
231
|
|
256
232
|
def sibling_of?(node)
|
257
|
-
self.
|
233
|
+
self.ancestor_ids == node.ancestor_ids
|
258
234
|
end
|
259
235
|
|
260
236
|
# Descendants
|
261
237
|
|
262
|
-
def descendant_conditions
|
263
|
-
self.ancestry_base_class.descendant_conditions(self)
|
264
|
-
end
|
265
|
-
|
266
238
|
def descendants depth_options = {}
|
267
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
239
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
|
268
240
|
end
|
269
241
|
|
270
242
|
def descendant_ids depth_options = {}
|
@@ -277,12 +249,8 @@ module Ancestry
|
|
277
249
|
|
278
250
|
# Indirects
|
279
251
|
|
280
|
-
def indirect_conditions
|
281
|
-
self.ancestry_base_class.indirect_conditions(self)
|
282
|
-
end
|
283
|
-
|
284
252
|
def indirects depth_options = {}
|
285
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
253
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
|
286
254
|
end
|
287
255
|
|
288
256
|
def indirect_ids depth_options = {}
|
@@ -295,12 +263,8 @@ module Ancestry
|
|
295
263
|
|
296
264
|
# Subtree
|
297
265
|
|
298
|
-
def subtree_conditions
|
299
|
-
self.ancestry_base_class.subtree_conditions(self)
|
300
|
-
end
|
301
|
-
|
302
266
|
def subtree depth_options = {}
|
303
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
267
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
|
304
268
|
end
|
305
269
|
|
306
270
|
def subtree_ids depth_options = {}
|
@@ -320,17 +284,9 @@ module Ancestry
|
|
320
284
|
end
|
321
285
|
|
322
286
|
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
287
|
def unscoped_descendants
|
332
288
|
unscoped_where do |scope|
|
333
|
-
scope.where descendant_conditions
|
289
|
+
scope.where self.ancestry_base_class.descendant_conditions(self)
|
334
290
|
end
|
335
291
|
end
|
336
292
|
|