module Ancestry::InstanceMethods

Public Instance Methods

ancestor_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 152
def ancestor_of?(node)
  node.ancestor_ids.include?(self.id)
end
ancestors(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 123
def ancestors depth_options = {}
  return self.ancestry_base_class.none unless has_parent?
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
end
ancestors?()
Also aliased as: parent_id?
Alias for: has_parent?
ancestry_callbacks_disabled?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 304
def ancestry_callbacks_disabled?
  defined?(@disable_ancestry_callbacks) && @disable_ancestry_callbacks
end
ancestry_changed?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 99
def ancestry_changed?
  column = self.ancestry_base_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_self() click to toggle source

Validate that the ancestors don't include itself

# File lib/ancestry/instance_methods.rb, line 4
def ancestry_exclude_self
  errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.name.humanize)) if ancestor_ids.include? self.id
end
apply_orphan_strategy() click to toggle source

Apply orphan strategy (before destroy - no changes)

# File lib/ancestry/instance_methods.rb, line 24
def apply_orphan_strategy
  if !ancestry_callbacks_disabled? && !new_record?
    case self.ancestry_base_class.orphan_strategy
    when :rootify # make all children root if orphan strategy is rootify
      unscoped_descendants.each do |descendant|
        descendant.without_ancestry_callbacks do
          descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
        end
      end
    when :destroy # destroy all descendants if orphan strategy is destroy
      unscoped_descendants.each do |descendant|
        descendant.without_ancestry_callbacks do
          descendant.destroy
        end
      end
    when :adopt # make child elements of this node, child of its parent
      descendants.each do |descendant|
        descendant.without_ancestry_callbacks do
          descendant.update_attribute :ancestor_ids, (descendant.ancestor_ids.delete_if { |x| x == self.id })
        end
      end
    when :restrict # throw an exception if it has children
      raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
    end
  end
end
cache_depth() click to toggle source
# File lib/ancestry/instance_methods.rb, line 148
def cache_depth
  write_attribute self.ancestry_base_class.depth_cache_column, depth
end
child_ids() click to toggle source
# File lib/ancestry/instance_methods.rb, line 214
def child_ids
  children.pluck(self.ancestry_base_class.primary_key)
end
child_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 228
def child_of?(node)
  self.parent_id == node.id
end
childless?()
Alias for: is_childless?
children() click to toggle source

Children

# File lib/ancestry/instance_methods.rb, line 210
def children
  self.ancestry_base_class.children_of(self)
end
children?()
Alias for: has_children?
decrease_parent_counter_cache() click to toggle source
# File lib/ancestry/instance_methods.rb, line 68
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 rails@5.1.0.alpha.
  # 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.ancestry_base_class.decrement_counter counter_cache_column, parent_id
end
depth() click to toggle source
# File lib/ancestry/instance_methods.rb, line 144
def depth
  ancestor_ids.size
end
descendant_ids(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 263
def descendant_ids depth_options = {}
  descendants(depth_options).pluck(self.ancestry_base_class.primary_key)
end
descendant_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 267
def descendant_of?(node)
  ancestor_ids.include?(node.id)
end
descendants(depth_options = {}) click to toggle source

Descendants

# File lib/ancestry/instance_methods.rb, line 259
def descendants depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
end
has_children?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 218
def has_children?
  self.children.exists?
end
Also aliased as: children?
has_parent?() click to toggle source

Ancestors

# File lib/ancestry/instance_methods.rb, line 94
def has_parent?
  ancestor_ids.present?
end
Also aliased as: ancestors?
has_siblings?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 243
def has_siblings?
  self.siblings.count > 1
end
Also aliased as: siblings?
increase_parent_counter_cache() click to toggle source

Counter Cache

# File lib/ancestry/instance_methods.rb, line 64
def increase_parent_counter_cache
  self.ancestry_base_class.increment_counter counter_cache_column, parent_id
end
indirect_ids(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 277
def indirect_ids depth_options = {}
  indirects(depth_options).pluck(self.ancestry_base_class.primary_key)
end
indirect_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 281
def indirect_of?(node)
  ancestor_ids[0..-2].include?(node.id)
end
indirects(depth_options = {}) click to toggle source

Indirects

# File lib/ancestry/instance_methods.rb, line 273
def indirects depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
end
is_childless?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 223
def is_childless?
  !has_children?
end
Also aliased as: childless?
is_only_child?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 248
def is_only_child?
  !has_siblings?
end
Also aliased as: only_child?
is_root?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 199
def is_root?
  !has_parent?
end
Also aliased as: root?
only_child?()
Alias for: is_only_child?
parent() click to toggle source
# File lib/ancestry/instance_methods.rb, line 173
def parent
  if has_parent?
    unscoped_where do |scope|
      scope.find_by scope.primary_key => parent_id
    end
  end
end
parent=(parent) click to toggle source

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

# File lib/ancestry/instance_methods.rb, line 160
def parent= parent
  self.ancestor_ids = parent ? parent.path_ids : []
end
parent_id() click to toggle source
# File lib/ancestry/instance_methods.rb, line 168
def parent_id
  ancestor_ids.last if has_parent?
end
parent_id=(new_parent_id) click to toggle source
# File lib/ancestry/instance_methods.rb, line 164
def parent_id= new_parent_id
  self.parent = new_parent_id.present? ? unscoped_find(new_parent_id) : nil
end
parent_id?()
Alias for: ancestors?
parent_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 181
def parent_of?(node)
  self.id == node.parent_id
end
path(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 140
def path depth_options = {}
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
end
path_ids() click to toggle source
# File lib/ancestry/instance_methods.rb, line 128
def path_ids
  ancestor_ids + [id]
end
path_ids_before_last_save() click to toggle source
# File lib/ancestry/instance_methods.rb, line 132
def path_ids_before_last_save
  ancestor_ids_before_last_save + [id]
end
path_ids_in_database() click to toggle source
# File lib/ancestry/instance_methods.rb, line 136
def path_ids_in_database
  ancestor_ids_in_database + [id]
end
root() click to toggle source
# File lib/ancestry/instance_methods.rb, line 191
def root
  if has_parent?
    unscoped_where { |scope| scope.find_by(scope.primary_key => root_id) } || self
  else
    self
  end
end
root?()
Alias for: is_root?
root_id() click to toggle source

Root

# File lib/ancestry/instance_methods.rb, line 187
def root_id
  has_parent? ? ancestor_ids.first : id
end
root_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 204
def root_of?(node)
  self.id == node.root_id
end
sane_ancestor_ids?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 106
def sane_ancestor_ids?
  current_context, self.validation_context = validation_context, nil
  errors.clear

  attribute = ancestry_base_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_ids() click to toggle source

NOTE: includes self

# File lib/ancestry/instance_methods.rb, line 239
def sibling_ids
  siblings.pluck(self.ancestry_base_class.primary_key)
end
sibling_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 253
def sibling_of?(node)
  self.ancestor_ids == node.ancestor_ids
end
siblings() click to toggle source

Siblings

# File lib/ancestry/instance_methods.rb, line 234
def siblings
  self.ancestry_base_class.siblings_of(self)
end
siblings?()
Alias for: has_siblings?
subtree(depth_options = {}) click to toggle source

Subtree

# File lib/ancestry/instance_methods.rb, line 287
def subtree depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
end
subtree_ids(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 291
def subtree_ids depth_options = {}
  subtree(depth_options).pluck(self.ancestry_base_class.primary_key)
end
touch_ancestors_callback() click to toggle source

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

# File lib/ancestry/instance_methods.rb, line 52
def touch_ancestors_callback
  if !ancestry_callbacks_disabled? && self.ancestry_base_class.touch_ancestors
    # 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_ancestry() click to toggle source

Update descendants with new ancestry (after update)

# File lib/ancestry/instance_methods.rb, line 9
def update_descendants_with_new_ancestry
  # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
  if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
    # ... for each descendant ...
    unscoped_descendants_before_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_cache() click to toggle source
# File lib/ancestry/instance_methods.rb, line 80
def update_parent_counter_cache
  changed = saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)

  return unless changed

  if parent_id_was = parent_id_before_last_save
    self.ancestry_base_class.decrement_counter counter_cache_column, parent_id_was
  end

  parent_id && increase_parent_counter_cache
end
without_ancestry_callbacks() { || ... } click to toggle source

Callback disabling

# File lib/ancestry/instance_methods.rb, line 297
def without_ancestry_callbacks
  @disable_ancestry_callbacks = true
  yield
ensure
  @disable_ancestry_callbacks = false
end

Private Instance Methods

unscoped_current_and_previous_ancestors() click to toggle source

works with after save context (hence before_last_save)

# File lib/ancestry/instance_methods.rb, line 322
def unscoped_current_and_previous_ancestors
  unscoped_where do |scope|
    scope.where scope.primary_key => (ancestor_ids + ancestor_ids_before_last_save).uniq
  end
end
unscoped_descendants() click to toggle source
# File lib/ancestry/instance_methods.rb, line 309
def unscoped_descendants
  unscoped_where do |scope|
    scope.where self.ancestry_base_class.descendant_conditions(self)
  end
end
unscoped_descendants_before_save() click to toggle source
# File lib/ancestry/instance_methods.rb, line 315
def unscoped_descendants_before_save
  unscoped_where do |scope|
    scope.where self.ancestry_base_class.descendant_before_save_conditions(self)
  end
end
unscoped_find(id) click to toggle source
# File lib/ancestry/instance_methods.rb, line 328
def unscoped_find id
  unscoped_where do |scope|
    scope.find id
  end
end
unscoped_where() { |scope| ... } click to toggle source
# File lib/ancestry/instance_methods.rb, line 334
def unscoped_where
  self.ancestry_base_class.unscoped_where do |scope|
    yield scope
  end
end