Module: Ancestry::InstanceMethods

Defined in:
lib/ancestry/instance_methods.rb

Instance Method Summary collapse

Instance Method Details

#ancestor_of?(node) ⇒ Boolean



192
193
194
# File 'lib/ancestry/instance_methods.rb', line 192

def ancestor_of?(node)
  node.ancestor_ids.include?(id)
end

#ancestors(depth_options = {}) ⇒ Object



162
163
164
165
166
# File 'lib/ancestry/instance_methods.rb', line 162

def ancestors(depth_options = {})
  return self.class.ancestry_base_class.none unless has_parent?

  self.class.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
end

#ancestry_callbacks_disabled?Boolean



347
348
349
# File 'lib/ancestry/instance_methods.rb', line 347

def ancestry_callbacks_disabled?
  defined?(@disable_ancestry_callbacks) && @disable_ancestry_callbacks
end

#ancestry_changed?Boolean



138
139
140
141
142
143
# File 'lib/ancestry/instance_methods.rb', line 138

def ancestry_changed?
  column = self.class.ancestry_column.to_s
  # These methods return nil if there are no changes.
  # This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
  !!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
end

#ancestry_depth_of_descendantsObject

Validate that descendants' depths don't exceed max depth when moving them



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/ancestry/instance_methods.rb', line 11

def ancestry_depth_of_descendants
  return if new_record? || (respond_to?(:previously_new_record?) && previously_new_record?)
  return unless self.class.respond_to?(:depth_cache_column) && self.class.depth_cache_column

  column = self.class.depth_cache_column
  validator = self.class.validators_on(column).find do |v|
    v.is_a?(ActiveModel::Validations::NumericalityValidator) &&
      (v.options[:less_than_or_equal_to] || v.options[:less_than])
  end
  return unless validator

  max_depth = validator.options[:less_than_or_equal_to] || (validator.options[:less_than] - 1)

  old_value = attribute_in_database(self.class.ancestry_column)
  new_value = read_attribute(self.class.ancestry_column)
  depth_change = self.class.ancestry_depth_change(old_value, new_value)

  if depth_change > 0
    max_descendant_depth = unscoped_descendants.maximum(column) || attribute_in_database(column) || 0
    if max_descendant_depth + depth_change > max_depth
      errors.add(column, :less_than_or_equal_to, count: max_depth)
    end
  end
end

#ancestry_exclude_selfObject

Validate that the ancestors don't include itself



6
7
8
# File 'lib/ancestry/instance_methods.rb', line 6

def ancestry_exclude_self
  errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.model_name.human)) if ancestor_ids.include?(id)
end

#apply_orphan_strategy_adoptObject

make child elements of this node, child of its parent



75
76
77
78
79
80
81
82
83
# File 'lib/ancestry/instance_methods.rb', line 75

def apply_orphan_strategy_adopt
  return if ancestry_callbacks_disabled? || new_record?

  descendants.each do |descendant|
    descendant.without_ancestry_callbacks do
      descendant.update_attribute :ancestor_ids, (descendant.ancestor_ids.delete_if { |x| x == id })
    end
  end
end

#apply_orphan_strategy_destroyObject

destroy all descendants if orphan strategy is destroy



64
65
66
67
68
69
70
71
72
# File 'lib/ancestry/instance_methods.rb', line 64

def apply_orphan_strategy_destroy
  return if ancestry_callbacks_disabled? || new_record?

  unscoped_descendants.ordered_by_ancestry.reverse_order.each do |descendant|
    descendant.without_ancestry_callbacks do
      descendant.destroy
    end
  end
end

#apply_orphan_strategy_restrictObject

throw an exception if it has children



86
87
88
89
90
# File 'lib/ancestry/instance_methods.rb', line 86

def apply_orphan_strategy_restrict
  return if ancestry_callbacks_disabled? || new_record?

  raise(Ancestry::AncestryException, I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
end

#apply_orphan_strategy_rootifyObject

make all children root if orphan strategy is rootify



53
54
55
56
57
58
59
60
61
# File 'lib/ancestry/instance_methods.rb', line 53

def apply_orphan_strategy_rootify
  return if ancestry_callbacks_disabled? || new_record?

  unscoped_descendants.each do |descendant|
    descendant.without_ancestry_callbacks do
      descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
    end
  end
end

#cache_depthObject



188
189
190
# File 'lib/ancestry/instance_methods.rb', line 188

def cache_depth
  write_attribute self.class.ancestry_base_class.depth_cache_column, depth
end

#child_idsObject



254
255
256
# File 'lib/ancestry/instance_methods.rb', line 254

def child_ids
  children.pluck(self.class.primary_key)
end

#child_of?(node) ⇒ Boolean



268
269
270
# File 'lib/ancestry/instance_methods.rb', line 268

def child_of?(node)
  parent_id == node.id
end

#childrenObject

Children



250
251
252
# File 'lib/ancestry/instance_methods.rb', line 250

def children
  self.class.ancestry_base_class.children_of(self)
end

#decrease_parent_counter_cacheObject



109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ancestry/instance_methods.rb', line 109

def decrease_parent_counter_cache
  # @_trigger_destroy_callback comes from activerecord, which makes sure only once decrement when concurrent deletion.
  # but @_trigger_destroy_callback began after [email protected].
  # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L340
  # https://github.com/rails/rails/pull/14735
  # https://github.com/rails/rails/pull/27248
  return if defined?(@_trigger_destroy_callback) && !@_trigger_destroy_callback
  return if ancestry_callbacks_disabled?

  self.class.ancestry_base_class.decrement_counter counter_cache_column, parent_id
end

#depthObject



184
185
186
# File 'lib/ancestry/instance_methods.rb', line 184

def depth
  ancestor_ids.size
end

#descendant_ids(depth_options = {}) ⇒ Object



302
303
304
# File 'lib/ancestry/instance_methods.rb', line 302

def descendant_ids(depth_options = {})
  descendants(depth_options).pluck(self.class.primary_key)
end

#descendant_of?(node) ⇒ Boolean



306
307
308
# File 'lib/ancestry/instance_methods.rb', line 306

def descendant_of?(node)
  ancestor_ids.include?(node.id)
end

#descendants(depth_options = {}) ⇒ Object

Descendants



298
299
300
# File 'lib/ancestry/instance_methods.rb', line 298

def descendants(depth_options = {})
  self.class.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
end

#has_children?Boolean Also known as: children?



258
259
260
# File 'lib/ancestry/instance_methods.rb', line 258

def has_children?
  children.exists?
end

#has_parent?Boolean Also known as: ancestors?

Ancestors



133
134
135
# File 'lib/ancestry/instance_methods.rb', line 133

def has_parent?
  ancestor_ids.present?
end

#has_siblings?Boolean Also known as: siblings?



282
283
284
# File 'lib/ancestry/instance_methods.rb', line 282

def has_siblings?
  siblings.exists?
end

#in_subtree_of?(node) ⇒ Boolean



334
335
336
# File 'lib/ancestry/instance_methods.rb', line 334

def in_subtree_of?(node)
  id == node.id || descendant_of?(node)
end

#increase_parent_counter_cacheObject

Counter Cache



105
106
107
# File 'lib/ancestry/instance_methods.rb', line 105

def increase_parent_counter_cache
  self.class.ancestry_base_class.increment_counter counter_cache_column, parent_id
end

#indirect_ids(depth_options = {}) ⇒ Object



316
317
318
# File 'lib/ancestry/instance_methods.rb', line 316

def indirect_ids(depth_options = {})
  indirects(depth_options).pluck(self.class.primary_key)
end

#indirect_of?(node) ⇒ Boolean



320
321
322
# File 'lib/ancestry/instance_methods.rb', line 320

def indirect_of?(node)
  ancestor_ids[0..-2].include?(node.id)
end

#indirects(depth_options = {}) ⇒ Object

Indirects



312
313
314
# File 'lib/ancestry/instance_methods.rb', line 312

def indirects(depth_options = {})
  self.class.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
end

#is_childless?Boolean Also known as: childless?



263
264
265
# File 'lib/ancestry/instance_methods.rb', line 263

def is_childless?
  !has_children?
end

#is_only_child?Boolean Also known as: only_child?



287
288
289
# File 'lib/ancestry/instance_methods.rb', line 287

def is_only_child?
  !has_siblings?
end

#is_root?Boolean Also known as: root?



239
240
241
# File 'lib/ancestry/instance_methods.rb', line 239

def is_root?
  !has_parent?
end

#parentObject



213
214
215
216
217
218
219
# File 'lib/ancestry/instance_methods.rb', line 213

def parent
  if has_parent?
    unscoped_where do |scope|
      scope.find_by scope.primary_key => parent_id
    end
  end
end

#parent=(parent) ⇒ Object

currently parent= does not work in after save callbacks assuming that parent hasn't changed



200
201
202
# File 'lib/ancestry/instance_methods.rb', line 200

def parent=(parent)
  self.ancestor_ids = parent ? parent.path_ids : []
end

#parent_idObject



208
209
210
# File 'lib/ancestry/instance_methods.rb', line 208

def parent_id
  ancestor_ids.last if has_parent?
end

#parent_id=(new_parent_id) ⇒ Object



204
205
206
# File 'lib/ancestry/instance_methods.rb', line 204

def parent_id=(new_parent_id)
  self.parent = new_parent_id.present? ? unscoped_find(new_parent_id) : nil
end

#parent_of?(node) ⇒ Boolean



221
222
223
# File 'lib/ancestry/instance_methods.rb', line 221

def parent_of?(node)
  id == node.parent_id
end

#path(depth_options = {}) ⇒ Object



180
181
182
# File 'lib/ancestry/instance_methods.rb', line 180

def path(depth_options = {})
  self.class.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
end

#path_idsObject



168
169
170
# File 'lib/ancestry/instance_methods.rb', line 168

def path_ids
  ancestor_ids + [id]
end

#path_ids_before_last_saveObject



172
173
174
# File 'lib/ancestry/instance_methods.rb', line 172

def path_ids_before_last_save
  ancestor_ids_before_last_save + [id]
end

#path_ids_in_databaseObject



176
177
178
# File 'lib/ancestry/instance_methods.rb', line 176

def path_ids_in_database
  ancestor_ids_in_database + [id]
end

#rootObject



231
232
233
234
235
236
237
# File 'lib/ancestry/instance_methods.rb', line 231

def root
  if has_parent?
    unscoped_where { |scope| scope.find_by(scope.primary_key => root_id) } || self
  else
    self
  end
end

#root_idObject

Root



227
228
229
# File 'lib/ancestry/instance_methods.rb', line 227

def root_id
  has_parent? ? ancestor_ids.first : id
end

#root_of?(node) ⇒ Boolean



244
245
246
# File 'lib/ancestry/instance_methods.rb', line 244

def root_of?(node)
  id == node.root_id
end

#sane_ancestor_ids?Boolean



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/ancestry/instance_methods.rb', line 145

def sane_ancestor_ids?
  current_context, self.validation_context = validation_context, nil
  errors.clear

  attribute = self.class.ancestry_column
  ancestry_value = send(attribute)
  return true unless ancestry_value

  self.class.validators_on(attribute).each do |validator|
    validator.validate_each(self, attribute, ancestry_value)
  end
  ancestry_exclude_self
  errors.none?
ensure
  self.validation_context = current_context
end

#sibling_idsObject



278
279
280
# File 'lib/ancestry/instance_methods.rb', line 278

def sibling_ids
  siblings.pluck(self.class.primary_key)
end

#sibling_of?(node) ⇒ Boolean



292
293
294
# File 'lib/ancestry/instance_methods.rb', line 292

def sibling_of?(node)
  ancestor_ids == node.ancestor_ids
end

#siblingsObject

Siblings



274
275
276
# File 'lib/ancestry/instance_methods.rb', line 274

def siblings
  self.class.ancestry_base_class.siblings_of(self).where.not(self.class.primary_key => id)
end

#subtree(depth_options = {}) ⇒ Object

Subtree



326
327
328
# File 'lib/ancestry/instance_methods.rb', line 326

def subtree(depth_options = {})
  self.class.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
end

#subtree_ids(depth_options = {}) ⇒ Object



330
331
332
# File 'lib/ancestry/instance_methods.rb', line 330

def subtree_ids(depth_options = {})
  subtree(depth_options).pluck(self.class.primary_key)
end

#touch_ancestors_callbackObject

Touch each of this record's ancestors (after save)



93
94
95
96
97
98
99
100
101
102
# File 'lib/ancestry/instance_methods.rb', line 93

def touch_ancestors_callback
  if !ancestry_callbacks_disabled?
    # Touch each of the old *and* new ancestors
    unscoped_current_and_previous_ancestors.each do |ancestor|
      ancestor.without_ancestry_callbacks do
        ancestor.touch
      end
    end
  end
end

#update_descendants_with_new_ancestryObject

Update descendants with new ancestry (after update)



37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/ancestry/instance_methods.rb', line 37

def update_descendants_with_new_ancestry
  # If enabled and the new ancestry is sane ...
  # The only way the ancestry could be bad is via `update_attribute` with a bad value
  if !ancestry_callbacks_disabled? && sane_ancestor_ids?
    # ... for each descendant ...
    unscoped_descendants_before_last_save.each do |descendant|
      # ... replace old ancestry with new ancestry
      descendant.without_ancestry_callbacks do
        new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_before_last_save)
        descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
      end
    end
  end
end

#update_parent_counter_cacheObject



121
122
123
124
125
126
127
128
129
# File 'lib/ancestry/instance_methods.rb', line 121

def update_parent_counter_cache
  return unless saved_change_to_attribute?(self.class.ancestry_column)

  if (parent_id_was = parent_id_before_last_save)
    self.class.ancestry_base_class.decrement_counter counter_cache_column, parent_id_was
  end

  parent_id && increase_parent_counter_cache
end

#without_ancestry_callbacksObject

Callback disabling



340
341
342
343
344
345
# File 'lib/ancestry/instance_methods.rb', line 340

def without_ancestry_callbacks
  @disable_ancestry_callbacks = true
  yield
ensure
  @disable_ancestry_callbacks = false
end