collectiveidea/awesome_nested_set

View on GitHub
lib/awesome_nested_set/model/prunable.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module CollectiveIdea #:nodoc:
  module Acts #:nodoc:
    module NestedSet #:nodoc:
      module Model
        module Prunable

          # Prunes a branch off of the tree, shifting all of the elements on the right
          # back to the left so the counts still work.
          def destroy_descendants
            return if right.nil? || left.nil? || skip_before_destroy

            in_tenacious_transaction do
              # Rescue from +ActiveRecord::RecordNotFound+ error as there may be a case
              # that an +object+ has already been destroyed by its parent, but objects that are
              # in memory are not aware about this.
              begin
                reload_nested_set
              rescue ActiveRecord::RecordNotFound
                self.skip_before_destroy = true
                return true
              end
              # select the rows in the model that extend past the deletion point and apply a lock
              nested_set_scope.right_of(left).select(primary_id).lock(true)

              return false unless destroy_or_delete_descendants

              # update lefts and rights for remaining nodes
              update_siblings_for_remaining_nodes

              # Reload is needed because children may have updated their parent (self) during deletion.
              reload

              # Don't allow multiple calls to destroy to corrupt the set
              self.skip_before_destroy = true
            end
          end

          # Use reverse to delete from deepest child to parent in order to respect any possible foreign keys
          def decendants_to_destroy_in_order
            descendants.reverse
          end

          def destroy_or_delete_descendants
            if acts_as_nested_set_options[:dependent] == :destroy
              decendants_to_destroy_in_order.each do |model|
                model.skip_before_destroy = true
                model.destroy
              end
            elsif acts_as_nested_set_options[:dependent] == :restrict_with_exception
              raise ActiveRecord::DeleteRestrictionError.new(:children) unless leaf?
              return true
            elsif acts_as_nested_set_options[:dependent] == :restrict_with_error
              unless leaf?
                record = self.class.human_attribute_name(:children).downcase
                if Rails::VERSION::MAJOR < 5
                  errors.add(:base, :"restrict_dependent_destroy.many", record: record)
                  return false
                else
                  errors.add(:base, :"restrict_dependent_destroy.has_many", record: record)
                  throw :abort
                end
              end
              return true
             elsif acts_as_nested_set_options[:dependent] == :nullify
               descendants.update_all(parent_column_name => nil)
             else
              descendants.delete_all
            end
          end

          def update_siblings_for_remaining_nodes
            update_siblings(:left)
            update_siblings(:right)
          end

          def update_siblings(direction)
            full_column_name = send("quoted_#{direction}_column_full_name")
            column_name = send("quoted_#{direction}_column_name")

            nested_set_scope.where(["#{full_column_name} > ?", right]).
              update_all(["#{column_name} = (#{column_name} - ?)", diff])
          end

          def diff
            right - left + 1
          end
        end
      end
    end
  end
end