collectiveidea/awesome_nested_set

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

Summary

Maintainability
A
2 hrs
Test Coverage
require 'awesome_nested_set/move'

module CollectiveIdea #:nodoc:
  module Acts #:nodoc:
    module NestedSet #:nodoc:
      module Model
        module Movable

          def move_possible?(target)
            self != target && # Can't target self
              same_scope?(target) && # can't be in different scopes
              # detect impossible move
              within_bounds?(target.left, target.left) &&
              within_bounds?(target.right, target.right)
          end

          # Shorthand method for finding the left sibling and moving to the left of it.
          def move_left
            move_to_left_of left_sibling
          end

          # Shorthand method for finding the right sibling and moving to the right of it.
          def move_right
            move_to_right_of right_sibling
          end

          # Move the node to the left of another node
          def move_to_left_of(node)
            move_to node, :left
          end

          # Move the node to the right of another node
          def move_to_right_of(node)
            move_to node, :right
          end

          # Move the node to the child of another node
          def move_to_child_of(node)
            if node == :root
              move_to_root
            else
              move_to node, :child
            end
          end

          # Move the node to the child of another node with specify index
          def move_to_child_with_index(node, index)
            siblings = node == :root ? roots : node.children
            if siblings.empty?
              move_to_child_of(node)
            elsif siblings.count == index
              move_to_right_of(siblings.last)
            else
              my_position = siblings.index(self)
              if my_position && my_position < index
                # e.g. if self is at position 0 and we want to move self to position 1 then self
                # needs to move to the *right* of the node at position 1. That's because the node
                # that is currently at position 1 will be at position 0 after the move completes.
                move_to_right_of(siblings[index])
              elsif my_position && my_position == index
                # do nothing. already there.
              else
                move_to_left_of(siblings[index])
              end
            end
          end

          # Move the node to root nodes
          def move_to_root
            move_to self, :root
          end

          # Order children in a nested set by an attribute
          # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
          # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
          def move_to_ordered_child_of(parent, order_attribute, ascending = true)
            self.move_to_root and return unless parent

            left_neighbor = find_left_neighbor(parent, order_attribute, ascending)
            self.move_to_child_of(parent)

            return unless parent.children.many?

            if left_neighbor
              self.move_to_right_of(left_neighbor)
            else # Self is the left most node.
              self.move_to_left_of(parent.children[0])
            end
          end

          # Find the node immediately to the left of this node.
          def find_left_neighbor(parent, order_attribute, ascending)
            left = nil
            parent.children.each do |n|
              if ascending
                left = n if n.send(order_attribute) < self.send(order_attribute)
              else
                left = n if n.send(order_attribute) > self.send(order_attribute)
              end
            end
            left
          end

          def move_to(target, position)
            prevent_unpersisted_move

            run_callbacks :move do
              in_tenacious_transaction do
                target = reload_target(target, position)
                self.reload_nested_set

                Move.new(target, position, self).move
                update_counter_cache
              end
              after_move_to(target, position)
            end
          end

          protected

          def after_move_to(target, position)
            target.reload_nested_set if target
            self.set_depth_for_self_and_descendants!
            self.reload_nested_set
          end

          def move_to_new_parent
            if @move_to_new_parent_id.nil?
              move_to_root
            elsif @move_to_new_parent_id
              move_to_child_of(@move_to_new_parent_id)
            end
          end

          def out_of_bounds?(left_bound, right_bound)
            left <= left_bound && right >= right_bound
          end

          def prevent_unpersisted_move
            if self.new_record?
              raise ActiveRecord::ActiveRecordError, "You cannot move a new node"
            end
          end

          def within_bounds?(left_bound, right_bound)
            !out_of_bounds?(left_bound, right_bound)
          end
        end
      end
    end
  end
end