├── .rspec ├── Rakefile ├── .document ├── lib ├── mongoid_nested_set │ ├── version.rb │ ├── remove_order_by.rb │ ├── fields.rb │ ├── rebuild.rb │ ├── validation.rb │ ├── relations.rb │ ├── outline_number.rb │ ├── base.rb │ ├── document.rb │ └── update.rb └── mongoid_nested_set.rb ├── spec ├── models │ ├── circle_node.rb │ ├── square_node.rb │ ├── node_without_nested_set.rb │ ├── renamed_fields.rb │ ├── unscoped_node.rb │ ├── node.rb │ ├── numbering_node.rb │ ├── shape_node.rb │ └── test_document.rb ├── spec_helper.rb ├── matchers │ └── nestedset_pos.rb └── mongoid_nested_set_spec.rb ├── Gemfile ├── .gitignore ├── LICENSE.txt ├── mongoid_nested_set.gemspec └── README.markdown /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/version.rb: -------------------------------------------------------------------------------- 1 | module MongoidNestedSet 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/circle_node.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/shape_node" 2 | 3 | class CircleNode < ShapeNode 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/square_node.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/shape_node" 2 | 3 | class SquareNode < ShapeNode 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/node_without_nested_set.rb: -------------------------------------------------------------------------------- 1 | 2 | class NodeWithoutNestedSet 3 | include Mongoid::Document 4 | 5 | field :name 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/renamed_fields.rb: -------------------------------------------------------------------------------- 1 | 2 | class RenamedFields 3 | include Mongoid::Document 4 | acts_as_nested_set :parent_field => 'mother_id', :left_field => 'red', :right_field => 'black' 5 | 6 | field :name 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in mongoid_nested_set.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'rspec-expectations', "~> 2.0" 8 | gem 'rr' 9 | gem 'remarkable_mongoid' 10 | end 11 | -------------------------------------------------------------------------------- /spec/models/unscoped_node.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/test_document" 2 | 3 | class UnscopedNode 4 | include Mongoid::Document 5 | include Mongoid::Acts::NestedSet::TestDocument 6 | acts_as_nested_set 7 | 8 | field :name 9 | end 10 | -------------------------------------------------------------------------------- /spec/models/node.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/test_document" 2 | 3 | class Node 4 | include Mongoid::Document 5 | include Mongoid::Acts::NestedSet::TestDocument 6 | acts_as_nested_set :scope => :root_id 7 | 8 | field :name 9 | field :root_id, :type => Integer 10 | end 11 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/remove_order_by.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module Criterion 3 | module Ordering 4 | def remove_order_by 5 | @options[:sort] = nil 6 | self 7 | end 8 | end 9 | end 10 | 11 | class Criteria 12 | include Criterion::Ordering 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /spec/models/numbering_node.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/test_document" 2 | 3 | class NumberingNode 4 | include Mongoid::Document 5 | include Mongoid::Acts::NestedSet::TestDocument 6 | acts_as_nested_set :scope => :root_id, :outline_number_field => 'number' 7 | 8 | field :name 9 | field :root_id, :type => Integer 10 | end 11 | -------------------------------------------------------------------------------- /spec/models/shape_node.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/test_document" 2 | 3 | class ShapeNode 4 | include Mongoid::Document 5 | include Mongoid::Acts::NestedSet::TestDocument 6 | acts_as_nested_set 7 | 8 | field :name 9 | 10 | def test_set_attributes(attrs) 11 | @attributes.update(attrs) 12 | self 13 | end 14 | 15 | def self.test_set_dependent_option(val) 16 | self.acts_as_nested_set_options[:dependent] = val 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/models/test_document.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module TestDocument 4 | 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | base.send(:include, InstanceMethods) 8 | end 9 | 10 | 11 | module ClassMethods 12 | 13 | def test_set_dependent_option(val) 14 | self.acts_as_nested_set_options[:dependent] = val 15 | end 16 | 17 | end 18 | 19 | 20 | module InstanceMethods 21 | 22 | def test_set_attributes(attrs) 23 | attrs.each do |key, val| 24 | key = key.to_s 25 | if Mongoid.allow_dynamic_fields || 26 | fields.keys.any? { |k| k.to_s == key } || 27 | associations.any? { |a| a[0].to_s == key || a[1].foreign_key.to_s == key } 28 | @attributes[key] = fields[key].mongoize(val) 29 | end 30 | end 31 | self 32 | end 33 | 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | # rcov generated 3 | coverage 4 | 5 | # rdoc generated 6 | rdoc 7 | 8 | # yard generated 9 | doc 10 | .yardoc 11 | 12 | # bundler 13 | .bundle 14 | 15 | # jeweler generated 16 | pkg 17 | 18 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 19 | # 20 | # * Create a file at ~/.gitignore 21 | # * Include files you want ignored 22 | # * Run: git config --global core.excludesfile ~/.gitignore 23 | # 24 | # After doing this, these files will be ignored in all your git projects, 25 | # saving you from having to 'pollute' every project you touch with them 26 | # 27 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 28 | # 29 | # For MacOS: 30 | # 31 | #.DS_Store 32 | # 33 | # For TextMate 34 | #*.tmproj 35 | #tmtags 36 | # 37 | # For emacs: 38 | #*~ 39 | #\#* 40 | #.\#* 41 | # 42 | # For vim: 43 | #*.swp 44 | .rvmrc 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Brandon Turner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/fields.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | # Mixed int both classes and instances to provide easy access to the field names 4 | module Fields 5 | 6 | def left_field_name 7 | acts_as_nested_set_options[:left_field] 8 | end 9 | 10 | 11 | def right_field_name 12 | acts_as_nested_set_options[:right_field] 13 | end 14 | 15 | 16 | def parent_field_name 17 | acts_as_nested_set_options[:parent_field] 18 | end 19 | 20 | 21 | def outline_number_field_name 22 | acts_as_nested_set_options[:outline_number_field] 23 | end 24 | 25 | 26 | def scope_field_names 27 | Array(acts_as_nested_set_options[:scope]) 28 | end 29 | 30 | 31 | def scope_class 32 | acts_as_nested_set_options[:klass] 33 | end 34 | 35 | 36 | def quoted_left_field_name 37 | # TODO 38 | left_field_name 39 | end 40 | 41 | 42 | def quoted_right_field_name 43 | # TODO 44 | right_field_name 45 | end 46 | 47 | 48 | def quoted_parent_field_name 49 | # TODO 50 | parent_field_name 51 | end 52 | 53 | 54 | def quoted_scope_field_names 55 | # TODO 56 | scope_field_names 57 | end 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | require 'rspec' 4 | require 'rr' 5 | require 'mongoid' 6 | require 'mongoid_nested_set' 7 | require 'remarkable/mongoid' 8 | 9 | if ENV['COVERAGE'] == 'yes' 10 | require 'simplecov' 11 | require 'simplecov-rcov' 12 | 13 | class SimpleCov::Formatter::MergedFormatter 14 | def format(result) 15 | SimpleCov::Formatter::HTMLFormatter.new.format(result) 16 | SimpleCov::Formatter::RcovFormatter.new.format(result) 17 | end 18 | end 19 | 20 | SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter 21 | SimpleCov.start 22 | end 23 | 24 | module Mongoid::Acts::NestedSet::Matchers 25 | end 26 | 27 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 28 | Dir["#{File.dirname(__FILE__)}/models/*.rb"].each {|file| require file } 29 | Dir["#{File.dirname(__FILE__)}/matchers/*.rb"].each {|file| require file } 30 | 31 | Mongoid.configure do |config| 32 | config.connect_to("mongoid_nested_set_test") 33 | config.allow_dynamic_fields = false 34 | end 35 | 36 | RSpec.configure do |config| 37 | config.mock_with :rr 38 | config.include(Mongoid::Acts::NestedSet::Matchers) 39 | 40 | config.after(:each) do 41 | Mongoid::Config.purge! 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/matchers/nestedset_pos.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | module Matchers 3 | 4 | def have_nestedset_pos(lft, rgt, options = {}) 5 | NestedSetPosition.new(lft, rgt, options) 6 | end 7 | 8 | class NestedSetPosition 9 | 10 | def initialize(lft, rgt, options) 11 | @lft = lft 12 | @rgt = rgt 13 | @options = options 14 | end 15 | 16 | def matches?(node) 17 | @node = node 18 | !!( 19 | node.respond_to?('left') && node.respond_to?('right') && 20 | node.left == @lft && 21 | node.right == @rgt 22 | ) 23 | end 24 | 25 | def description 26 | "have position {left: #{@lft}, right: #{@rgt}}" 27 | end 28 | 29 | def failure_message_for_should 30 | sprintf("expected nested set position: {left: %2s, right: %2s}\n" + 31 | " got: {left: %2s, right: %2s}", 32 | @lft, 33 | @rgt, 34 | @node.respond_to?('left') ? @node.left : '?', 35 | @node.respond_to?('right') ? @node.right : '?' 36 | ) 37 | end 38 | 39 | def failure_message_for_should_not 40 | sprintf("expected nested set to not have position: {left: %2s, right: %2s}", @lft, @rgt) 41 | end 42 | 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mongoid_nested_set.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "mongoid_nested_set/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "mongoid_nested_set" 7 | s.version = MongoidNestedSet::VERSION 8 | s.authors = ["Brandon Turner"] 9 | s.email = ["bt@brandonturner.net"] 10 | s.homepage = "http://github.com/thinkwell/mongoid_nested_set" 11 | s.summary = %q{Nested set based tree implementation for Mongoid} 12 | s.description = %q{Fully featured tree implementation for Mongoid using the nested set model} 13 | s.licenses = ["MIT"] 14 | 15 | s.rubyforge_project = "mongoid_nested_set" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | # specifiy any dependencies here; for example: 23 | # s.add_development_dependency "rspec" 24 | s.add_runtime_dependency(%q, [">= 3.0.0"]) 25 | 26 | s.add_development_dependency(%q, [">= 2.7.0"]) 27 | s.add_development_dependency(%q, [">= 1.0.21"]) 28 | s.add_development_dependency(%q) 29 | s.add_development_dependency(%q) 30 | end 31 | 32 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/rebuild.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module Rebuild 4 | 5 | # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree. 6 | # Warning: Very expensive! 7 | def rebuild!(options = {}) 8 | # Don't rebuild a valid tree. 9 | return true if valid? 10 | 11 | scope = lambda{ |node| {} } 12 | if acts_as_nested_set_options[:scope] 13 | scope = lambda { |node| node.nested_set_scope.options.merge(node.nested_set_scope.selector) } 14 | end 15 | indices = {} 16 | 17 | set_left_and_rights = lambda do |node| 18 | # set left 19 | left = (indices[scope.call(node)] += 1) 20 | # find 21 | node.nested_set_scope.where(parent_field_name => node.id).asc(left_field_name).asc(right_field_name).each { |n| set_left_and_rights.call(n) } 22 | # set right 23 | right = (indices[scope.call(node)] += 1) 24 | 25 | node.class.collection.find(:_id => node.id).update( 26 | {"$set" => {left_field_name => left, right_field_name => right}}, 27 | {:safe => true} 28 | ) 29 | end 30 | 31 | # Find root node(s) 32 | root_nodes = self.where(parent_field_name => nil).asc(left_field_name).asc(right_field_name).asc(:_id).each do |root_node| 33 | # setup index for this scope 34 | indices[scope.call(root_node)] ||= 0 35 | set_left_and_rights.call(root_node) 36 | end 37 | end 38 | 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'mongoid_nested_set/remove_order_by' 3 | 4 | # This acts provides Nested Set functionality. Nested Set is a smart way to implement 5 | # an _ordered_ tree, with the added feature that you can select the children and all of 6 | # their descendants with a single query. The drawback is that insertion or move need 7 | # multiple queries. But everything is done here by this module! 8 | # 9 | # Nested sets are appropriate each time you want either an ordered tree (menus, 10 | # commercial categories) or an efficient way of querying big trees (threaded posts). 11 | # 12 | # == API 13 | # 14 | # Method names are aligned with acts_as_tree as much as possible to make replacement 15 | # from one by another easier. 16 | # 17 | # item.children.create(:name => 'child1') 18 | # 19 | module Mongoid 20 | module Acts 21 | module NestedSet 22 | require 'mongoid_nested_set/base' 23 | autoload :Document, 'mongoid_nested_set/document' 24 | autoload :Fields, 'mongoid_nested_set/fields' 25 | autoload :Rebuild, 'mongoid_nested_set/rebuild' 26 | autoload :Relations, 'mongoid_nested_set/relations' 27 | autoload :Update, 'mongoid_nested_set/update' 28 | autoload :Validation, 'mongoid_nested_set/validation' 29 | autoload :OutlineNumber, 'mongoid_nested_set/outline_number' 30 | 31 | def self.included(base) 32 | base.extend(Base) 33 | end 34 | end 35 | end 36 | end 37 | 38 | 39 | # Enable the acts_as_nested_set method 40 | Mongoid::Document::ClassMethods.send(:include, Mongoid::Acts::NestedSet::Base) 41 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/validation.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module Validation 4 | 5 | # Warning: Very expensive! Do not use unless you know what you are doing. 6 | # This method is only useful for determining if the entire tree is valid 7 | def valid? 8 | left_and_rights_valid? && no_duplicates_for_fields? && all_roots_valid? 9 | end 10 | 11 | 12 | # Warning: Very expensive! Do not use unless you know what you are doing. 13 | def left_and_rights_valid? 14 | all.detect { |node| 15 | node.send(left_field_name).nil? || 16 | node.send(right_field_name).nil? || 17 | node.send(left_field_name) >= node.send(right_field_name) || 18 | !node.parent.nil? && ( 19 | node.send(left_field_name) <= node.parent.send(left_field_name) || 20 | node.send(right_field_name) >= node.parent.send(right_field_name) 21 | ) 22 | }.nil? 23 | end 24 | 25 | 26 | # Warning: Very expensive! Do not use unless you know what you are doing. 27 | def no_duplicates_for_fields? 28 | roots.group_by{|record| scope_field_names.collect{|field| record.send(field.to_sym)}}.all? do |scope, grouped_roots| 29 | [left_field_name, right_field_name].all? do |field| 30 | grouped_roots.first.nested_set_scope.only(field).group_by {|doc| doc.send(field)}.all? {|k, v| v.size == 1} 31 | end 32 | end 33 | end 34 | 35 | 36 | # Wrapper for each_root_valid? that can deal with scope 37 | # Warning: Very expensive! Do not use unless you know what you are doing. 38 | def all_roots_valid? 39 | if acts_as_nested_set_options[:scope] 40 | roots.group_by{|record| scope_field_names.collect{|field| record.send(field.to_sym)}}.all? do |scope, grouped_roots| 41 | each_root_valid?(grouped_roots) 42 | end 43 | else 44 | each_root_valid?(roots) 45 | end 46 | end 47 | 48 | 49 | def each_root_valid?(roots_to_validate) 50 | right = 0 51 | roots_to_validate.all? do |root| 52 | (root.left > right && root.right > right).tap do 53 | right = root.right 54 | end 55 | end 56 | end 57 | 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/relations.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module Relations 4 | 5 | # Returns root 6 | def root 7 | self_and_ancestors.first 8 | end 9 | 10 | 11 | # Returns the array of all parents and self 12 | def self_and_ancestors 13 | nested_set_scope.where( 14 | left_field_name => {"$lte" => left}, 15 | right_field_name => {"$gte" => right} 16 | ) 17 | end 18 | 19 | 20 | # Returns an array of all parents 21 | def ancestors 22 | without_self self_and_ancestors 23 | end 24 | 25 | 26 | # Returns the array of all children of the parent, including self 27 | def self_and_siblings 28 | nested_set_scope.where(parent_field_name => parent_id) 29 | end 30 | 31 | 32 | # Returns the array of all children of the parent, except self 33 | def siblings 34 | without_self self_and_siblings 35 | end 36 | 37 | 38 | # Returns a set of all of its nested children which do not have children 39 | def leaves 40 | descendants.where("this.#{right_field_name} - this.#{left_field_name} == 1") 41 | end 42 | 43 | 44 | # Returns the level of this object in the tree 45 | # root level is 0 46 | def level 47 | parent_id.nil? ? 0 : ancestors.count 48 | end 49 | 50 | 51 | # Returns a set of itself and all of its nested children 52 | def self_and_descendants 53 | nested_set_scope.where( 54 | left_field_name => {"$gte" => left}, 55 | right_field_name => {"$lte" => right} 56 | ) 57 | end 58 | 59 | 60 | # Returns a set of all of its children and nested children 61 | def descendants 62 | without_self self_and_descendants 63 | end 64 | 65 | 66 | def is_descendant_of?(other) 67 | other.left < self.left && self.left < other.right && same_scope?(other) 68 | end 69 | alias :descendant_of? is_descendant_of? 70 | 71 | 72 | def is_or_is_descendant_of?(other) 73 | other.left <= self.left && self.left < other.right && same_scope?(other) 74 | end 75 | 76 | 77 | def is_ancestor_of?(other) 78 | self.left < other.left && other.left < self.right && same_scope?(other) 79 | end 80 | alias :ancestor_of? is_ancestor_of? 81 | 82 | 83 | def is_or_is_ancestor_of?(other) 84 | self.left <= other.left && other.left < self.right && same_scope?(other) 85 | end 86 | 87 | 88 | # Find the first sibling to the left 89 | def left_sibling 90 | siblings.where(left_field_name => {"$lt" => left}).remove_order_by.desc(left_field_name).first 91 | end 92 | 93 | 94 | # Find the first sibling to the right 95 | def right_sibling 96 | siblings.where(left_field_name => {"$gt" => left}).asc(left_field_name).first 97 | end 98 | 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/outline_number.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module OutlineNumber 4 | 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | base.send(:include, InstanceMethods) 8 | end 9 | 10 | module ClassMethods 11 | 12 | # Iterates over tree elements and determines the current outline number in 13 | # the tree. 14 | # Only accepts default ordering, ordering by an other field than lft 15 | # does not work. 16 | # This method does not used the cached number field. 17 | # 18 | # Example: 19 | # Category.each_with_outline_number(Category.root.self_and_descendants) do |o, level| 20 | # 21 | def each_with_outline_number(objects, parent_number=nil) 22 | objects = Array(objects) unless objects.is_a? Array 23 | 24 | stack = [] 25 | last_num = parent_number 26 | objects.each_with_index do |o, i| 27 | if i == 0 && last_num == nil && !o.root? 28 | last_num = o.parent.outline_number 29 | end 30 | 31 | if stack.last.nil? || o.parent_id != stack.last[:parent_id] 32 | # we are on a new level, did we descend or ascend? 33 | if stack.any? { |h| h[:parent_id] == o.parent_id } 34 | # ascend 35 | stack.pop while stack.last[:parent_id] != o.parent_id 36 | else 37 | # descend 38 | stack << {:parent_id => o.parent_id, :parent_number => last_num, :siblings => []} 39 | end 40 | end 41 | 42 | if o.root? && !roots_have_outline_numbers? 43 | num = nil 44 | else 45 | num = o.send(:build_outline_number, 46 | o.root? ? '' : stack.last[:parent_number], 47 | o.send(:outline_number_sequence, stack.last[:siblings]) 48 | ) 49 | end 50 | yield(o, num) 51 | 52 | stack.last[:siblings] << o 53 | last_num = num 54 | end 55 | end 56 | 57 | 58 | def update_outline_numbers(objects, parent_number=nil) 59 | each_with_outline_number(objects, parent_number) do |o, num| 60 | o.update_attributes(outline_number_field_name => num) 61 | end 62 | end 63 | 64 | 65 | # Do root nodes have outline numbers 66 | def roots_have_outline_numbers? 67 | false 68 | end 69 | 70 | end 71 | 72 | module InstanceMethods 73 | 74 | def outline_number 75 | self[outline_number_field_name] 76 | end 77 | 78 | 79 | def update_outline_number 80 | self.class.update_outline_numbers(self) 81 | end 82 | 83 | 84 | def update_self_and_descendants_outline_number 85 | self.class.update_outline_numbers(self_and_descendants) 86 | end 87 | 88 | 89 | def update_descendants_outline_number 90 | self.class.update_outline_numbers(self.descendants, self.outline_number) 91 | end 92 | 93 | 94 | protected 95 | 96 | # Gets the outline sequence number for this node 97 | # 98 | # For example, if the parent's outline number is 1.2 and this is the 99 | # 3rd sibling this will return 3. 100 | # 101 | def outline_number_sequence(prev_siblings) 102 | prev_siblings.count + 1 103 | end 104 | 105 | 106 | # Constructs the full outline number 107 | # 108 | def build_outline_number(parent_number, sequence) 109 | if parent_number && parent_number != '' 110 | parent_number + outline_number_seperator + sequence.to_s 111 | else 112 | sequence.to_s 113 | end 114 | end 115 | 116 | def outline_number_seperator 117 | '.' 118 | end 119 | 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/base.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module Base 4 | 5 | # Configuration options are: 6 | # 7 | # * +:parent_field+ - field name to use for keeping the parent id (default: parent_id) 8 | # * +:left_field+ - field name for left boundary data, default 'lft' 9 | # * +:right_field+ - field name for right boundary data, default 'rgt' 10 | # * +:outline_number_field+ - field name for the number field, default nil. If set, 11 | # the value will be used as a field name to keep track of each node's 12 | # "outline number" (e.g. 1.2.5). 13 | # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach 14 | # "_id" (if it hasn't been already) and use that as the foreign key restriction. You 15 | # can also pass an array to scope by multiple attributes 16 | # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the child 17 | # objects are destroyed alongside this object by calling their destroy method. If set 18 | # to :delete_all (default), all the child objects are deleted without calling their 19 | # destroy method. 20 | # * +:klass+ - class to use for queries (defaults to self) 21 | # 22 | # See Mongoid::Acts::NestedSet::ClassMethods for a list of class methods and 23 | # Mongoid::Acts::NestedSet::InstanceMethods for a list of instance methods added to 24 | # acts_as_nested_set models 25 | def acts_as_nested_set(options = {}) 26 | options = { 27 | :parent_field => 'parent_id', 28 | :left_field => 'lft', 29 | :right_field => 'rgt', 30 | :outline_number_field => nil, 31 | :dependent => :delete_all, # or :destroy 32 | :klass => self, 33 | }.merge(options) 34 | 35 | if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/ 36 | options[:scope] = "#{options[:scope]}_id".intern 37 | end 38 | 39 | class_attribute :acts_as_nested_set_options, :instance_writer => false 40 | self.acts_as_nested_set_options = options 41 | 42 | unless self.is_a?(Document::ClassMethods) 43 | include Document 44 | include OutlineNumber if outline_number_field_name 45 | 46 | field left_field_name, :type => Integer 47 | field right_field_name, :type => Integer 48 | field outline_number_field_name, :type => String if outline_number_field_name 49 | field :depth, :type => Integer 50 | 51 | has_many :children, :class_name => self.name, :foreign_key => parent_field_name, :inverse_of => :parent, :order => left_field_name.to_sym.asc 52 | belongs_to :parent, :class_name => self.name, :foreign_key => parent_field_name 53 | 54 | attr_accessor :skip_before_destroy 55 | 56 | if accessible_attributes.blank? 57 | attr_protected left_field_name.intern, right_field_name.intern 58 | end 59 | 60 | define_callbacks :move, :terminator => "result == false" 61 | 62 | before_create :set_default_left_and_right 63 | before_save :store_new_parent 64 | after_save :move_to_new_parent 65 | before_destroy :destroy_descendants 66 | 67 | # no assignment to structure fields 68 | [left_field_name, right_field_name].each do |field| 69 | module_eval <<-"end_eval", __FILE__, __LINE__ 70 | def #{field}=(x) 71 | raise NameError, "Unauthorized assignment to #{field}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead.", "#{field}" 72 | end 73 | end_eval 74 | end 75 | 76 | scope :roots, lambda { 77 | where(parent_field_name => nil).asc(left_field_name) 78 | } 79 | scope :leaves, lambda { 80 | where("this.#{quoted_right_field_name} - this.#{quoted_left_field_name} == 1").asc(left_field_name) 81 | } 82 | scope :with_depth, lambda { |level| 83 | where(:depth => level).asc(left_field_name) 84 | } 85 | 86 | end 87 | end 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Mongoid Nested Set 2 | ================== 3 | 4 | Mongoid Nested Set is an implementation of the nested set pattern for Mongoid. 5 | It is a port of [AwesomeNestedSet for ActiveRecord](https://github.com/galetahub/awesome_nested_set). 6 | It supports Mongoid 2 and Rails 3. 7 | 8 | Nested Set represents hierarchies of trees in MongoDB using references rather 9 | than embedded documents. A tree is stored as a flat list of documents in a 10 | collection. Nested Set allows quick, ordered queries of nodes and their 11 | descendants. Tree modification is more costly. The nested set pattern is 12 | ideal for models that are read more frequently than modified. 13 | 14 | For more on the nested set pattern: 15 | 16 | 17 | ## Installation 18 | 19 | Install as Gem 20 | 21 | gem install mongoid_nested_set 22 | 23 | via Gemfile 24 | 25 | gem 'mongoid_nested_set', '0.1.3' 26 | 27 | 28 | ## Usage 29 | 30 | To start using Mongoid Nested Set, just declare `acts_as_nested_set` on your 31 | model: 32 | 33 | class Category 34 | include Mongoid::Document 35 | acts_as_nested_set 36 | end 37 | 38 | ### Creating a root node 39 | 40 | root = Category.create(:name => 'Root Category') 41 | 42 | ### Inserting a node 43 | 44 | child1 = root.children.create(:name => 'Child Category #1') 45 | 46 | child2 = Category.create(:name => 'Child Category #2') 47 | root.children << child2 48 | 49 | ### Deleting a node 50 | 51 | child1.destroy 52 | 53 | Descendants of a destroyed nodes will also be deleted. By default, descendant's 54 | `destroy` method will not be called. To enable calling descendant's `destroy` 55 | method: 56 | 57 | class Category 58 | include Mongoid::Document 59 | acts_as_nested_set :dependent => :destroy 60 | end 61 | 62 | ### Moving a node 63 | 64 | Several methods exist for moving nodes: 65 | 66 | * move\_left 67 | * move\_right 68 | * move\_to\_left\_of(other_node) 69 | * move\_to\_right\_of(other_node) 70 | * move\_to\_child\_of(other_node) 71 | * move\_to\_root 72 | 73 | 74 | ### Scopes 75 | 76 | Scopes restrict what is considered a list. This is commonly used to represent multiple trees 77 | (or multiple roots) in a single collection. 78 | 79 | class Category 80 | include Mongoid::Document 81 | acts_as_nested_set :scope => :root_id 82 | end 83 | 84 | ### Conversion from other trees 85 | 86 | Coming from acts_as_tree or adjacency list system where you only have parent_id? 87 | No problem. Simply add `acts_as_nested_set` and run: 88 | 89 | Category.rebuild! 90 | 91 | Your tree will be converted to a valid nested set. 92 | 93 | 94 | ### Outline Numbering 95 | 96 | Mongoid Nested Set can manage outline numbers (e.g. 1.3.2) for your documents if 97 | you wish. Simply add `:outline_number_field`: 98 | 99 | acts_as_nested_set, :outline_number_field => 'number' 100 | 101 | Your documents will now include a `number` field (you can call it anything you 102 | wish) that will contain outline numbers. 103 | 104 | Don't like the outline numbers format? Simply override `outline_number_seperator`, 105 | `build_outline_number`, or `outline_number_sequence` in your model classes. For 106 | example: 107 | 108 | class Category 109 | include Mongoid::Document 110 | acts_as_nested_set :scope => :root_id, :outline_number_field => 'number' 111 | 112 | # Use a dash instead of a dot for outline numbers 113 | # e.g. 1-3-2 114 | def outline_number_seperator 115 | '-' 116 | end 117 | 118 | # Use 0-based indexing instead of 1-based indexing 119 | # e.g. 1.0 120 | def outline_number_sequence(prev_siblings) 121 | prev_siblings.count 122 | end 123 | end 124 | 125 | 126 | ## References 127 | 128 | You can learn more about nested sets at: 129 | 130 | 131 | 132 | 133 | 134 | ## Contributing to mongoid\_nested\_set 135 | 136 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 137 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 138 | * Fork the project 139 | * Start a feature/bugfix branch 140 | * Commit and push until you are happy with your contribution 141 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 142 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 143 | 144 | ## Copyright 145 | 146 | Copyright (c) 2010 Brandon Turner. See LICENSE.txt for 147 | further details. 148 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/document.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module Document 4 | 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | base.send(:include, InstanceMethods) 8 | end 9 | 10 | 11 | module ClassMethods 12 | 13 | include Rebuild 14 | include Validation 15 | include Fields 16 | 17 | # Returns the first root 18 | def root 19 | roots.first 20 | end 21 | 22 | 23 | def scope_condition_by_options(options) 24 | h = {} 25 | scope_string = Array(acts_as_nested_set_options[:scope]).reject{|s| !options.has_key?(s) }.each do |c| 26 | h[c] = options[c] 27 | end 28 | h 29 | end 30 | 31 | 32 | # Iterates over tree elements and determines the current level in the tree. 33 | # Only accepts default ordering, ordering by an other field than lft 34 | # does not work. This method is much more efficient then calling level 35 | # because it doesn't require any additional database queries. 36 | # This method does not used the cached depth field. 37 | # 38 | # Example: 39 | # Category.each_with_level(Category.root.self_and_descendants) do |o, level| 40 | # 41 | def each_with_level(objects) 42 | offset = nil 43 | path = [nil] 44 | objects.each do |o| 45 | if offset == nil 46 | offset = o.parent_id.nil? ? 0 : o.parent.level 47 | end 48 | if o.parent_id != path.last 49 | # we are on a new level, did we descend or ascend? 50 | if path.include?(o.parent_id) 51 | # remove wrong tailing path elements 52 | path.pop while path.last != o.parent_id 53 | else 54 | path << o.parent_id 55 | end 56 | end 57 | yield(o, path.length - 1 + offset) 58 | end 59 | end 60 | 61 | 62 | # Iterates over tree elements with ancestors. 63 | # Only accepts default ordering, ordering by an other field than lft 64 | # does not work. This is much more efficient than calling ancestors for 65 | # each object because it doesn't require any additional database queries. 66 | # 67 | # Example: 68 | # Category.each_with_ancestors(Category.root.self_and_descendants) do |o, ancestors| 69 | # 70 | def each_with_ancestors(objects) 71 | ancestors = nil 72 | last_parent = nil 73 | objects.each do |o| 74 | if ancestors == nil 75 | ancestors = o.root? ? [] : o.ancestors.entries 76 | end 77 | if ancestors.empty? || o.parent_id != ancestors.last.id 78 | # we are on a new level, did we descend or ascend? 79 | if ancestors.any? {|a| a.id == o.parent_id} 80 | # ascend 81 | ancestors.pop while (!ancestors.empty? && ancestors.last.id != o.parent_id) 82 | elsif !o.root? 83 | # descend 84 | ancestors << last_parent 85 | end 86 | end 87 | yield(o, ancestors) 88 | last_parent = o 89 | end 90 | end 91 | 92 | 93 | # Provides a chainable relation to select all descendants of a set of records, 94 | # excluding the record set itself. 95 | # Similar to parent.descendants, except this allows you to find all descendants 96 | # of a set of nodes, rather than being restricted to find the descendants of only 97 | # a single node. 98 | # 99 | # Example: 100 | # parents = Category.roots.all 101 | # parents_descendants = Category.where(:deleted => false).descendants_of(parents) 102 | # 103 | def descendants_of(parents) 104 | # TODO: Add root or scope? 105 | conditions = parents.map do |parent| 106 | {left_field_name => {"$gt" => parent.left}, right_field_name => {"$lt" => parent.right}} 107 | end 108 | where("$or" => conditions) 109 | end 110 | 111 | 112 | def before_move(*args, &block) 113 | set_callback :move, :before, *args, &block 114 | end 115 | 116 | 117 | def after_move(*args, &block) 118 | set_callback :move, :after, *args, &block 119 | end 120 | 121 | end 122 | 123 | 124 | 125 | 126 | module InstanceMethods 127 | 128 | include Comparable 129 | include Relations 130 | include Update 131 | include Fields 132 | 133 | # Value fo the parent field 134 | def parent_id 135 | self[parent_field_name] 136 | end 137 | 138 | 139 | # Value of the left field 140 | def left 141 | self[left_field_name] 142 | end 143 | 144 | 145 | # Value of the right field 146 | def right 147 | self[right_field_name] 148 | end 149 | 150 | 151 | # Returns true if this is a root node 152 | def root? 153 | parent_id.nil? 154 | end 155 | 156 | 157 | # Returns true if this is a leaf node 158 | def leaf? 159 | #!new_record? && right - left == 1 160 | right - left == 1 161 | end 162 | 163 | 164 | # Returns true if this is a child node 165 | def child? 166 | !parent_id.nil? 167 | end 168 | 169 | 170 | # Returns true if depth is supported 171 | def depth? 172 | true 173 | end 174 | 175 | 176 | # Returns true if outline numbering is supported 177 | def outline_numbering? 178 | !!outline_number_field_name 179 | end 180 | 181 | 182 | # order by left field 183 | def <=>(x) 184 | left <=> x.left 185 | end 186 | 187 | 188 | # Redefine to act like active record 189 | def ==(comparison_object) 190 | comparison_object.equal?(self) || 191 | (comparison_object.instance_of?(scope_class) && 192 | comparison_object.id == id && 193 | !comparison_object.new_record?) 194 | end 195 | 196 | 197 | # Check if other model is in the same scope 198 | def same_scope?(other) 199 | Array(acts_as_nested_set_options[:scope]).all? do |attr| 200 | self.send(attr) == other.send(attr) 201 | end 202 | end 203 | 204 | 205 | def to_text 206 | self_and_descendants.map do |node| 207 | "#('*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})" 208 | end.join("\n") 209 | end 210 | 211 | 212 | # All nested set queries should use this nested_set_scope, which performs finds 213 | # using the :scope declared in the acts_as_nested_set declaration 214 | def nested_set_scope 215 | scopes = Array(acts_as_nested_set_options[:scope]) 216 | conditions = scopes.inject({}) do |conditions,attr| 217 | conditions.merge attr => self[attr] 218 | end unless scopes.empty? 219 | scope_class.criteria.where(conditions).asc(left_field_name) 220 | end 221 | 222 | 223 | 224 | protected 225 | 226 | def without_self(scope) 227 | scope.where(:_id.ne => self.id) 228 | end 229 | 230 | 231 | # reload left, right, and parent 232 | def reload_nested_set 233 | reload 234 | end 235 | 236 | end 237 | end # Document 238 | end # Mongoid::Acts::NestedSet 239 | -------------------------------------------------------------------------------- /lib/mongoid_nested_set/update.rb: -------------------------------------------------------------------------------- 1 | module Mongoid::Acts::NestedSet 2 | 3 | module Update 4 | 5 | # Shorthand method for finding the left sibling and moving to the left of it 6 | def move_left 7 | move_to_left_of left_sibling 8 | end 9 | 10 | 11 | # Shorthand method for finding the right sibling and moving to the right of it 12 | def move_right 13 | move_to_right_of right_sibling 14 | end 15 | 16 | 17 | # Move the node to the left of another node (you can pass id only) 18 | def move_to_left_of(node) 19 | move_to node, :left 20 | end 21 | 22 | 23 | # Move the node to the right of another node (you can pass id only) 24 | def move_to_right_of(node) 25 | move_to node, :right 26 | end 27 | 28 | 29 | # Move the node to the child of another node (you can pass id only) 30 | def move_to_child_of(node) 31 | move_to node, :child 32 | end 33 | 34 | 35 | # Move the node to root nodes 36 | def move_to_root 37 | move_to nil, :root 38 | end 39 | 40 | 41 | def move_possible?(target) 42 | self != target && # Can't target self 43 | same_scope?(target) && # can't be in different scopes 44 | !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right)) 45 | end 46 | 47 | 48 | 49 | protected 50 | 51 | def store_new_parent 52 | @move_to_new_parent_id = ((self.new_record? && parent_id) || send("#{parent_field_name}_changed?")) ? parent_id : false 53 | true # force callback to return true 54 | end 55 | 56 | 57 | def move_to_new_parent 58 | if @move_to_new_parent_id.nil? 59 | move_to_root 60 | elsif @move_to_new_parent_id 61 | move_to_child_of(@move_to_new_parent_id) 62 | end 63 | end 64 | 65 | 66 | # on creation, set automatically lft and rgt to the end of the tree 67 | def set_default_left_and_right 68 | maxright = nested_set_scope.remove_order_by.max(right_field_name) || 0 69 | self[left_field_name] = maxright + 1 70 | self[right_field_name] = maxright + 2 71 | self[:depth] = 0 72 | end 73 | 74 | 75 | def move_to(target, position) 76 | raise Mongoid::Errors::MongoidError, "You cannot move a new node" if self.new_record? 77 | 78 | res = run_callbacks :move do 79 | 80 | # No transaction support in MongoDB. 81 | # ACID is not guaranteed 82 | # TODO 83 | 84 | if target.is_a? scope_class 85 | target.reload_nested_set 86 | elsif position != :root 87 | # load object if node is not an object 88 | target = nested_set_scope.where(:_id => target).first 89 | end 90 | self.reload_nested_set 91 | 92 | unless position == :root || target 93 | raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be found." 94 | end 95 | 96 | unless position == :root || move_possible?(target) 97 | raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be inside moved tree." 98 | end 99 | 100 | bound = case position 101 | when :child; target[right_field_name] 102 | when :left; target[left_field_name] 103 | when :right; target[right_field_name] + 1 104 | when :root; 1 105 | else raise Mongoid::Errors::MongoidError, "Position should be :child, :left, :right or :root ('#{position}' received)." 106 | end 107 | 108 | old_parent = self[parent_field_name] 109 | new_parent = case position 110 | when :child; target.id 111 | when :root; nil 112 | else target[parent_field_name] 113 | end 114 | 115 | left, right = [self[left_field_name], self[right_field_name]] 116 | width, distance = [right - left + 1, bound - left] 117 | edge = bound > right ? bound - 1 : bound 118 | 119 | # there would be no change 120 | return self if left == edge || right == edge 121 | 122 | # moving backwards 123 | if distance < 0 124 | distance -= width 125 | left += width 126 | end 127 | 128 | scope_class.mongo_session.with(:safe => true) do |session| 129 | collection = session[scope_class.collection_name] 130 | scope = nested_set_scope.remove_order_by 131 | 132 | # allocate space for new move 133 | collection.find( 134 | scope.gte(left_field_name => bound).selector 135 | ).update_all("$inc" => { left_field_name => width }) 136 | 137 | collection.find( 138 | scope.gte(right_field_name => bound).selector 139 | ).update_all("$inc" => { right_field_name => width }) 140 | 141 | # move the nodes 142 | collection.find( 143 | scope.and(left_field_name => {"$gte" => left}, right_field_name => {"$lt" => left + width}).selector 144 | ).update_all("$inc" => { left_field_name => distance, right_field_name => distance }) 145 | 146 | # remove the hole 147 | collection.find( 148 | scope.gt(left_field_name => right).selector 149 | ).update_all("$inc" => { left_field_name => -width }) 150 | 151 | collection.find( 152 | scope.gt(right_field_name => right).selector 153 | ).update_all("$inc" => { right_field_name => -width }) 154 | end 155 | 156 | self.set(parent_field_name, new_parent) 157 | self.reload_nested_set 158 | self.update_self_and_descendants_depth 159 | 160 | if outline_numbering? 161 | if old_parent && old_parent != new_parent 162 | scope_class.where(:_id => old_parent).first.update_descendants_outline_number 163 | end 164 | if new_parent 165 | scope_class.where(:_id => new_parent).first.update_descendants_outline_number 166 | else 167 | update_self_and_descendants_outline_number 168 | end 169 | self.reload_nested_set 170 | end 171 | 172 | target.reload_nested_set if target 173 | end 174 | self 175 | end 176 | 177 | 178 | # Update cached level attribute 179 | def update_depth 180 | if depth? 181 | self.update_attribute(:depth, level) 182 | end 183 | self 184 | end 185 | 186 | 187 | # Update cached level attribute for self and descendants 188 | def update_self_and_descendants_depth 189 | if depth? 190 | scope_class.each_with_level(self_and_descendants) do |node, level| 191 | node.with(:safe => true).set(:depth, level) unless node.depth == level 192 | end 193 | self.reload 194 | end 195 | self 196 | end 197 | 198 | 199 | # Prunes a branch off of the tree, shifting all of the elements on the right 200 | # back to the left so the counts still work 201 | def destroy_descendants 202 | return if right.nil? || left.nil? || skip_before_destroy 203 | 204 | if acts_as_nested_set_options[:dependent] == :destroy 205 | descendants.each do |model| 206 | model.skip_before_destroy = true 207 | model.destroy 208 | end 209 | else 210 | c = nested_set_scope.where(left_field_name.to_sym.gt => left, right_field_name.to_sym.lt => right) 211 | scope_class.where(c.selector).delete_all 212 | end 213 | 214 | # update lefts and rights for remaining nodes 215 | diff = right - left + 1 216 | 217 | scope_class.with(:safe => true).where( 218 | nested_set_scope.where(left_field_name.to_sym.gt => right).selector 219 | ).inc(left_field_name, -diff) 220 | 221 | scope_class.with(:safe => true).where( 222 | nested_set_scope.where(right_field_name.to_sym.gt => right).selector 223 | ).inc(right_field_name, -diff) 224 | 225 | # Don't allow multiple calls to destroy to corrupt the set 226 | self.skip_before_destroy = true 227 | end 228 | 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /spec/mongoid_nested_set_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | 4 | describe Mongoid::Acts::NestedSet do 5 | 6 | it "provides the acts_as_nested_set method" do 7 | Node.should respond_to('acts_as_nested_set') 8 | NodeWithoutNestedSet.should respond_to('acts_as_nested_set') 9 | end 10 | 11 | end 12 | 13 | 14 | describe "A Mongoid::Document" do 15 | 16 | def create_clothing_nodes(klass=Node) 17 | nodes = {} 18 | # See Wikipedia for an illustration of the first tree 19 | # http://en.wikipedia.org/wiki/Nested_set_model#Example 20 | nodes[:clothing] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Clothing', 'lft' => 1, 'rgt' => 22, 'depth' => 0, 'number' => nil, 'parent_id' => nil) 21 | nodes[:mens] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Men\'s', 'lft' => 2, 'rgt' => 9, 'depth' => 1, 'number' => '1', 'parent_id' => nodes[:clothing].id) 22 | nodes[:suits] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Suits', 'lft' => 3, 'rgt' => 8, 'depth' => 2, 'number' => '1.1', 'parent_id' => nodes[:mens].id) 23 | nodes[:slacks] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Slacks', 'lft' => 4, 'rgt' => 5, 'depth' => 3, 'number' => '1.1.1', 'parent_id' => nodes[:suits].id) 24 | nodes[:jackets] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Jackets', 'lft' => 6, 'rgt' => 7, 'depth' => 3, 'number' => '1.1.2', 'parent_id' => nodes[:suits].id) 25 | nodes[:womens] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Women\'s', 'lft' => 10, 'rgt' => 21, 'depth' => 1, 'number' => '2', 'parent_id' => nodes[:clothing].id) 26 | nodes[:dresses] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Dresses', 'lft' => 11, 'rgt' => 16, 'depth' => 2, 'number' => '2.1', 'parent_id' => nodes[:womens].id) 27 | nodes[:skirts] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Skirts', 'lft' => 17, 'rgt' => 18, 'depth' => 2, 'number' => '2.2', 'parent_id' => nodes[:womens].id) 28 | nodes[:blouses] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Blouses', 'lft' => 19, 'rgt' => 20, 'depth' => 2, 'number' => '2.3', 'parent_id' => nodes[:womens].id) 29 | nodes[:gowns] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Gowns', 'lft' => 12, 'rgt' => 13, 'depth' => 3, 'number' => '2.1.1', 'parent_id' => nodes[:dresses].id) 30 | nodes[:sundress] = klass.new.test_set_attributes('root_id' => 1, 'name' => 'Sun Dresses', 'lft' => 14, 'rgt' => 15, 'depth' => 3, 'number' => '2.1.2', 'parent_id' => nodes[:dresses].id) 31 | nodes 32 | end 33 | 34 | def create_electronics_nodes(klass=Node) 35 | nodes = {} 36 | # See MySQL for an illustration of the second tree 37 | # http://dev.mysql.com/tech-resources/articles/hierarchical-data.html 38 | nodes[:electronics] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Electronics', 'lft' => 1, 'rgt' => 20, 'depth' => 0, 'number' => nil, 'parent_id' => nil) 39 | nodes[:televisions] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Televisions', 'lft' => 2, 'rgt' => 9, 'depth' => 1, 'number' => '1', 'parent_id' => nodes[:electronics].id) 40 | nodes[:tube] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Tube', 'lft' => 3, 'rgt' => 4, 'depth' => 2, 'number' => '1.1', 'parent_id' => nodes[:televisions].id) 41 | nodes[:lcd] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'LCD', 'lft' => 5, 'rgt' => 6, 'depth' => 2, 'number' => '1.2', 'parent_id' => nodes[:televisions].id) 42 | nodes[:plasma] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Plasma', 'lft' => 7, 'rgt' => 8, 'depth' => 2, 'number' => '1.3', 'parent_id' => nodes[:televisions].id) 43 | nodes[:portable] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Portable', 'lft' => 10, 'rgt' => 19, 'depth' => 1, 'number' => '2', 'parent_id' => nodes[:electronics].id) 44 | nodes[:mp3] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'MP3', 'lft' => 11, 'rgt' => 14, 'depth' => 2, 'number' => '2.1', 'parent_id' => nodes[:portable].id) 45 | nodes[:cd] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'CD', 'lft' => 15, 'rgt' => 16, 'depth' => 2, 'number' => '2.2', 'parent_id' => nodes[:portable].id) 46 | nodes[:radio] = klass.new.test_set_attributes('root_id' => 2, 'name' => '2 Way Radio', 'lft' => 17, 'rgt' => 18, 'depth' => 2, 'number' => '2.3', 'parent_id' => nodes[:portable].id) 47 | nodes[:flash] = klass.new.test_set_attributes('root_id' => 2, 'name' => 'Flash', 'lft' => 12, 'rgt' => 13, 'depth' => 3, 'number' => '2.1.1', 'parent_id' => nodes[:mp3].id) 48 | nodes 49 | end 50 | 51 | def persist_nodes(nodes, collection_name=nil) 52 | nodes = {:first => nodes} unless nodes.is_a? Hash 53 | collection_name = nodes.values.first.class.collection_name if collection_name.nil? 54 | 55 | nodes.each_value do |node| 56 | # As soon as there is no upsert callbacks set 57 | # this is effectively identical to the straight driver call 58 | node.with(:conllection => collection_name).upsert 59 | node.new_record = false 60 | end 61 | nodes 62 | end 63 | 64 | 65 | 66 | 67 | context "that does not act as a nested set" do 68 | it "does not have a left field" do 69 | NodeWithoutNestedSet.should_not have_field('lft', :type => Integer) 70 | end 71 | 72 | it "does not have a right field" do 73 | NodeWithoutNestedSet.should_not have_field('rgt', :type => Integer) 74 | end 75 | 76 | it "does not include NestedSet methods" do 77 | NodeWithoutNestedSet.should_not respond_to('descendant_of') 78 | NodeWithoutNestedSet.new.should_not respond_to('left') 79 | NodeWithoutNestedSet.should_not respond_to('each_with_outline_number') 80 | end 81 | end 82 | 83 | 84 | context "that acts as an un-scoped nested set" do 85 | 86 | context "in a tree" do 87 | before(:each) do 88 | @nodes = persist_nodes(create_clothing_nodes(UnscopedNode)) 89 | end 90 | 91 | it "can detect if roots are valid" do 92 | UnscopedNode.should be_all_roots_valid 93 | 94 | persist_nodes(UnscopedNode.new(:name => 'Test').test_set_attributes(:lft => 20, :rgt => 30, :parent_id=>nil)) 95 | UnscopedNode.should_not be_all_roots_valid 96 | end 97 | 98 | it "can detect if left and rights are valid" do 99 | UnscopedNode.should be_left_and_rights_valid 100 | 101 | # left > right 102 | n = UnscopedNode.new(:name => 'Test').test_set_attributes(:lft => 6, :rgt => 5, :parent_id=>@nodes[:suits].id) 103 | persist_nodes(n) 104 | UnscopedNode.should_not be_left_and_rights_valid 105 | 106 | # left == right 107 | persist_nodes(n.test_set_attributes(:rgt => 6)) 108 | UnscopedNode.should_not be_left_and_rights_valid 109 | 110 | # Overlaps parent 111 | persist_nodes(n.test_set_attributes(:rgt => 8)) 112 | UnscopedNode.should_not be_left_and_rights_valid 113 | end 114 | 115 | it "can detect duplicate left and right values" do 116 | UnscopedNode.should be_no_duplicates_for_fields 117 | 118 | n = UnscopedNode.new(:name => 'Test').test_set_attributes(:lft => 6, :rgt => 25, :parent_id=>@nodes[:suits].id) 119 | persist_nodes(n) 120 | UnscopedNode.should_not be_no_duplicates_for_fields 121 | 122 | persist_nodes(n.test_set_attributes(:lft => 5, :rgt => 7, :parent_id=>@nodes[:suits].id)) 123 | UnscopedNode.should_not be_no_duplicates_for_fields 124 | end 125 | end 126 | end 127 | 128 | 129 | context "that acts as a scoped nested set" do 130 | 131 | it "does not include outline number methods" do 132 | Node.should_not respond_to('each_with_outline_number') 133 | end 134 | 135 | # Adds fields 136 | 137 | it "has a left field" do 138 | Node.should have_field('lft', :type => Integer) 139 | RenamedFields.should have_field('red', :type => Integer) 140 | RenamedFields.should_not have_field('lft', :type => Integer) 141 | end 142 | 143 | it "has a right field" do 144 | Node.should have_field('rgt', :type => Integer) 145 | RenamedFields.should have_field('red', :type => Integer) 146 | RenamedFields.should_not have_field('rgt', :type => Integer) 147 | end 148 | 149 | it "has a parent field" do 150 | # Starting in Mongoid 2.0.rc1, all foreign keys are Objects 151 | Node.should have_field('parent_id', :type => Object) 152 | RenamedFields.should have_field('mother_id', :type => Object) 153 | RenamedFields.should_not have_field('parent_id', :type => Object) 154 | end 155 | 156 | it "does not have a number field" do 157 | Node.should_not have_field('number', :type => String) 158 | end 159 | 160 | it "has a default left field name" do 161 | Node.acts_as_nested_set_options[:left_field].should == 'lft' 162 | end 163 | 164 | it "has a default right field name" do 165 | Node.acts_as_nested_set_options[:right_field].should == 'rgt' 166 | end 167 | 168 | it "has a default parent field name" do 169 | Node.acts_as_nested_set_options[:parent_field].should == 'parent_id' 170 | end 171 | 172 | it "returns the left field name" do 173 | Node.left_field_name.should == 'lft' 174 | Node.new.left_field_name.should == 'lft' 175 | RenamedFields.left_field_name.should == 'red' 176 | RenamedFields.new.left_field_name.should == 'red' 177 | end 178 | 179 | it "returns the right field name" do 180 | Node.right_field_name.should == 'rgt' 181 | Node.new.right_field_name.should == 'rgt' 182 | RenamedFields.right_field_name.should == 'black' 183 | RenamedFields.new.right_field_name.should == 'black' 184 | end 185 | 186 | it "returns the parent field name" do 187 | Node.parent_field_name.should == 'parent_id' 188 | Node.new.parent_field_name.should == 'parent_id' 189 | RenamedFields.parent_field_name.should == 'mother_id' 190 | RenamedFields.new.parent_field_name.should == 'mother_id' 191 | end 192 | 193 | it "does not allow assigning the left field" do 194 | expect { Node.new.lft = 1 }.to raise_error(NameError) 195 | expect { RenamedFields.new.red = 1 }.to raise_error(NameError) 196 | end 197 | 198 | it "does not allow assigning the right field" do 199 | expect { Node.new.rgt = 1 }.to raise_error(NameError) 200 | expect { RenamedFields.new.black = 1 }.to raise_error(NameError) 201 | end 202 | 203 | 204 | 205 | 206 | # No-Database Calculations 207 | 208 | context "with other nodes" do 209 | before(:each) do 210 | @nodes = create_clothing_nodes.merge(create_electronics_nodes) 211 | end 212 | 213 | it "determines if it is a root node" do 214 | @nodes[:mens].should_not be_root 215 | @nodes[:clothing].should be_root 216 | end 217 | 218 | it "determines if it is a leaf node" do 219 | @nodes[:suits].should_not be_leaf 220 | @nodes[:jackets].should be_leaf 221 | end 222 | 223 | it "determines if it is a child node" do 224 | @nodes[:mens].should be_child 225 | @nodes[:clothing].should_not be_child 226 | end 227 | 228 | it "determines if it is a descendant of another node" do 229 | @nodes[:sundress].should be_descendant_of(@nodes[:dresses]) 230 | @nodes[:dresses].should_not be_descendant_of(@nodes[:sundress]) 231 | @nodes[:dresses].should_not be_descendant_of(@nodes[:dresses]) 232 | @nodes[:flash].should_not be_descendant_of(@nodes[:dresses]) 233 | end 234 | 235 | it "determines if it is a descendant of or equal to another node" do 236 | @nodes[:sundress].should be_is_or_is_descendant_of(@nodes[:dresses]) 237 | @nodes[:sundress].should be_is_or_is_descendant_of(@nodes[:sundress]) 238 | @nodes[:dresses].should_not be_is_or_is_descendant_of(@nodes[:sundress]) 239 | @nodes[:flash].should_not be_is_or_is_descendant_of(@nodes[:dresses]) 240 | @nodes[:skirts].should_not be_is_or_is_descendant_of(@nodes[:radio]) 241 | end 242 | 243 | it "determines if it is an ancestor of another node" do 244 | @nodes[:suits].should be_ancestor_of(@nodes[:jackets]) 245 | @nodes[:jackets].should_not be_ancestor_of(@nodes[:suits]) 246 | @nodes[:suits].should_not be_ancestor_of(@nodes[:suits]) 247 | @nodes[:dresses].should_not be_ancestor_of(@nodes[:flash]) 248 | end 249 | 250 | it "determines if it is an ancestor of or equal to another node" do 251 | @nodes[:suits].should be_is_or_is_ancestor_of(@nodes[:jackets]) 252 | @nodes[:suits].should be_is_or_is_ancestor_of(@nodes[:suits]) 253 | @nodes[:jackets].should_not be_is_or_is_ancestor_of(@nodes[:suits]) 254 | @nodes[:dresses].should_not be_is_or_is_ancestor_of(@nodes[:flash]) 255 | @nodes[:radio].should_not be_is_or_is_ancestor_of(@nodes[:skirts]) 256 | end 257 | 258 | end 259 | 260 | 261 | context "in an empty tree" do 262 | 263 | it "can create a root node" do 264 | root = Node.create(:name => 'Root Category') 265 | root.should have_nestedset_pos(1, 2) 266 | root.depth.should == 0 267 | end 268 | 269 | it "can create a child node via children.create" do 270 | root = Node.create(:name => 'Root Category') 271 | child = root.children.create(:name => 'Child Category') 272 | child.should have_nestedset_pos(2, 3) 273 | child.parent_id.should == root.id 274 | child.depth.should == 1 275 | root.reload.should have_nestedset_pos(1, 4) 276 | root.depth.should == 0 277 | end 278 | 279 | it "can create a child node via children<<" do 280 | root = Node.create(:name => 'Root Category') 281 | child = Node.create(:name => 'Child Category') 282 | root.children << child 283 | child.parent_id.should == root.id 284 | child.should have_nestedset_pos(2, 3) 285 | child.depth.should == 1 286 | root.reload.should have_nestedset_pos(1, 4) 287 | root.depth.should == 0 288 | end 289 | 290 | it "can create 2 level child nodes via children<<" do 291 | root = Node.create(:name => 'Root Category', :root_id => 10) 292 | child = Node.create(:name => 'Child Category', :root_id => 10) 293 | grandchild = Node.create(:name => 'Grandchild Category', :root_id => 10) 294 | root.children << child 295 | child.children << grandchild 296 | grandchild.parent_id.should == child.id 297 | grandchild.reload.should have_nestedset_pos(3, 4) 298 | grandchild.depth.should == 2 299 | child.parent_id.should == root.id 300 | child.reload.should have_nestedset_pos(2, 5) 301 | child.depth.should == 1 302 | root.reload.should have_nestedset_pos(1, 6) 303 | root.depth.should == 0 304 | end 305 | 306 | it "can create a child node with parent pre-assigned" do 307 | root = Node.create(:name => 'Root Category') 308 | child = Node.create(:name => 'Child Category', :parent => root) 309 | child.should have_nestedset_pos(2, 3) 310 | child.parent_id.should == root.id 311 | child.depth.should == 1 312 | root.reload.should have_nestedset_pos(1, 4) 313 | root.depth.should == 0 314 | end 315 | 316 | it "can create a child node with parent id pre-assigned" do 317 | root = Node.create(:name => 'Root Category') 318 | child = Node.create(:name => 'Child Category', :parent_id => root.id) 319 | child.should have_nestedset_pos(2, 3) 320 | child.parent_id.should == root.id 321 | child.depth.should == 1 322 | root.reload.should have_nestedset_pos(1, 4) 323 | root.depth.should == 0 324 | end 325 | 326 | it "can change a new node's parent before saving" do 327 | root = Node.create(:name => 'Root Category') 328 | child = Node.new(:name => 'Child Category') 329 | child.parent = root 330 | child.save 331 | child.should have_nestedset_pos(2, 3) 332 | child.parent_id.should == root.id 333 | child.depth.should == 1 334 | root.reload.should have_nestedset_pos(1, 4) 335 | root.depth.should == 0 336 | end 337 | 338 | it "can change a new node's parent id before saving" do 339 | root = Node.create(:name => 'Root Category') 340 | child = Node.new(:name => 'Child Category') 341 | child.parent_id = root.id 342 | child.save 343 | child.should have_nestedset_pos(2, 3) 344 | child.parent_id.should == root.id 345 | child.depth.should == 1 346 | root.reload.should have_nestedset_pos(1, 4) 347 | root.depth.should == 0 348 | end 349 | 350 | end 351 | 352 | 353 | context "in a tree" do 354 | 355 | before(:each) do 356 | @nodes = persist_nodes(create_clothing_nodes.merge(create_electronics_nodes)) 357 | end 358 | 359 | 360 | # Scopes 361 | 362 | it "fetches all root nodes" do 363 | Node.roots.should have(2).entries 364 | end 365 | 366 | it "fetches all leaf nodes in order" do 367 | Node.leaves.where(:root_id=>1).map {|e| e.name}.should == %w[Slacks Jackets Gowns Sun\ Dresses Skirts Blouses] 368 | end 369 | 370 | it "fetches all nodes with a given depth in order" do 371 | Node.with_depth(1).where(:root_id=>1).map {|e| e.name}.should == %w[Men's Women's] 372 | end 373 | 374 | 375 | # Queries 376 | 377 | it "fetches descendants of multiple parents" do 378 | parents = Node.any_in(:name => %w[Men's Dresses]) 379 | Node.where(:root_id=>1).descendants_of(parents).should have(5).entries 380 | end 381 | 382 | it "fetches self and ancestors in order" do 383 | @nodes[:dresses].self_and_ancestors.map {|e| e.name}.should == %w[Clothing Women's Dresses] 384 | end 385 | 386 | it "fetches ancestors in order" do 387 | @nodes[:dresses].ancestors.map {|e| e.name}.should == %w[Clothing Women's] 388 | end 389 | 390 | it "fetches its root" do 391 | @nodes[:dresses].root.name.should == 'Clothing' 392 | end 393 | 394 | it "fetches self and siblings in order" do 395 | @nodes[:skirts].self_and_siblings.map {|e| e.name}.should == %w[Dresses Skirts Blouses] 396 | end 397 | 398 | it "fetches siblings in order" do 399 | @nodes[:skirts].siblings.map {|e| e.name}.should == %w[Dresses Blouses] 400 | end 401 | 402 | it "fetches leaves in order" do 403 | @nodes[:womens].leaves.map {|e| e.name}.should == %w[Gowns Sun\ Dresses Skirts Blouses] 404 | end 405 | 406 | it "fetches its current level" do 407 | @nodes[:suits].level.should == 2 408 | end 409 | 410 | it "fetches self and descendants in order" do 411 | @nodes[:womens].self_and_descendants.map {|e| e.name}.should == %w[Women's Dresses Gowns Sun\ Dresses Skirts Blouses] 412 | end 413 | 414 | it "fetches descendants in order" do 415 | @nodes[:womens].descendants.map {|e| e.name}.should == %w[Dresses Gowns Sun\ Dresses Skirts Blouses] 416 | end 417 | 418 | it "fetches its first sibling to the left" do 419 | @nodes[:skirts].left_sibling.name.should == 'Dresses' 420 | @nodes[:slacks].left_sibling.should == nil 421 | end 422 | 423 | it "fetches its first sibling to the right" do 424 | @nodes[:skirts].right_sibling.name.should == 'Blouses' 425 | @nodes[:jackets].right_sibling.should == nil 426 | end 427 | 428 | it "can detect if roots are valid" do 429 | Node.should be_all_roots_valid 430 | 431 | persist_nodes(Node.new(:name => 'Test').test_set_attributes(:root_id => 1, :lft => 20, :rgt => 30, :parent_id=>nil)) 432 | Node.should_not be_all_roots_valid 433 | end 434 | 435 | it "can detect if left and rights are valid" do 436 | Node.should be_left_and_rights_valid 437 | 438 | # left > right 439 | n = Node.new(:name => 'Test').test_set_attributes(:root_id => 1, :lft => 6, :rgt => 5, :parent_id=>@nodes[:suits].id) 440 | persist_nodes(n) 441 | Node.should_not be_left_and_rights_valid 442 | 443 | # left == right 444 | persist_nodes(n.test_set_attributes(:rgt => 6)) 445 | Node.should_not be_left_and_rights_valid 446 | 447 | # Overlaps parent 448 | persist_nodes(n.test_set_attributes(:rgt => 8)) 449 | Node.should_not be_left_and_rights_valid 450 | end 451 | 452 | it "can detect duplicate left and right values" do 453 | Node.should be_no_duplicates_for_fields 454 | 455 | n = Node.new(:name => 'Test').test_set_attributes(:root_id => 1, :lft => 6, :rgt => 25, :parent_id=>@nodes[:suits].id) 456 | persist_nodes(n) 457 | Node.should_not be_no_duplicates_for_fields 458 | 459 | persist_nodes(n.test_set_attributes(:lft => 5, :rgt => 7, :parent_id=>@nodes[:suits].id)) 460 | Node.should_not be_no_duplicates_for_fields 461 | end 462 | 463 | 464 | # Moves 465 | 466 | it "cannot move a new node" do 467 | n = Node.new(:name => 'Test', :root_id => 1) 468 | expect { 469 | n.move_to_right_of(Node.where(:name => 'Jackets').first) 470 | }.to raise_error(Mongoid::Errors::MongoidError, /move.*new node/) 471 | end 472 | 473 | it "cannot move a node inside its tree" do 474 | n = Node.where(:name => 'Men\'s').first 475 | expect { 476 | n.move_to_right_of(Node.where(:name => 'Suits').first) 477 | }.to raise_error(Mongoid::Errors::MongoidError, /possible/) 478 | end 479 | 480 | it "cannot move a node to a non-existent target" do 481 | @nodes[:mens].parent_id = Moped::BSON::ObjectId.new 482 | expect { 483 | @nodes[:mens].save 484 | }.to raise_error(Mongoid::Errors::MongoidError, /possible.*(exist|found)/) 485 | end 486 | 487 | it "adds newly created nodes to the end of the tree" do 488 | Node.create(:name => 'Vests', :root_id => 1).should have_nestedset_pos(23, 24) 489 | 490 | n = Node.new(:name => 'Test', :root_id => 1) 491 | n.save 492 | n.should have_nestedset_pos(25, 26) 493 | end 494 | 495 | it "can move left" do 496 | @nodes[:jackets].move_left 497 | @nodes[:jackets] .should have_nestedset_pos( 4, 5) 498 | @nodes[:slacks].reload.should have_nestedset_pos( 6, 7) 499 | @nodes[:suits] .reload.should have_nestedset_pos( 3, 8) 500 | @nodes[:jackets].depth.should == 3 501 | @nodes[:slacks].depth.should == 3 502 | @nodes[:suits].depth.should == 2 503 | end 504 | 505 | it "can move right" do 506 | @nodes[:slacks].move_right 507 | @nodes[:slacks] .should have_nestedset_pos( 6, 7) 508 | @nodes[:jackets].reload.should have_nestedset_pos( 4, 5) 509 | @nodes[:suits] .reload.should have_nestedset_pos( 3, 8) 510 | @nodes[:slacks].depth.should == 3 511 | @nodes[:jackets].depth.should == 3 512 | @nodes[:suits].depth.should == 2 513 | end 514 | 515 | it "can move left of another node" do 516 | @nodes[:slacks].move_to_left_of(@nodes[:skirts]) 517 | @nodes[:slacks] .should have_nestedset_pos(15, 16) 518 | @nodes[:skirts] .should have_nestedset_pos(17, 18) 519 | @nodes[:skirts] .reload.should have_nestedset_pos(17, 18) 520 | @nodes[:dresses].reload.should have_nestedset_pos( 9, 14) 521 | @nodes[:womens] .reload.should have_nestedset_pos( 8, 21) 522 | @nodes[:slacks].depth.should == 2 523 | end 524 | 525 | it "can move right of another node" do 526 | @nodes[:slacks].move_to_right_of(@nodes[:skirts]) 527 | @nodes[:slacks] .should have_nestedset_pos(17, 18) 528 | @nodes[:skirts] .should have_nestedset_pos(15, 16) 529 | @nodes[:skirts] .reload.should have_nestedset_pos(15, 16) 530 | @nodes[:blouses].reload.should have_nestedset_pos(19, 20) 531 | @nodes[:womens] .reload.should have_nestedset_pos( 8, 21) 532 | @nodes[:slacks].depth.should == 2 533 | end 534 | 535 | it "can move as a child of another node" do 536 | @nodes[:slacks].move_to_child_of(@nodes[:dresses]) 537 | @nodes[:slacks] .should have_nestedset_pos(14, 15) 538 | @nodes[:dresses] .should have_nestedset_pos( 9, 16) 539 | @nodes[:dresses].reload.should have_nestedset_pos( 9, 16) 540 | @nodes[:gowns] .reload.should have_nestedset_pos(10, 11) 541 | @nodes[:mens] .reload.should have_nestedset_pos( 2, 7) 542 | @nodes[:slacks].depth.should == 3 543 | end 544 | 545 | it "can change it's parent id" do 546 | @nodes[:slacks].parent_id = @nodes[:dresses].id 547 | @nodes[:slacks].save 548 | @nodes[:slacks] .reload.should have_nestedset_pos(14, 15) 549 | @nodes[:dresses].reload.should have_nestedset_pos( 9, 16) 550 | @nodes[:gowns] .reload.should have_nestedset_pos(10, 11) 551 | @nodes[:mens] .reload.should have_nestedset_pos( 2, 7) 552 | @nodes[:slacks].depth.should == 3 553 | end 554 | 555 | it "can move to the root position" do 556 | @nodes[:suits].move_to_root 557 | @nodes[:suits] .should be_root 558 | @nodes[:suits] .should have_nestedset_pos( 1, 6) 559 | @nodes[:jackets] .reload.should have_nestedset_pos( 4, 5) 560 | @nodes[:clothing].reload.should have_nestedset_pos( 7, 22) 561 | @nodes[:mens] .reload.should have_nestedset_pos( 8, 9) 562 | @nodes[:womens] .reload.should have_nestedset_pos(10, 21) 563 | end 564 | 565 | it "can move to the left of root" do 566 | @nodes[:suits].move_to_left_of(@nodes[:clothing]) 567 | @nodes[:suits] .should be_root 568 | @nodes[:suits] .should have_nestedset_pos( 1, 6) 569 | @nodes[:jackets] .reload.should have_nestedset_pos( 4, 5) 570 | @nodes[:clothing].reload.should have_nestedset_pos( 7, 22) 571 | @nodes[:mens] .reload.should have_nestedset_pos( 8, 9) 572 | @nodes[:womens] .reload.should have_nestedset_pos(10, 21) 573 | end 574 | 575 | it "can move to the right of root" do 576 | @nodes[:suits].move_to_right_of(@nodes[:clothing]) 577 | @nodes[:suits] .should be_root 578 | @nodes[:suits] .should have_nestedset_pos(17, 22) 579 | @nodes[:jackets] .reload.should have_nestedset_pos(20, 21) 580 | @nodes[:clothing].reload.should have_nestedset_pos( 1, 16) 581 | @nodes[:mens] .reload.should have_nestedset_pos( 2, 3) 582 | @nodes[:womens] .reload.should have_nestedset_pos( 4, 15) 583 | end 584 | 585 | it "can move node with children" do 586 | @nodes[:suits].move_to_child_of(@nodes[:dresses]) 587 | @nodes[:suits] .should have_nestedset_pos(10, 15) 588 | @nodes[:dresses] .should have_nestedset_pos( 5, 16) 589 | @nodes[:mens] .reload.should have_nestedset_pos( 2, 3) 590 | @nodes[:womens] .reload.should have_nestedset_pos( 4, 21) 591 | @nodes[:sundress].reload.should have_nestedset_pos( 8, 9) 592 | @nodes[:jackets] .reload.should have_nestedset_pos(13, 14) 593 | @nodes[:suits].depth.should == 3 594 | @nodes[:jackets].depth.should == 4 595 | end 596 | 597 | it "can loop over elements starting at root with level" do 598 | i = 0 599 | Node.each_with_level(@nodes[:clothing].self_and_descendants) do |o, level| 600 | level.should == o.depth 601 | i += 1 602 | end 603 | i.should == 11 604 | end 605 | 606 | it "can loop over elements starting at non-root with level" do 607 | i = 0 608 | Node.each_with_level(@nodes[:mens].self_and_descendants) do |o, level| 609 | level.should == o.depth 610 | i += 1 611 | end 612 | i.should == 4 613 | end 614 | 615 | it "can loop over elements starting at root with ancestors" do 616 | i = 0 617 | Node.each_with_ancestors(@nodes[:clothing].self_and_descendants) do |o, ancestors| 618 | ancestors.should == o.ancestors.entries 619 | i += 1 620 | end 621 | i.should == 11 622 | end 623 | 624 | it "can loop over elements starting at non-root with ancestors" do 625 | i = 0 626 | Node.each_with_ancestors(@nodes[:mens].self_and_descendants) do |o, ancestors| 627 | ancestors.should == o.ancestors.entries 628 | i += 1 629 | end 630 | i.should == 4 631 | end 632 | 633 | context "with dependent=delete_all" do 634 | it "deletes descendants when destroyed" do 635 | @nodes[:mens].destroy 636 | @nodes[:clothing].reload.should have_nestedset_pos( 1, 14) 637 | @nodes[:womens] .reload.should have_nestedset_pos( 2, 13) 638 | Node.where(:name => 'Men\'s').count.should == 0 639 | Node.where(:name => 'Suits').count.should == 0 640 | Node.where(:name => 'Slacks').count.should == 0 641 | end 642 | end 643 | 644 | context "with dependent=destroy" do 645 | it "deletes descendants when destroyed" do 646 | Node.test_set_dependent_option :destroy 647 | @nodes[:mens].destroy 648 | @nodes[:clothing].reload.should have_nestedset_pos( 1, 14) 649 | @nodes[:womens] .reload.should have_nestedset_pos( 2, 13) 650 | Node.where(:name => 'Men\'s').count.should == 0 651 | Node.where(:name => 'Suits').count.should == 0 652 | Node.where(:name => 'Slacks').count.should == 0 653 | end 654 | end 655 | 656 | end 657 | 658 | 659 | context "in an adjaceny list tree" do 660 | before(:each) do 661 | @nodes = create_clothing_nodes(Node) 662 | @nodes.each_value { |node| node.test_set_attributes(:rgt => nil) } 663 | persist_nodes(@nodes) 664 | end 665 | 666 | it "can rebuild nested set properties" do 667 | Node.rebuild! 668 | root = Node.root 669 | root.should be_a(Node) 670 | root.name.should == 'Clothing' 671 | 672 | @nodes[:clothing].reload.should have_nestedset_pos( 1, 22) 673 | @nodes[:mens] .reload.should have_nestedset_pos( 2, 9) 674 | @nodes[:womens] .reload.should have_nestedset_pos(10, 21) 675 | @nodes[:suits] .reload.should have_nestedset_pos( 3, 8) 676 | @nodes[:skirts] .reload.should have_nestedset_pos(17, 18) 677 | end 678 | 679 | end 680 | end 681 | 682 | 683 | context "that acts as a nested set with inheritance" do 684 | def create_shape_nodes 685 | nodes = {} 686 | nodes[:root] = SquareNode.new.test_set_attributes('name' => 'Root', 'lft' => 1, 'rgt' => 12, 'depth' => 0, 'parent_id' => nil) 687 | nodes[:c1] = SquareNode.new.test_set_attributes('name' => '1', 'lft' => 2, 'rgt' => 7, 'depth' => 1, 'parent_id' => nodes[:root].id) 688 | nodes[:c2] = SquareNode.new.test_set_attributes('name' => '2', 'lft' => 8, 'rgt' => 9, 'depth' => 1, 'parent_id' => nodes[:root].id) 689 | nodes[:c3] = CircleNode.new.test_set_attributes('name' => '3', 'lft' => 10, 'rgt' => 11, 'depth' => 1, 'parent_id' => nodes[:root].id) 690 | nodes[:c11] = CircleNode.new.test_set_attributes('name' => '1.1', 'lft' => 3, 'rgt' => 4, 'depth' => 2, 'parent_id' => nodes[:c1].id) 691 | nodes[:c12] = SquareNode.new.test_set_attributes('name' => '1.2', 'lft' => 5, 'rgt' => 6, 'depth' => 2, 'parent_id' => nodes[:c1].id) 692 | nodes 693 | end 694 | 695 | context "in a tree" do 696 | before(:each) do 697 | @nodes = create_shape_nodes 698 | persist_nodes(@nodes) 699 | end 700 | 701 | it "fetches self and descendants in order" do 702 | @nodes[:root].self_and_descendants.map {|e| e.name}.should == %w[Root 1 1.1 1.2 2 3] 703 | end 704 | end 705 | end 706 | 707 | 708 | context "that acts as a nested set with outline numbering" do 709 | 710 | it "includes outline number methods" do 711 | NumberingNode.should respond_to('each_with_outline_number') 712 | end 713 | 714 | it "has a number field" do 715 | NumberingNode.should have_field('number', :type => String) 716 | end 717 | 718 | context "in a tree" do 719 | before(:each) do 720 | @nodes = persist_nodes(create_clothing_nodes(NumberingNode).merge(create_electronics_nodes(NumberingNode))) 721 | end 722 | 723 | it "sets the number for new child nodes" do 724 | n = NumberingNode.create(:name => 'Vests', :root_id => 1, :parent_id => @nodes[:suits].id) 725 | n.number.should == '1.1.3' 726 | end 727 | 728 | it "updates the number for nodes moved within the same parent" do 729 | @nodes[:slacks].move_right 730 | @nodes[:slacks] .number.should == '1.1.2' 731 | @nodes[:jackets].reload.number.should == '1.1.1' 732 | end 733 | 734 | it "updates the number for nodes moved to a new parent" do 735 | @nodes[:slacks].move_to_child_of(@nodes[:dresses]) 736 | @nodes[:slacks].number.should == '2.1.3' 737 | end 738 | 739 | it "updates the number for nodes moved to root" do 740 | @nodes[:suits].move_to_root 741 | @nodes[:suits] .number.should be_nil 742 | @nodes[:suits] .reload.number.should be_nil 743 | @nodes[:jackets].reload.number.should == '2' 744 | @nodes[:skirts] .reload.number.should == '2.2' 745 | end 746 | 747 | it "updates the number for old siblings of moved nodes" do 748 | @nodes[:slacks].move_to_child_of(@nodes[:dresses]) 749 | @nodes[:jackets].reload.number.should == '1.1.1' 750 | end 751 | 752 | it "updates the number for new siblings of moved nodes" do 753 | @nodes[:slacks].move_to_left_of(@nodes[:gowns]) 754 | @nodes[:gowns].reload.number.should == '2.1.2' 755 | end 756 | 757 | it "updates the number for descendants of moved nodes" do 758 | @nodes[:suits].move_to_child_of(@nodes[:dresses]) 759 | @nodes[:suits] .number.should == '2.1.3' 760 | @nodes[:jackets].reload.number.should == '2.1.3.2' 761 | end 762 | 763 | it "updates the number for descendants of old siblings of moved nodes" do 764 | @nodes[:mens].move_to_child_of(@nodes[:womens]) 765 | @nodes[:womens] .reload.number.should == '1' 766 | @nodes[:dresses].reload.number.should == '1.1' 767 | end 768 | 769 | it "updates the number for descendants of new siblings of moved nodes" do 770 | @nodes[:dresses].move_to_left_of(@nodes[:suits]) 771 | @nodes[:jackets].reload.number == '1.2.2' 772 | end 773 | 774 | it "updates the number for a single node" do 775 | @nodes[:suits].update_attributes(NumberingNode.outline_number_field_name => '3.1') 776 | @nodes[:suits].number.should == '3.1' 777 | @nodes[:suits].update_outline_number 778 | @nodes[:suits].number.should == '1.1' 779 | end 780 | 781 | 782 | end 783 | 784 | end 785 | 786 | end 787 | --------------------------------------------------------------------------------