Module: Ancestry::InstanceMethods

Defined in:
lib/ancestry/instance_methods.rb

Instance Method Summary collapse

Instance Method Details

#ancestor_of?(node) ⇒ Boolean

Returns:



166
167
168
# File 'lib/ancestry/instance_methods.rb', line 166

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

#ancestors(depth_options = {}) ⇒ Object



136
137
138
139
140
# File 'lib/ancestry/instance_methods.rb', line 136

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

Returns:



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

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

#ancestry_changed?Boolean

Returns:



112
113
114
115
116
117
# File 'lib/ancestry/instance_methods.rb', line 112

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_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



49
50
51
52
53
54
55
56
57
# File 'lib/ancestry/instance_methods.rb', line 49

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



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

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

Raises:



60
61
62
63
64
# File 'lib/ancestry/instance_methods.rb', line 60

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



27
28
29
30
31
32
33
34
35
# File 'lib/ancestry/instance_methods.rb', line 27

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



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

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

#child_idsObject



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

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

#child_of?(node) ⇒ Boolean

Returns:



242
243
244
# File 'lib/ancestry/instance_methods.rb', line 242

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

#childrenObject

Children



224
225
226
# File 'lib/ancestry/instance_methods.rb', line 224

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

#decrease_parent_counter_cacheObject



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/ancestry/instance_methods.rb', line 83

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



158
159
160
# File 'lib/ancestry/instance_methods.rb', line 158

def depth
  ancestor_ids.size
end

#descendant_ids(depth_options = {}) ⇒ Object



276
277
278
# File 'lib/ancestry/instance_methods.rb', line 276

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

#descendant_of?(node) ⇒ Boolean

Returns:



280
281
282
# File 'lib/ancestry/instance_methods.rb', line 280

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

#descendants(depth_options = {}) ⇒ Object

Descendants



272
273
274
# File 'lib/ancestry/instance_methods.rb', line 272

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?

Returns:



232
233
234
# File 'lib/ancestry/instance_methods.rb', line 232

def has_children?
  children.exists?
end

#has_parent?Boolean Also known as: ancestors?

Ancestors

Returns:



107
108
109
# File 'lib/ancestry/instance_methods.rb', line 107

def has_parent?
  ancestor_ids.present?
end

#has_siblings?Boolean Also known as: siblings?

Returns:



256
257
258
# File 'lib/ancestry/instance_methods.rb', line 256

def has_siblings?
  siblings.exists?
end

#in_subtree_of?(node) ⇒ Boolean

Returns:



308
309
310
# File 'lib/ancestry/instance_methods.rb', line 308

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

#increase_parent_counter_cacheObject

Counter Cache



79
80
81
# File 'lib/ancestry/instance_methods.rb', line 79

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

#indirect_ids(depth_options = {}) ⇒ Object



290
291
292
# File 'lib/ancestry/instance_methods.rb', line 290

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

#indirect_of?(node) ⇒ Boolean

Returns:



294
295
296
# File 'lib/ancestry/instance_methods.rb', line 294

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

#indirects(depth_options = {}) ⇒ Object

Indirects



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

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?

Returns:



237
238
239
# File 'lib/ancestry/instance_methods.rb', line 237

def is_childless?
  !has_children?
end

#is_only_child?Boolean Also known as: only_child?

Returns:



261
262
263
# File 'lib/ancestry/instance_methods.rb', line 261

def is_only_child?
  !has_siblings?
end

#is_root?Boolean Also known as: root?

Returns:



213
214
215
# File 'lib/ancestry/instance_methods.rb', line 213

def is_root?
  !has_parent?
end

#parentObject



187
188
189
190
191
192
193
# File 'lib/ancestry/instance_methods.rb', line 187

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



174
175
176
# File 'lib/ancestry/instance_methods.rb', line 174

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

#parent_idObject



182
183
184
# File 'lib/ancestry/instance_methods.rb', line 182

def parent_id
  ancestor_ids.last if has_parent?
end

#parent_id=(new_parent_id) ⇒ Object



178
179
180
# File 'lib/ancestry/instance_methods.rb', line 178

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

#parent_of?(node) ⇒ Boolean

Returns:



195
196
197
# File 'lib/ancestry/instance_methods.rb', line 195

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

#path(depth_options = {}) ⇒ Object



154
155
156
# File 'lib/ancestry/instance_methods.rb', line 154

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

#path_idsObject



142
143
144
# File 'lib/ancestry/instance_methods.rb', line 142

def path_ids
  ancestor_ids + [id]
end

#path_ids_before_last_saveObject



146
147
148
# File 'lib/ancestry/instance_methods.rb', line 146

def path_ids_before_last_save
  ancestor_ids_before_last_save + [id]
end

#path_ids_in_databaseObject



150
151
152
# File 'lib/ancestry/instance_methods.rb', line 150

def path_ids_in_database
  ancestor_ids_in_database + [id]
end

#rootObject



205
206
207
208
209
210
211
# File 'lib/ancestry/instance_methods.rb', line 205

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

#root_idObject

Root



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

def root_id
  has_parent? ? ancestor_ids.first : id
end

#root_of?(node) ⇒ Boolean

Returns:



218
219
220
# File 'lib/ancestry/instance_methods.rb', line 218

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

#sane_ancestor_ids?Boolean

Returns:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/ancestry/instance_methods.rb', line 119

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



252
253
254
# File 'lib/ancestry/instance_methods.rb', line 252

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

#sibling_of?(node) ⇒ Boolean

Returns:



266
267
268
# File 'lib/ancestry/instance_methods.rb', line 266

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

#siblingsObject

Siblings



248
249
250
# File 'lib/ancestry/instance_methods.rb', line 248

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

#subtree(depth_options = {}) ⇒ Object

Subtree



300
301
302
# File 'lib/ancestry/instance_methods.rb', line 300

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



304
305
306
# File 'lib/ancestry/instance_methods.rb', line 304

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)



67
68
69
70
71
72
73
74
75
76
# File 'lib/ancestry/instance_methods.rb', line 67

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)



11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/ancestry/instance_methods.rb', line 11

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



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

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



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

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