ancestry 4.3.3 → 5.1.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 +161 -65
- data/README.md +56 -40
- data/lib/ancestry/class_methods.rb +105 -69
- data/lib/ancestry/exceptions.rb +3 -1
- data/lib/ancestry/has_ancestry.rb +82 -49
- data/lib/ancestry/instance_methods.rb +129 -87
- data/lib/ancestry/locales/en.yml +0 -1
- data/lib/ancestry/materialized_path.rb +117 -54
- data/lib/ancestry/materialized_path2.rb +66 -21
- data/lib/ancestry/materialized_path_pg.rb +32 -13
- data/lib/ancestry/version.rb +3 -1
- data/lib/ancestry.rb +3 -1
- metadata +18 -5
|
@@ -1,33 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ancestry
|
|
2
4
|
module HasAncestry
|
|
3
|
-
def has_ancestry
|
|
5
|
+
def has_ancestry(options = {})
|
|
4
6
|
# Check options
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
unless options.is_a? Hash
|
|
8
|
+
raise Ancestry::AncestryException, I18n.t("ancestry.option_must_be_hash")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
extra_keys = options.keys - [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy, :ancestry_format]
|
|
12
|
+
if (key = extra_keys.first)
|
|
13
|
+
raise Ancestry::AncestryException, I18n.t("ancestry.unknown_option", key: key.inspect, value: options[key].inspect)
|
|
10
14
|
end
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
ancestry_format = options[:ancestry_format] || Ancestry.default_ancestry_format
|
|
17
|
+
if ![:materialized_path, :materialized_path2].include?(ancestry_format)
|
|
18
|
+
raise Ancestry::AncestryException, I18n.t("ancestry.unknown_format", value: ancestry_format)
|
|
14
19
|
end
|
|
15
20
|
|
|
21
|
+
orphan_strategy = options[:orphan_strategy] || :destroy
|
|
22
|
+
|
|
16
23
|
# Create ancestry column accessor and set to option or default
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
class_variable_set('@@ancestry_column', options[:ancestry_column] || :ancestry)
|
|
25
|
+
cattr_reader :ancestry_column, instance_reader: false
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
self.ancestry_primary_key_format = options[:primary_key_format].presence || Ancestry.default_primary_key_format
|
|
27
|
+
primary_key_format = options[:primary_key_format].presence || Ancestry.default_primary_key_format
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
class_variable_set('@@ancestry_delimiter', '/')
|
|
30
|
+
cattr_reader :ancestry_delimiter, instance_reader: false
|
|
25
31
|
|
|
26
32
|
# Save self as base class (for STI)
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
class_variable_set('@@ancestry_base_class', self)
|
|
34
|
+
cattr_reader :ancestry_base_class, instance_reader: false
|
|
29
35
|
|
|
30
36
|
# Touch ancestors after updating
|
|
37
|
+
# days are limited. need to handle touch in pg case
|
|
31
38
|
cattr_accessor :touch_ancestors
|
|
32
39
|
self.touch_ancestors = options[:touch] || false
|
|
33
40
|
|
|
@@ -36,41 +43,54 @@ module Ancestry
|
|
|
36
43
|
|
|
37
44
|
# Include dynamic class methods
|
|
38
45
|
extend Ancestry::ClassMethods
|
|
46
|
+
extend Ancestry::HasAncestry.ancestry_format_module(ancestry_format)
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
self.ancestry_format = options[:ancestry_format] || Ancestry.default_ancestry_format
|
|
48
|
+
attribute ancestry_column, default: ancestry_root
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
extend Ancestry::MaterializedPath2
|
|
45
|
-
else
|
|
46
|
-
extend Ancestry::MaterializedPath
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
attribute self.ancestry_column, default: self.ancestry_root
|
|
50
|
-
|
|
51
|
-
validates self.ancestry_column, ancestry_validation_options
|
|
50
|
+
validates ancestry_column, ancestry_validation_options(primary_key_format)
|
|
52
51
|
|
|
53
52
|
update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
|
|
54
|
-
include Ancestry::MaterializedPathPg
|
|
55
|
-
|
|
56
|
-
# Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
|
|
57
|
-
cattr_reader :orphan_strategy
|
|
58
|
-
self.orphan_strategy = options[:orphan_strategy] || :destroy
|
|
53
|
+
include Ancestry::MaterializedPathPg
|
|
59
54
|
|
|
60
55
|
# Validate that the ancestor ids don't include own id
|
|
61
56
|
validate :ancestry_exclude_self
|
|
62
57
|
|
|
58
|
+
# Validate descendants' depths don't exceed max depth when moving them
|
|
59
|
+
validate :ancestry_depth_of_descendants, if: :ancestry_changed?
|
|
60
|
+
|
|
63
61
|
# Update descendants with new ancestry after update
|
|
64
|
-
|
|
62
|
+
if update_strategy == :sql
|
|
63
|
+
after_update :update_descendants_with_new_ancestry_sql, if: :ancestry_changed?
|
|
64
|
+
else
|
|
65
|
+
after_update :update_descendants_with_new_ancestry, if: :ancestry_changed?
|
|
66
|
+
end
|
|
65
67
|
|
|
66
68
|
# Apply orphan strategy before destroy
|
|
67
|
-
|
|
69
|
+
orphan_strategy_helper = "apply_orphan_strategy_#{orphan_strategy}"
|
|
70
|
+
if method_defined?(orphan_strategy_helper)
|
|
71
|
+
alias_method :apply_orphan_strategy, orphan_strategy_helper
|
|
72
|
+
before_destroy :apply_orphan_strategy
|
|
73
|
+
elsif orphan_strategy.to_s != "none"
|
|
74
|
+
raise Ancestry::AncestryException, I18n.t("ancestry.invalid_orphan_strategy")
|
|
75
|
+
end
|
|
68
76
|
|
|
69
77
|
# Create ancestry column accessor and set to option or default
|
|
70
|
-
|
|
78
|
+
|
|
79
|
+
if options[:cache_depth] == :virtual
|
|
80
|
+
# NOTE: not setting self.depth_cache_column so the code does not try to update the column
|
|
81
|
+
depth_cache_sql = options[:depth_cache_column]&.to_s || 'ancestry_depth'
|
|
82
|
+
elsif options[:cache_depth]
|
|
71
83
|
# Create accessor for column name and set to option or default
|
|
72
|
-
|
|
73
|
-
self.depth_cache_column =
|
|
84
|
+
cattr_accessor :depth_cache_column
|
|
85
|
+
self.depth_cache_column =
|
|
86
|
+
if options[:cache_depth] == true
|
|
87
|
+
options[:depth_cache_column]&.to_s || 'ancestry_depth'
|
|
88
|
+
else
|
|
89
|
+
options[:cache_depth].to_s
|
|
90
|
+
end
|
|
91
|
+
if options[:depth_cache_column]
|
|
92
|
+
ActiveSupport::Deprecation.warn("has_ancestry :depth_cache_column is deprecated. Use :cache_depth instead.")
|
|
93
|
+
end
|
|
74
94
|
|
|
75
95
|
# Cache depth in depth cache column before save
|
|
76
96
|
before_validation :cache_depth
|
|
@@ -78,8 +98,19 @@ module Ancestry
|
|
|
78
98
|
|
|
79
99
|
# Validate depth column
|
|
80
100
|
validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
|
|
101
|
+
|
|
102
|
+
depth_cache_sql = depth_cache_column
|
|
103
|
+
else
|
|
104
|
+
# this is not efficient, but it works
|
|
105
|
+
depth_cache_sql = ancestry_depth_sql
|
|
81
106
|
end
|
|
82
107
|
|
|
108
|
+
scope :before_depth, lambda { |depth| where("#{depth_cache_sql} < ?", depth) }
|
|
109
|
+
scope :to_depth, lambda { |depth| where("#{depth_cache_sql} <= ?", depth) }
|
|
110
|
+
scope :at_depth, lambda { |depth| where("#{depth_cache_sql} = ?", depth) }
|
|
111
|
+
scope :from_depth, lambda { |depth| where("#{depth_cache_sql} >= ?", depth) }
|
|
112
|
+
scope :after_depth, lambda { |depth| where("#{depth_cache_sql} > ?", depth) }
|
|
113
|
+
|
|
83
114
|
# Create counter cache column accessor and set to option or default
|
|
84
115
|
if options[:counter_cache]
|
|
85
116
|
cattr_accessor :counter_cache_column
|
|
@@ -90,25 +121,27 @@ module Ancestry
|
|
|
90
121
|
after_update :update_parent_counter_cache
|
|
91
122
|
end
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
:scope_name => scope_name
|
|
98
|
-
)) unless options[:cache_depth]
|
|
99
|
-
where("#{depth_cache_column} #{operator} ?", depth)
|
|
100
|
-
}
|
|
124
|
+
if options[:touch]
|
|
125
|
+
after_touch :touch_ancestors_callback
|
|
126
|
+
after_destroy :touch_ancestors_callback
|
|
127
|
+
after_save :touch_ancestors_callback, if: :saved_changes?
|
|
101
128
|
end
|
|
102
|
-
|
|
103
|
-
after_touch :touch_ancestors_callback
|
|
104
|
-
after_destroy :touch_ancestors_callback
|
|
105
|
-
after_save :touch_ancestors_callback, if: :saved_changes?
|
|
106
129
|
end
|
|
107
130
|
|
|
108
131
|
def acts_as_tree(*args)
|
|
109
132
|
return super if defined?(super)
|
|
133
|
+
|
|
110
134
|
has_ancestry(*args)
|
|
111
135
|
end
|
|
136
|
+
|
|
137
|
+
def self.ancestry_format_module(ancestry_format)
|
|
138
|
+
ancestry_format ||= Ancestry.default_ancestry_format
|
|
139
|
+
if ancestry_format == :materialized_path2
|
|
140
|
+
Ancestry::MaterializedPath2
|
|
141
|
+
else
|
|
142
|
+
Ancestry::MaterializedPath
|
|
143
|
+
end
|
|
144
|
+
end
|
|
112
145
|
end
|
|
113
146
|
end
|
|
114
147
|
|
|
@@ -1,16 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ancestry
|
|
2
4
|
module InstanceMethods
|
|
3
5
|
# Validate that the ancestors don't include itself
|
|
4
6
|
def ancestry_exclude_self
|
|
5
|
-
errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.
|
|
7
|
+
errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.model_name.human)) if ancestor_ids.include?(id)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Validate that descendants' depths don't exceed max depth when moving them
|
|
11
|
+
def ancestry_depth_of_descendants
|
|
12
|
+
return if new_record? || (respond_to?(:previously_new_record?) && previously_new_record?)
|
|
13
|
+
return unless self.class.respond_to?(:depth_cache_column) && self.class.depth_cache_column
|
|
14
|
+
|
|
15
|
+
column = self.class.depth_cache_column
|
|
16
|
+
validator = self.class.validators_on(column).find do |v|
|
|
17
|
+
v.is_a?(ActiveModel::Validations::NumericalityValidator) &&
|
|
18
|
+
(v.options[:less_than_or_equal_to] || v.options[:less_than])
|
|
19
|
+
end
|
|
20
|
+
return unless validator
|
|
21
|
+
|
|
22
|
+
max_depth = validator.options[:less_than_or_equal_to] || (validator.options[:less_than] - 1)
|
|
23
|
+
|
|
24
|
+
old_value = attribute_in_database(self.class.ancestry_column)
|
|
25
|
+
new_value = read_attribute(self.class.ancestry_column)
|
|
26
|
+
depth_change = self.class.ancestry_depth_change(old_value, new_value)
|
|
27
|
+
|
|
28
|
+
if depth_change > 0
|
|
29
|
+
max_descendant_depth = unscoped_descendants.maximum(column) || attribute_in_database(column) || 0
|
|
30
|
+
if max_descendant_depth + depth_change > max_depth
|
|
31
|
+
errors.add(column, :less_than_or_equal_to, count: max_depth)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
6
34
|
end
|
|
7
35
|
|
|
8
36
|
# Update descendants with new ancestry (after update)
|
|
9
37
|
def update_descendants_with_new_ancestry
|
|
10
|
-
# If enabled and
|
|
11
|
-
|
|
38
|
+
# If enabled and the new ancestry is sane ...
|
|
39
|
+
# The only way the ancestry could be bad is via `update_attribute` with a bad value
|
|
40
|
+
if !ancestry_callbacks_disabled? && sane_ancestor_ids?
|
|
12
41
|
# ... for each descendant ...
|
|
13
|
-
|
|
42
|
+
unscoped_descendants_before_last_save.each do |descendant|
|
|
14
43
|
# ... replace old ancestry with new ancestry
|
|
15
44
|
descendant.without_ancestry_callbacks do
|
|
16
45
|
new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_before_last_save)
|
|
@@ -20,37 +49,49 @@ module Ancestry
|
|
|
20
49
|
end
|
|
21
50
|
end
|
|
22
51
|
|
|
23
|
-
#
|
|
24
|
-
def
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
when :destroy # destroy all descendants if orphan strategy is destroy
|
|
34
|
-
unscoped_descendants.each do |descendant|
|
|
35
|
-
descendant.without_ancestry_callbacks do
|
|
36
|
-
descendant.destroy
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
when :adopt # make child elements of this node, child of its parent
|
|
40
|
-
descendants.each do |descendant|
|
|
41
|
-
descendant.without_ancestry_callbacks do
|
|
42
|
-
descendant.update_attribute :ancestor_ids, (descendant.ancestor_ids.delete_if { |x| x == self.id })
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
when :restrict # throw an exception if it has children
|
|
46
|
-
raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
|
|
52
|
+
# make all children root if orphan strategy is rootify
|
|
53
|
+
def apply_orphan_strategy_rootify
|
|
54
|
+
return if ancestry_callbacks_disabled? || new_record?
|
|
55
|
+
|
|
56
|
+
unscoped_descendants.each do |descendant|
|
|
57
|
+
descendant.without_ancestry_callbacks do
|
|
58
|
+
descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
|
|
47
59
|
end
|
|
48
60
|
end
|
|
49
61
|
end
|
|
50
62
|
|
|
63
|
+
# destroy all descendants if orphan strategy is destroy
|
|
64
|
+
def apply_orphan_strategy_destroy
|
|
65
|
+
return if ancestry_callbacks_disabled? || new_record?
|
|
66
|
+
|
|
67
|
+
unscoped_descendants.ordered_by_ancestry.reverse_order.each do |descendant|
|
|
68
|
+
descendant.without_ancestry_callbacks do
|
|
69
|
+
descendant.destroy
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# make child elements of this node, child of its parent
|
|
75
|
+
def apply_orphan_strategy_adopt
|
|
76
|
+
return if ancestry_callbacks_disabled? || new_record?
|
|
77
|
+
|
|
78
|
+
descendants.each do |descendant|
|
|
79
|
+
descendant.without_ancestry_callbacks do
|
|
80
|
+
descendant.update_attribute :ancestor_ids, (descendant.ancestor_ids.delete_if { |x| x == id })
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# throw an exception if it has children
|
|
86
|
+
def apply_orphan_strategy_restrict
|
|
87
|
+
return if ancestry_callbacks_disabled? || new_record?
|
|
88
|
+
|
|
89
|
+
raise(Ancestry::AncestryException, I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
|
|
90
|
+
end
|
|
91
|
+
|
|
51
92
|
# Touch each of this record's ancestors (after save)
|
|
52
93
|
def touch_ancestors_callback
|
|
53
|
-
if !ancestry_callbacks_disabled?
|
|
94
|
+
if !ancestry_callbacks_disabled?
|
|
54
95
|
# Touch each of the old *and* new ancestors
|
|
55
96
|
unscoped_current_and_previous_ancestors.each do |ancestor|
|
|
56
97
|
ancestor.without_ancestry_callbacks do
|
|
@@ -62,7 +103,7 @@ module Ancestry
|
|
|
62
103
|
|
|
63
104
|
# Counter Cache
|
|
64
105
|
def increase_parent_counter_cache
|
|
65
|
-
self.ancestry_base_class.increment_counter counter_cache_column, parent_id
|
|
106
|
+
self.class.ancestry_base_class.increment_counter counter_cache_column, parent_id
|
|
66
107
|
end
|
|
67
108
|
|
|
68
109
|
def decrease_parent_counter_cache
|
|
@@ -74,16 +115,14 @@ module Ancestry
|
|
|
74
115
|
return if defined?(@_trigger_destroy_callback) && !@_trigger_destroy_callback
|
|
75
116
|
return if ancestry_callbacks_disabled?
|
|
76
117
|
|
|
77
|
-
self.ancestry_base_class.decrement_counter counter_cache_column, parent_id
|
|
118
|
+
self.class.ancestry_base_class.decrement_counter counter_cache_column, parent_id
|
|
78
119
|
end
|
|
79
120
|
|
|
80
121
|
def update_parent_counter_cache
|
|
81
|
-
|
|
122
|
+
return unless saved_change_to_attribute?(self.class.ancestry_column)
|
|
82
123
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if parent_id_was = parent_id_before_last_save
|
|
86
|
-
self.ancestry_base_class.decrement_counter counter_cache_column, parent_id_was
|
|
124
|
+
if (parent_id_was = parent_id_before_last_save)
|
|
125
|
+
self.class.ancestry_base_class.decrement_counter counter_cache_column, parent_id_was
|
|
87
126
|
end
|
|
88
127
|
|
|
89
128
|
parent_id && increase_parent_counter_cache
|
|
@@ -94,20 +133,20 @@ module Ancestry
|
|
|
94
133
|
def has_parent?
|
|
95
134
|
ancestor_ids.present?
|
|
96
135
|
end
|
|
97
|
-
alias
|
|
136
|
+
alias ancestors? has_parent?
|
|
98
137
|
|
|
99
138
|
def ancestry_changed?
|
|
100
|
-
column = self.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
139
|
+
column = self.class.ancestry_column.to_s
|
|
140
|
+
# These methods return nil if there are no changes.
|
|
141
|
+
# This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
|
|
142
|
+
!!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
|
|
104
143
|
end
|
|
105
144
|
|
|
106
145
|
def sane_ancestor_ids?
|
|
107
146
|
current_context, self.validation_context = validation_context, nil
|
|
108
147
|
errors.clear
|
|
109
148
|
|
|
110
|
-
attribute =
|
|
149
|
+
attribute = self.class.ancestry_column
|
|
111
150
|
ancestry_value = send(attribute)
|
|
112
151
|
return true unless ancestry_value
|
|
113
152
|
|
|
@@ -120,9 +159,10 @@ module Ancestry
|
|
|
120
159
|
self.validation_context = current_context
|
|
121
160
|
end
|
|
122
161
|
|
|
123
|
-
def ancestors
|
|
124
|
-
return self.ancestry_base_class.none unless has_parent?
|
|
125
|
-
|
|
162
|
+
def ancestors(depth_options = {})
|
|
163
|
+
return self.class.ancestry_base_class.none unless has_parent?
|
|
164
|
+
|
|
165
|
+
self.class.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
|
|
126
166
|
end
|
|
127
167
|
|
|
128
168
|
def path_ids
|
|
@@ -137,8 +177,8 @@ module Ancestry
|
|
|
137
177
|
ancestor_ids_in_database + [id]
|
|
138
178
|
end
|
|
139
179
|
|
|
140
|
-
def path
|
|
141
|
-
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
|
|
180
|
+
def path(depth_options = {})
|
|
181
|
+
self.class.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
|
|
142
182
|
end
|
|
143
183
|
|
|
144
184
|
def depth
|
|
@@ -146,29 +186,29 @@ module Ancestry
|
|
|
146
186
|
end
|
|
147
187
|
|
|
148
188
|
def cache_depth
|
|
149
|
-
write_attribute self.ancestry_base_class.depth_cache_column, depth
|
|
189
|
+
write_attribute self.class.ancestry_base_class.depth_cache_column, depth
|
|
150
190
|
end
|
|
151
191
|
|
|
152
192
|
def ancestor_of?(node)
|
|
153
|
-
node.ancestor_ids.include?(
|
|
193
|
+
node.ancestor_ids.include?(id)
|
|
154
194
|
end
|
|
155
195
|
|
|
156
196
|
# Parent
|
|
157
197
|
|
|
158
198
|
# currently parent= does not work in after save callbacks
|
|
159
199
|
# assuming that parent hasn't changed
|
|
160
|
-
def parent=
|
|
200
|
+
def parent=(parent)
|
|
161
201
|
self.ancestor_ids = parent ? parent.path_ids : []
|
|
162
202
|
end
|
|
163
203
|
|
|
164
|
-
def parent_id=
|
|
204
|
+
def parent_id=(new_parent_id)
|
|
165
205
|
self.parent = new_parent_id.present? ? unscoped_find(new_parent_id) : nil
|
|
166
206
|
end
|
|
167
207
|
|
|
168
208
|
def parent_id
|
|
169
209
|
ancestor_ids.last if has_parent?
|
|
170
210
|
end
|
|
171
|
-
alias
|
|
211
|
+
alias parent_id? ancestors?
|
|
172
212
|
|
|
173
213
|
def parent
|
|
174
214
|
if has_parent?
|
|
@@ -179,7 +219,7 @@ module Ancestry
|
|
|
179
219
|
end
|
|
180
220
|
|
|
181
221
|
def parent_of?(node)
|
|
182
|
-
|
|
222
|
+
id == node.parent_id
|
|
183
223
|
end
|
|
184
224
|
|
|
185
225
|
# Root
|
|
@@ -199,24 +239,24 @@ module Ancestry
|
|
|
199
239
|
def is_root?
|
|
200
240
|
!has_parent?
|
|
201
241
|
end
|
|
202
|
-
alias
|
|
242
|
+
alias root? is_root?
|
|
203
243
|
|
|
204
244
|
def root_of?(node)
|
|
205
|
-
|
|
245
|
+
id == node.root_id
|
|
206
246
|
end
|
|
207
247
|
|
|
208
248
|
# Children
|
|
209
249
|
|
|
210
250
|
def children
|
|
211
|
-
self.ancestry_base_class.children_of(self)
|
|
251
|
+
self.class.ancestry_base_class.children_of(self)
|
|
212
252
|
end
|
|
213
253
|
|
|
214
254
|
def child_ids
|
|
215
|
-
children.pluck(self.
|
|
255
|
+
children.pluck(self.class.primary_key)
|
|
216
256
|
end
|
|
217
257
|
|
|
218
258
|
def has_children?
|
|
219
|
-
|
|
259
|
+
children.exists?
|
|
220
260
|
end
|
|
221
261
|
alias_method :children?, :has_children?
|
|
222
262
|
|
|
@@ -226,22 +266,21 @@ module Ancestry
|
|
|
226
266
|
alias_method :childless?, :is_childless?
|
|
227
267
|
|
|
228
268
|
def child_of?(node)
|
|
229
|
-
|
|
269
|
+
parent_id == node.id
|
|
230
270
|
end
|
|
231
271
|
|
|
232
272
|
# Siblings
|
|
233
273
|
|
|
234
274
|
def siblings
|
|
235
|
-
self.ancestry_base_class.siblings_of(self)
|
|
275
|
+
self.class.ancestry_base_class.siblings_of(self).where.not(self.class.primary_key => id)
|
|
236
276
|
end
|
|
237
277
|
|
|
238
|
-
# NOTE: includes self
|
|
239
278
|
def sibling_ids
|
|
240
|
-
siblings.pluck(self.
|
|
279
|
+
siblings.pluck(self.class.primary_key)
|
|
241
280
|
end
|
|
242
281
|
|
|
243
282
|
def has_siblings?
|
|
244
|
-
|
|
283
|
+
siblings.exists?
|
|
245
284
|
end
|
|
246
285
|
alias_method :siblings?, :has_siblings?
|
|
247
286
|
|
|
@@ -251,17 +290,17 @@ module Ancestry
|
|
|
251
290
|
alias_method :only_child?, :is_only_child?
|
|
252
291
|
|
|
253
292
|
def sibling_of?(node)
|
|
254
|
-
|
|
293
|
+
ancestor_ids == node.ancestor_ids
|
|
255
294
|
end
|
|
256
295
|
|
|
257
296
|
# Descendants
|
|
258
297
|
|
|
259
|
-
def descendants
|
|
260
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
|
|
298
|
+
def descendants(depth_options = {})
|
|
299
|
+
self.class.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
|
|
261
300
|
end
|
|
262
301
|
|
|
263
|
-
def descendant_ids
|
|
264
|
-
descendants(depth_options).pluck(self.
|
|
302
|
+
def descendant_ids(depth_options = {})
|
|
303
|
+
descendants(depth_options).pluck(self.class.primary_key)
|
|
265
304
|
end
|
|
266
305
|
|
|
267
306
|
def descendant_of?(node)
|
|
@@ -270,12 +309,12 @@ module Ancestry
|
|
|
270
309
|
|
|
271
310
|
# Indirects
|
|
272
311
|
|
|
273
|
-
def indirects
|
|
274
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
|
|
312
|
+
def indirects(depth_options = {})
|
|
313
|
+
self.class.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
|
|
275
314
|
end
|
|
276
315
|
|
|
277
|
-
def indirect_ids
|
|
278
|
-
indirects(depth_options).pluck(self.
|
|
316
|
+
def indirect_ids(depth_options = {})
|
|
317
|
+
indirects(depth_options).pluck(self.class.primary_key)
|
|
279
318
|
end
|
|
280
319
|
|
|
281
320
|
def indirect_of?(node)
|
|
@@ -284,12 +323,16 @@ module Ancestry
|
|
|
284
323
|
|
|
285
324
|
# Subtree
|
|
286
325
|
|
|
287
|
-
def subtree
|
|
288
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
|
|
326
|
+
def subtree(depth_options = {})
|
|
327
|
+
self.class.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
|
|
289
328
|
end
|
|
290
329
|
|
|
291
|
-
def subtree_ids
|
|
292
|
-
subtree(depth_options).pluck(self.
|
|
330
|
+
def subtree_ids(depth_options = {})
|
|
331
|
+
subtree(depth_options).pluck(self.class.primary_key)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def in_subtree_of?(node)
|
|
335
|
+
id == node.id || descendant_of?(node)
|
|
293
336
|
end
|
|
294
337
|
|
|
295
338
|
# Callback disabling
|
|
@@ -305,36 +348,35 @@ module Ancestry
|
|
|
305
348
|
defined?(@disable_ancestry_callbacks) && @disable_ancestry_callbacks
|
|
306
349
|
end
|
|
307
350
|
|
|
308
|
-
|
|
351
|
+
private
|
|
352
|
+
|
|
309
353
|
def unscoped_descendants
|
|
310
354
|
unscoped_where do |scope|
|
|
311
|
-
scope.where
|
|
355
|
+
scope.where(self.class.ancestry_base_class.descendant_conditions(self))
|
|
312
356
|
end
|
|
313
357
|
end
|
|
314
358
|
|
|
315
|
-
def
|
|
359
|
+
def unscoped_descendants_before_last_save
|
|
316
360
|
unscoped_where do |scope|
|
|
317
|
-
scope.where
|
|
361
|
+
scope.where(self.class.ancestry_base_class.descendant_before_last_save_conditions(self))
|
|
318
362
|
end
|
|
319
363
|
end
|
|
320
364
|
|
|
321
365
|
# works with after save context (hence before_last_save)
|
|
322
366
|
def unscoped_current_and_previous_ancestors
|
|
323
367
|
unscoped_where do |scope|
|
|
324
|
-
scope.where
|
|
368
|
+
scope.where(scope.primary_key => (ancestor_ids + ancestor_ids_before_last_save).uniq)
|
|
325
369
|
end
|
|
326
370
|
end
|
|
327
371
|
|
|
328
|
-
def unscoped_find
|
|
372
|
+
def unscoped_find(id)
|
|
329
373
|
unscoped_where do |scope|
|
|
330
|
-
scope.find
|
|
374
|
+
scope.find(id)
|
|
331
375
|
end
|
|
332
376
|
end
|
|
333
377
|
|
|
334
|
-
def unscoped_where
|
|
335
|
-
self.ancestry_base_class.unscoped_where
|
|
336
|
-
yield scope
|
|
337
|
-
end
|
|
378
|
+
def unscoped_where(&block)
|
|
379
|
+
self.class.ancestry_base_class.unscoped_where(&block)
|
|
338
380
|
end
|
|
339
381
|
end
|
|
340
382
|
end
|
data/lib/ancestry/locales/en.yml
CHANGED
|
@@ -10,7 +10,6 @@ en:
|
|
|
10
10
|
option_must_be_hash: "Options for has_ancestry must be in a hash."
|
|
11
11
|
unknown_option: "Unknown option for has_ancestry: %{key} => %{value}."
|
|
12
12
|
unknown_format: "Unknown ancestry format: %{value}."
|
|
13
|
-
named_scope_depth_cache: "Named scope '%{scope_name}' is only available when depth caching is enabled."
|
|
14
13
|
|
|
15
14
|
exclude_self: "%{class_name} cannot be a descendant of itself."
|
|
16
15
|
cannot_delete_descendants: "Cannot delete record because it has descendants."
|