module DataMapper module Is module NestedSet ## # docs in the works # def is_nested_set(options={}) options = { :child_key => [ :parent_id ], :scope => [] }.merge(options) extend DataMapper::Is::NestedSet::ClassMethods include DataMapper::Is::NestedSet::InstanceMethods @nested_set_scope = options[:scope] @nested_set_parent = options[:child_key] property :lft, Integer, :writer => :private property :rgt, Integer, :writer => :private # a temporary fix. I need to filter. now I just use parent.children in self_and_siblings, which could # be cut down to 1 instead of 2 queries. this would be the other way, but seems hackish: # options[:child_key].each{|pname| property(pname, Integer) unless properties.detect{|p| p.name == pname}} belongs_to :parent, :model => self, :child_key => options[:child_key], :order => [ :lft ], :required => false has n, :children, :model => self, :child_key => options[:child_key], :order => [ :lft ] before :create do if !parent # TODO must change for nested sets root ? move_without_saving(:into => root) : move_without_saving(:to => 1) elsif parent && !lft move_without_saving(:into => parent) end end before :update do if nested_set_scope != original_nested_set_scope # TODO detach from old list first. many edge-cases here, need good testing self.lft, self.rgt = nil, nil #puts "#{root.inspect} - #{[nested_set_scope,original_nested_set_scope].inspect}" root ? move_without_saving(:into => root) : move_without_saving(:to => 1) elsif (parent && !lft) || (parent != ancestor) # if the parent is set, we try to move this into that parent, otherwise move into root. parent ? move_without_saving(:into => parent) : move_without_saving(:into => model.root) end end before :destroy do __send__(:detach) end after_class_method :inherited do |retval, target| target.instance_variable_set(:@nested_set_scope, @nested_set_scope.dup) target.instance_variable_set(:@nested_set_parent, @nested_set_parent.dup) end end module ClassMethods attr_reader :nested_set_scope, :nested_set_parent def adjust_gap!(scoped_set,at,adjustment) scoped_set.all(:rgt.gt => at).adjust!({ :rgt => adjustment }, true) scoped_set.all(:lft.gt => at).adjust!({ :lft => adjustment }, true) end ## # get the root of the tree. if sets are scoped, this will return false # def root # TODO scoping # what should this return if there is a scope? always false, or node if there is only one? roots.length > 1 ? false : first(nested_set_parent.zip([]).to_hash) end ## # not implemented # def roots # TODO scoping # TODO supply filtering-option? all(nested_set_parent.zip([]).to_hash) end ## # # def leaves # TODO scoping, how should it act? # TODO supply filtering-option? all(:conditions => [ 'rgt = lft + ?', 1 ], :order => [ :lft ]) end ## # rebuilds the parent/child relationships (parent_id) from nested set (left/right values) # def rebuild_tree_from_set all.each do |node| node.parent = node.ancestor node.save end end ## # rebuilds the nested set using parent/child relationships and a chosen order # def rebuild_set_from_tree(order = nil) # TODO pending end end module InstanceMethods ## # # @private def nested_set_scope model.base_model.nested_set_scope.map{ |p| [ p, attribute_get(p) ] }.to_hash end ## # # @private def original_nested_set_scope # TODO commit model.base_model.nested_set_scope.map{ |p| [ p, (property = properties[p]) && original_attributes.key?(property) ? original_attributes[property] : attribute_get(p) ] }.to_hash end ## # the whole nested set this node belongs to. served flat like a pancake! # def nested_set # TODO add option for serving it as a nested array model.base_model.all(nested_set_scope.merge(:order => [ :lft ])) end ## # move self / node to a position in the set. position can _only_ be changed through this # # @example [Usage] # * node.move :higher # moves node higher unless it is at the top of parent # * node.move :lower # moves node lower unless it is at the bottom of parent # * node.move :below => other # moves this node below other resource in the set # * node.move :into => other # same as setting a parent-relationship # # @param vector A symbol, or a key-value pair that describes the requested movement # # @option :higher move node higher # @option :highest move node to the top of the list (within its parent) # @option :lower move node lower # @option :lowest move node to the bottom of the list (within its parent) # @option :indent move node into sibling above # @option :outdent move node out below its current parent # @option :into move node into another node # @option :above move node above other node # @option :below move node below other node # @option :to move node to a specific location in the nested set # # @return returns false if it cannot move to the position, or if it is already there # @raise if node is asked to position itself into one of its descendants # @raise if node is unable to calculate a new position for the element # @see move_without_saving def move(vector) move_without_saving(vector) && save end ## # @see move def move_without_saving(vector) if vector.kind_of?(Hash) action, object = vector.keys[0], vector.values[0] else action = vector end changed_scope = nested_set_scope != original_nested_set_scope position = case action when :higher then left_sibling ? left_sibling.lft : nil # : "already at the top" when :highest then ancestor ? ancestor.lft + 1 : nil # : "is root, or has no parent" when :lower then right_sibling ? right_sibling.rgt + 1 : nil # : "already at the bottom" when :lowest then ancestor ? ancestor.rgt : nil # : "is root, or has no parent" when :indent then left_sibling ? left_sibling.rgt : nil # : "cannot find a sibling to indent into" when :outdent then ancestor ? ancestor.rgt + 1 : nil # : "is root, or has no parent" when :into then object ? object.rgt : nil # : "supply an object" when :above then object ? object.lft : nil # : "supply an object" when :below then object ? object.rgt + 1 : nil # : "supply an object" when :to then object ? object.to_i : nil # : "supply a number" end ## # raising an error whenever it couldnt move seems a bit harsh. want to return self for nesting. # if anyone has a good idea about how it should react when it cant set a valid position, # don't hesitate to find me in #datamapper, or send me an email at sindre -a- identu -dot- no # # raise UnableToPositionError unless position.is_a?(Integer) && position > 0 return false if !position || position < 1 # return false if you are trying to move this into another scope return false if [ :into, :above, :below ].include?(action) && nested_set_scope != object.nested_set_scope # if node is already in the requested position if lft == position || rgt == position - 1 self.parent = ancestor # must set this again, because it might have been changed by the user before move. return false end ## # if this node is already positioned we need to move it, and close the gap it leaves behind etc # otherwise we only need to open a gap in the set, and smash that buggar in # if lft && rgt # raise exception if node is trying to move into one of its descendants (infinite loop, spacetime will warp) raise RecursiveNestingError if position > lft && position < rgt # find out how wide this node is, as we need to make a gap large enough for it to fit in gap = rgt - lft + 1 # make a gap at position, that is as wide as this node model.base_model.adjust_gap!(nested_set, position - 1, gap) eager_props = model.properties.values_at(:lft, :rgt) # FIXME don't use @api private # offset this node (and all its descendants) to the right position eager_load(eager_props) old_position = lft offset = position - old_position nested_set.all(:rgt => lft..rgt).adjust!({ :lft => offset, :rgt => offset }, true) # close the gap this movement left behind. model.base_model.adjust_gap!(nested_set, old_position, -gap) # FIXME don't use @api private eager_load(eager_props) else # make a gap where the new node can be inserted model.base_model.adjust_gap!(nested_set, position - 1, 2) # set the position fields self.lft, self.rgt = position, position + 1 end self.parent = ancestor end ## # get the level of this node, where 0 is root. temporary solution # # @return def level # TODO make a level-property that is cached and intelligently adjusted when saving objects ancestors.length end ## # get all ancestors of this node, up to (and including) self # # @return def self_and_ancestors nested_set.all(:lft.lte => lft, :rgt.gte => rgt) end ## # get all ancestors of this node # # @return collection of all parents, with root as first item # @see #self_and_ancestors def ancestors nested_set.all(:lft.lt => lft, :rgt.gt => rgt) #self_and_ancestors.reject{|r| r.key == key } # because identitymap is not used in console end ## # get the parent of this node. Same as #parent, but finds it from lft/rgt instead of parent-key # # @return returns the parent-object, or nil if this is root/detached def ancestor ancestors.last end ## # get the root this node belongs to. this will atm always be the same as Resource.root, but has a # meaning when scoped sets is implemented # # @return def root nested_set.first end ## # check if this node is a root # def root? !parent && !new_record? end ## # get all descendants of this node, including self # # @return flat collection, sorted according to nested_set positions def self_and_descendants # TODO supply filtering-option? nested_set.all(:lft => lft..rgt) end ## # get all descendants of this node # # @return flat collection, sorted according to nested_set positions # @see #self_and_descendants def descendants # TODO add argument for returning as a nested array. # TODO supply filtering-option? nested_set.all(:lft => (lft + 1)..(rgt - 1)) end ## # get all descendants of this node that does not have any children # # @return def leaves # TODO supply filtering-option? nested_set.all(:lft => (lft + 1)..rgt, :conditions => [ 'rgt = lft + ?', 1 ]) end ## # check if this node is a leaf (does not have subnodes). # use this instead ofdescendants.empty? def leaf? rgt - lft == 1 end ## # get all siblings of this node, and include self # # @return def self_and_siblings parent ? parent.children : [ self ] end ## # get all siblings of this node # # @return # @see #self_and_siblings def siblings # TODO find a way to return this as a collection? # TODO supply filtering-option? self_and_siblings.reject { |r| r.key == key } # because identitymap is not used in console end ## # get sibling to the left of/above this node in the nested tree # # @return the resource to the left, or nil if self is leftmost # @see #self_and_siblings def left_sibling self_and_siblings.detect { |v| v.rgt == lft - 1 } end ## # get sibling to the right of/above this node in the nested tree # # @return the resource to the right, or nil if self is rightmost # @see #self_and_siblings def right_sibling self_and_siblings.detect { |v| v.lft == rgt + 1 } end private def detach unless lft.nil? || rgt.nil? offset = lft - rgt - 1 model.base_model.adjust_gap!(nested_set, rgt, offset) end self.lft, self.rgt = nil, nil end end class UnableToPositionError < StandardError; end class RecursiveNestingError < StandardError; end Model.send(:include, self) end # NestedSet end # Is end # DataMapper