module Ancestry::InstanceMethods
def _counter_cache_column
def _counter_cache_column self.ancestry_base_class.counter_cache_column.to_s end
def ancestor_of?(node)
def ancestor_of?(node) node.ancestor_ids.include?(self.id) end
def ancestors depth_options = {}
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
def ancestry_callbacks_disabled?
def ancestry_callbacks_disabled? defined?(@disable_ancestry_callbacks) && @disable_ancestry_callbacks end
def ancestry_changed?
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
def ancestry_exclude_self
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
def apply_orphan_strategy
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
def cache_depth
def cache_depth write_attribute self.ancestry_base_class.depth_cache_column, depth end
def child_ids
def child_ids children.pluck(self.ancestry_base_class.primary_key) end
def child_of?(node)
def child_of?(node) self.parent_id == node.id end
def children
def children self.ancestry_base_class.children_of(self) end
def decrease_parent_counter_cache
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
def depth
def depth ancestor_ids.size end
def descendant_ids depth_options = {}
def descendant_ids depth_options = {} descendants(depth_options).pluck(self.ancestry_base_class.primary_key) end
def descendant_of?(node)
def descendant_of?(node) ancestor_ids.include?(node.id) end
def descendants depth_options = {}
def descendants depth_options = {} self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self) end
def has_children?
def has_children? self.children.exists? end
def has_parent?
def has_parent? ancestor_ids.present? end
def has_siblings?
def has_siblings? self.siblings.count > 1 end
def increase_parent_counter_cache
def increase_parent_counter_cache self.ancestry_base_class.increment_counter _counter_cache_column, parent_id end
def indirect_ids depth_options = {}
def indirect_ids depth_options = {} indirects(depth_options).pluck(self.ancestry_base_class.primary_key) end
def indirect_of?(node)
def indirect_of?(node) ancestor_ids[0..-2].include?(node.id) end
def indirects depth_options = {}
def indirects depth_options = {} self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self) end
def is_childless?
def is_childless? !has_children? end
def is_only_child?
def is_only_child? !has_siblings? end
def is_root?
def is_root? !has_parent? end
def parent
def parent if has_parent? unscoped_where do |scope| scope.find_by id: parent_id end end end
def parent= parent
currently parent= does not work in after save callbacks
def parent= parent self.ancestor_ids = parent ? parent.path_ids : [] end
def parent_id
def parent_id ancestor_ids.last if has_parent? end
def parent_id= new_parent_id
def parent_id= new_parent_id self.parent = new_parent_id.present? ? unscoped_find(new_parent_id) : nil end
def parent_of?(node)
def parent_of?(node) self.id == node.parent_id end
def path depth_options = {}
def path depth_options = {} self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self) end
def path_ids
def path_ids ancestor_ids + [id] end
def path_ids_in_database
def path_ids_in_database ancestor_ids_in_database + [id] end
def root
def root if has_parent? unscoped_where { |scope| scope.find_by(id: root_id) } || self else self end end
def root_id
def root_id has_parent? ? ancestor_ids.first : id end
def root_of?(node)
def root_of?(node) self.id == node.root_id end
def sane_ancestor_ids?
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
def sibling_ids
def sibling_ids siblings.pluck(self.ancestry_base_class.primary_key) end
def sibling_of?(node)
def sibling_of?(node) self.ancestor_ids == node.ancestor_ids end
def siblings
def siblings self.ancestry_base_class.siblings_of(self) end
def subtree depth_options = {}
def subtree depth_options = {} self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self) end
def subtree_ids depth_options = {}
def subtree_ids depth_options = {} subtree(depth_options).pluck(self.ancestry_base_class.primary_key) end
def touch_ancestors_callback
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
def unscoped_current_and_previous_ancestors
def unscoped_current_and_previous_ancestors unscoped_where do |scope| scope.where id: (ancestor_ids + ancestor_ids_before_last_save).uniq end end
def unscoped_descendants
def unscoped_descendants unscoped_where do |scope| scope.where self.ancestry_base_class.descendant_conditions(self) end end
def unscoped_find id
def unscoped_find id unscoped_where do |scope| scope.find id end end
def unscoped_where
def unscoped_where self.ancestry_base_class.unscoped_where do |scope| yield scope end end
def update_descendants_with_new_ancestry
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.each do |descendant| # ... replace old ancestry with new ancestry descendant.without_ancestry_callbacks do new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_in_database) descendant.update_attribute(:ancestor_ids, new_ancestor_ids) end end end end
def update_parent_counter_cache
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 && self.ancestry_base_class.increment_counter(_counter_cache_column, parent_id) end
def without_ancestry_callbacks
def without_ancestry_callbacks @disable_ancestry_callbacks = true yield @disable_ancestry_callbacks = false end