module Ancestry::InstanceMethods

Constants

ANCESTRY_DELIMITER
BEFORE_LAST_SAVE_SUFFIX
IN_DATABASE_SUFFIX

Public Instance Methods

_counter_cache_column() click to toggle source
# File lib/ancestry/instance_methods.rb, line 129
def _counter_cache_column
  self.ancestry_base_class.counter_cache_column.to_s
end
ancestor_conditions() click to toggle source
# File lib/ancestry/instance_methods.rb, line 156
def ancestor_conditions
  self.ancestry_base_class.ancestor_conditions(self)
end
ancestor_ids() click to toggle source
# File lib/ancestry/instance_methods.rb, line 152
def ancestor_ids
  parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
end
ancestor_ids_before_last_save() click to toggle source
# File lib/ancestry/instance_methods.rb, line 175
def ancestor_ids_before_last_save
  parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
end
ancestor_ids_was() click to toggle source

deprecated - probably don't want to use anymore

# File lib/ancestry/instance_methods.rb, line 171
def ancestor_ids_was
  parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}_was"))
end
ancestor_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 206
def ancestor_of?(node)
  node.ancestor_ids.include?(self.id)
end
ancestor_was_conditions() click to toggle source

deprecate

# File lib/ancestry/instance_methods.rb, line 166
def ancestor_was_conditions
  {primary_key_with_table => ancestor_ids_before_last_save}
end
ancestors(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 160
def ancestors depth_options = {}
  return self.ancestry_base_class.none unless ancestors?
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
end
ancestors?() click to toggle source

Ancestors

# File lib/ancestry/instance_methods.rb, line 135
def ancestors?
  # ancestor_ids.present?
  read_attribute(self.ancestry_base_class.ancestry_column).present?
end
Also aliased as: has_parent?
ancestry_callbacks_disabled?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 371
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 141
def ancestry_changed?
  column = self.ancestry_base_class.ancestry_column.to_s
  if ActiveRecord::VERSION::STRING >= '5.1.0'
    # 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))
  else
    changed.include?(column)
  end
end
ancestry_exclude_self() click to toggle source

Validate that the ancestors don't include itself

# File lib/ancestry/instance_methods.rb, line 7
def ancestry_exclude_self
  errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") 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 34
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
          new_ancestry = if descendant.ancestry == child_ancestry
            nil
          else
            # child_ancestry did not change so child_ancestry_was will work here
            descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
          end
          descendant.update_attribute descendant.class.ancestry_column, new_ancestry
        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
          new_ancestry = descendant.ancestor_ids.delete_if { |x| x == self.id }.join("/")
          # check for empty string if it's then set to nil
          new_ancestry = nil if new_ancestry.empty?
          descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
        end
      end
    when :restrict # throw an exception if it has children
      raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
    end
  end
end
cache_depth() click to toggle source
# File lib/ancestry/instance_methods.rb, line 202
def cache_depth
  write_attribute self.ancestry_base_class.depth_cache_column, depth
end
child_ancestry() click to toggle source

The ancestry value for this record's children (before save) This is technically child_ancestry_was

# File lib/ancestry/instance_methods.rb, line 84
def child_ancestry
  # New records cannot have children
  raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?

  if self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}").blank?
    id.to_s
  else
    "#{self.send "#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"}/#{id}"
  end
end
child_conditions() click to toggle source

Children

# File lib/ancestry/instance_methods.rb, line 259
def child_conditions
  self.ancestry_base_class.child_conditions(self)
end
child_ids() click to toggle source
# File lib/ancestry/instance_methods.rb, line 267
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 281
def child_of?(node)
  self.parent_id == node.id
end
childless?()
Alias for: is_childless?
children() click to toggle source
# File lib/ancestry/instance_methods.rb, line 263
def children
  self.ancestry_base_class.where child_conditions
end
children?()
Alias for: has_children?
decrease_parent_counter_cache() click to toggle source
# File lib/ancestry/instance_methods.rb, line 100
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.class.decrement_counter _counter_cache_column, parent_id
end
depth() click to toggle source
# File lib/ancestry/instance_methods.rb, line 198
def depth
  ancestor_ids.size
end
descendant_conditions() click to toggle source

Descendants

# File lib/ancestry/instance_methods.rb, line 315
def descendant_conditions
  self.ancestry_base_class.descendant_conditions(self)
end
descendant_ids(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 323
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 327
def descendant_of?(node)
  ancestor_ids.include?(node.id)
end
descendants(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 319
def descendants depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
end
has_children?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 271
def has_children?
  self.children.exists?
end
Also aliased as: children?
has_parent?()
Alias for: ancestors?
has_siblings?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 299
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 96
def increase_parent_counter_cache
  self.class.increment_counter _counter_cache_column, parent_id
end
indirect_conditions() click to toggle source

Indirects

# File lib/ancestry/instance_methods.rb, line 333
def indirect_conditions
  self.ancestry_base_class.indirect_conditions(self)
end
indirect_ids(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 341
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 345
def indirect_of?(node)
  ancestor_ids[0..-2].include?(node.id)
end
indirects(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 337
def indirects depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where indirect_conditions
end
is_childless?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 276
def is_childless?
  !has_children?
end
Also aliased as: childless?
is_only_child?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 304
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 248
def is_root?
  read_attribute(self.ancestry_base_class.ancestry_column).blank?
end
Also aliased as: root?
only_child?()
Alias for: is_only_child?
parent() click to toggle source
# File lib/ancestry/instance_methods.rb, line 226
def parent
  unscoped_find(parent_id) if ancestors?
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 214
def parent= parent
  write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
end
parent_id() click to toggle source
# File lib/ancestry/instance_methods.rb, line 222
def parent_id
  ancestor_ids.last if ancestors?
end
parent_id=(new_parent_id) click to toggle source
# File lib/ancestry/instance_methods.rb, line 218
def parent_id= new_parent_id
  self.parent = new_parent_id.present? ? unscoped_find(new_parent_id) : nil
end
parent_id?() click to toggle source
# File lib/ancestry/instance_methods.rb, line 230
def parent_id?
  ancestors?
end
parent_id_before_last_save() click to toggle source
# File lib/ancestry/instance_methods.rb, line 179
def parent_id_before_last_save
  ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
  return unless ancestry_was.present?

  ancestry_was.split(ANCESTRY_DELIMITER).last.to_i
end
parent_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 234
def parent_of?(node)
  self.id == node.parent_id
end
path(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 194
def path depth_options = {}
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
end
path_conditions() click to toggle source
# File lib/ancestry/instance_methods.rb, line 190
def path_conditions
  self.ancestry_base_class.path_conditions(self)
end
path_ids() click to toggle source
# File lib/ancestry/instance_methods.rb, line 186
def path_ids
  ancestor_ids + [id]
end
root() click to toggle source
# File lib/ancestry/instance_methods.rb, line 244
def root
  ancestors? ? unscoped_find(root_id) : self
end
root?()
Alias for: is_root?
root_id() click to toggle source

Root

# File lib/ancestry/instance_methods.rb, line 240
def root_id
  ancestors? ? ancestor_ids.first : id
end
root_of?(node) click to toggle source
# File lib/ancestry/instance_methods.rb, line 253
def root_of?(node)
  self.id == node.root_id
end
sibling_conditions() click to toggle source

Siblings

# File lib/ancestry/instance_methods.rb, line 287
def sibling_conditions
  self.ancestry_base_class.sibling_conditions(self)
end
sibling_ids() click to toggle source
# File lib/ancestry/instance_methods.rb, line 295
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 309
def sibling_of?(node)
  self.ancestry == node.ancestry
end
siblings() click to toggle source
# File lib/ancestry/instance_methods.rb, line 291
def siblings
  self.ancestry_base_class.where sibling_conditions
end
siblings?()
Alias for: has_siblings?
subtree(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 355
def subtree depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
end
subtree_conditions() click to toggle source

Subtree

# File lib/ancestry/instance_methods.rb, line 351
def subtree_conditions
  self.ancestry_base_class.subtree_conditions(self)
end
subtree_ids(depth_options = {}) click to toggle source
# File lib/ancestry/instance_methods.rb, line 359
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 71
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 (before save)

# File lib/ancestry/instance_methods.rb, line 12
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_ancestry?
    # ... for each descendant ...
    unscoped_descendants.each do |descendant|
      # ... replace old ancestry with new ancestry
      descendant.without_ancestry_callbacks do
        descendant.update_attribute(
          self.ancestry_base_class.ancestry_column,
          descendant.read_attribute(descendant.class.ancestry_column).gsub(
            # child_ancestry_was
            /^#{self.child_ancestry}/,
            # future child_ancestry
            if ancestors? then "#{read_attribute self.class.ancestry_column }/#{id}" else id.to_s end
          )
        )
      end
    end
  end
end
update_parent_counter_cache() click to toggle source
# File lib/ancestry/instance_methods.rb, line 112
def update_parent_counter_cache
  changed =
    if ActiveRecord::VERSION::STRING >= '5.1.0'
      saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
    else
      ancestry_changed?
    end

  return unless changed

  if parent_id_was = parent_id_before_last_save
    self.class.decrement_counter _counter_cache_column, parent_id_was
  end

  parent_id && self.class.increment_counter(_counter_cache_column, parent_id)
end
without_ancestry_callbacks() { || ... } click to toggle source

Callback disabling

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

Private Instance Methods

parse_ancestry_column(obj) click to toggle source
# File lib/ancestry/instance_methods.rb, line 378
def parse_ancestry_column obj
  return [] unless obj
  obj_ids = obj.split(ANCESTRY_DELIMITER)
  self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
end
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 391
def unscoped_current_and_previous_ancestors
  unscoped_where do |scope|
    scope.where id: (ancestor_ids + ancestor_ids_before_last_save).uniq
  end
end
unscoped_descendants() click to toggle source
# File lib/ancestry/instance_methods.rb, line 384
def unscoped_descendants
  unscoped_where do |scope|
    scope.where descendant_conditions
  end
end
unscoped_find(id) click to toggle source
# File lib/ancestry/instance_methods.rb, line 397
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 403
def unscoped_where
  self.ancestry_base_class.unscoped_where do |scope|
    yield scope
  end
end