awesome_nested_set-3.0.0/0000755000004100000410000000000012367660320015424 5ustar www-datawww-dataawesome_nested_set-3.0.0/CHANGELOG0000644000004100000410000000611612367660320016642 0ustar www-datawww-data3.0.0 * Support Rails 4.1 [Micah Geisel] * Support dependent: restrict_with_error [Tiago Moraes] * Added information to the README regarding indexes to be added for performance [bdarfler] * Modified associate_parents to add child objects to the parent#children collection [Tiago Moraes] 2.1.6 * Fixed rebuild! when there is a default_scope with order [Adrian Serafin] * Testing with stable bundler, ruby 2.0, MySQL and PostgreSQL [Philip Arndt] * Optimized move_to for large trees [ericsmith66] 2.1.5 * Worked around issues where AR#association wasn't present on Rails 3.0.x. [Philip Arndt] * Adds option 'order_column' which defaults to 'left_column_name'. [gudata] * Added moving with order functionality. [Sytse Sijbrandij] * Use tablename in all select queries. [Mikhail Dieterle] * Made sure all descendants' depths are updated when moving parent, not just immediate child. [Phil Thompson] * Add documentation of the callbacks. [Tobias Maier] 2.1.4 * nested_set_options accept both Class & AR Relation. [Semyon Perepelitsa] * Reduce the number of queries triggered by the canonical usage of `i.level` in the `nested_set` helpers. [thedarkone] * Specifically require active_record [Bogdan Gusiev] * compute_level now checks for a non nil association target. [Joel Nimety] 2.1.3 * Update child depth when parent node is moved. [Amanda Wagener] * Added move_to_child_with_index. [Ben Zhang] * Optimised self_and_descendants for when there's an index on lft. [Mark Torrance] * Added support for an unsaved record to return the right 'root'. [Philip Arndt] 2.1.2 * Fixed regressions introduced. [Philip Arndt] 2.1.1 * Added 'depth' which indicates how many levels deep the node is. This only works when you have a column called 'depth' in your table, otherwise it doesn't set itself. [Philip Arndt] * Rails 3.2 support added. [Gabriel Sobrinho] * Oracle compatibility added. [Pikender Sharma] * Adding row locking to deletion, locking source of pivot values, and adding retry on collisions. [Markus J. Q. Roberts] * Added method and helper for sorting children by column. [bluegod] * Fixed .all_roots_valid? to work with Postgres. [Joshua Clayton] * Made compatible with polymorphic belongs_to. [Graham Randall] * Added in the association callbacks to the children :has_many association. [Michael Deering] * Modified helper to allow using array of objects as argument. [Rahmat Budiharso] * Fixed cases where we were calling attr_protected. [Jacob Swanner] * Fixed nil cases involving lft and rgt. [Stuart Coyle] and [Patrick Morgan] 2.0.2 * Fixed deprecation warning under Rails 3.1 [Philip Arndt] * Converted Test::Unit matchers to RSpec. [Uģis Ozols] * Added inverse_of to associations to improve performance rendering trees. [Sergio Cambra] * Added row locking and fixed some race conditions. [Markus J. Q. Roberts] 2.0.1 * Fixed a bug with move_to not using nested_set_scope [Andreas Sekine] 2.0.0.pre * Expect Rails 3 * Changed how callbacks work. Returning false in a before_move action does not block save operations. Use a validation or exception in the callback if you need that. * Switched to RSpec * Remove use of Comparable awesome_nested_set-3.0.0/MIT-LICENSE0000644000004100000410000000205012367660320017055 0ustar www-datawww-dataCopyright (c) 2007-2011 Collective Idea Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. awesome_nested_set-3.0.0/lib/0000755000004100000410000000000012367660320016172 5ustar www-datawww-dataawesome_nested_set-3.0.0/lib/awesome_nested_set/0000755000004100000410000000000012367660320022047 5ustar www-datawww-dataawesome_nested_set-3.0.0/lib/awesome_nested_set/model.rb0000644000004100000410000001735312367660320023505 0ustar www-datawww-datarequire 'awesome_nested_set/model/prunable' require 'awesome_nested_set/model/movable' require 'awesome_nested_set/model/transactable' require 'awesome_nested_set/model/relatable' require 'awesome_nested_set/model/rebuildable' require 'awesome_nested_set/model/validatable' require 'awesome_nested_set/iterator' module CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: module Model extend ActiveSupport::Concern included do delegate :quoted_table_name, :arel_table, :to => self extend Validatable extend Rebuildable include Movable include Prunable include Relatable include Transactable end module ClassMethods def associate_parents(objects) return objects unless objects.all? {|o| o.respond_to?(:association)} id_indexed = objects.index_by(&primary_column_name.to_sym) objects.each do |object| association = object.association(:parent) parent = id_indexed[object.parent_id] if !association.loaded? && parent association.target = parent add_to_inverse_association(association, parent) end end end def add_to_inverse_association(association, record) inverse_reflection = association.send(:inverse_reflection_for, record) inverse = record.association(inverse_reflection.name) inverse.target << association.owner inverse.loaded! end def children_of(parent_id) where arel_table[parent_column_name].eq(parent_id) end # Iterates over tree elements and determines the current level in the tree. # Only accepts default ordering, odering by an other column than lft # does not work. This method is much more efficent than calling level # because it doesn't require any additional database queries. # # Example: # Category.each_with_level(Category.root.self_and_descendants) do |o, level| # def each_with_level(objects, &block) Iterator.new(objects).each_with_level(&block) end def leaves nested_set_scope.where "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1" end def left_of(node) where arel_table[left_column_name].lt(node) end def left_of_right_side(node) where arel_table[right_column_name].lteq(node) end def right_of(node) where arel_table[left_column_name].gteq(node) end def nested_set_scope(options = {}) options = {:order => quoted_order_column_full_name}.merge(options) where(options[:conditions]).order(options.delete(:order)) end def primary_key_scope(id) where arel_table[primary_column_name].eq(id) end def root roots.first end def roots nested_set_scope.children_of nil end end # end class methods # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder. # # category.self_and_descendants.count # category.ancestors.find(:all, :conditions => "name like '%foo%'") # Value of the parent column def parent_id(target = self) target[parent_column_name] end def primary_id(target = self) target[primary_column_name] end # Value of the left column def left(target = self) target[left_column_name] end # Value of the right column def right(target = self) target[right_column_name] end # Returns true if this is a root node. def root? parent_id.nil? end # Returns true is this is a child node def child? !root? end # Returns true if this is the end of a branch. def leaf? persisted? && right.to_i - left.to_i == 1 end # All nested set queries should use this nested_set_scope, which # performs finds on the base ActiveRecord class, using the :scope # declared in the acts_as_nested_set declaration. def nested_set_scope(options = {}) if (scopes = Array(acts_as_nested_set_options[:scope])).any? options[:conditions] = scopes.inject({}) do |conditions,attr| conditions.merge attr => self[attr] end end self.class.base_class.nested_set_scope options end def to_text self_and_descendants.map do |node| "#{'*'*(node.level+1)} #{node.primary_id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})" end.join("\n") end protected def without_self(scope) return scope if new_record? scope.where(["#{self.class.quoted_table_name}.#{self.class.quoted_primary_column_name} != ?", self.primary_id]) end def store_new_parent @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false true # force callback to return true end def has_depth_column? nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s) end def right_most_node @right_most_node ||= self.class.base_class.unscoped.nested_set_scope( :order => "#{quoted_right_column_full_name} desc" ).first end def right_most_bound @right_most_bound ||= begin return 0 if right_most_node.nil? right_most_node.lock! right_most_node[right_column_name] || 0 end end def set_depth! return unless has_depth_column? in_tenacious_transaction do reload update_depth(level) end end def set_depth_for_self_and_descendants! return unless has_depth_column? in_tenacious_transaction do reload self_and_descendants.select(primary_column_name).lock(true) old_depth = self[depth_column_name] || 0 new_depth = level update_depth(new_depth) change_descendants_depth!(new_depth - old_depth) new_depth end end def update_depth(depth) nested_set_scope.primary_key_scope(primary_id). update_all(["#{quoted_depth_column_name} = ?", depth]) self[depth_column_name] = depth end def change_descendants_depth!(diff) if !leaf? && diff != 0 sign = "++-"[diff <=> 0] descendants.update_all("#{quoted_depth_column_name} = #{quoted_depth_column_name} #{sign} #{diff.abs}") end end def set_default_left_and_right # adds the new node to the right of all existing nodes self[left_column_name] = right_most_bound + 1 self[right_column_name] = right_most_bound + 2 end # reload left, right, and parent def reload_nested_set reload( :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}", :lock => true ) end def reload_target(target, position) if target.is_a? self.class.base_class target.reload elsif position != :root nested_set_scope.where(primary_column_name => target).first! end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/helper.rb0000644000004100000410000000310712367660320023654 0ustar www-datawww-data# -*- coding: utf-8 -*- module CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: # This module provides some helpers for the model classes using acts_as_nested_set. # It is included by default in all views. # module Helper # Returns options for select. # You can exclude some items from the tree. # You can pass a block receiving an item and returning the string displayed in the select. # # == Params # * +class_or_item+ - Class name or top level times # * +mover+ - The item that is being move, used to exlude impossible moves # * +&block+ - a block that will be used to display: { |item| ... item.name } # # == Usage # # <%= f.select :parent_id, nested_set_options(Category, @category) {|i| # "#{'–' * i.level} #{i.name}" # }) %> # def nested_set_options(class_or_item, mover = nil) if class_or_item.is_a? Array items = class_or_item.reject { |e| !e.root? } else class_or_item = class_or_item.roots if class_or_item.respond_to?(:scope) items = Array(class_or_item) end result = [] items.each do |root| result += root.class.associate_parents(root.self_and_descendants).map do |i| if mover.nil? || mover.new_record? || mover.move_possible?(i) [yield(i), i.primary_id] end end.compact end result end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/model/0000755000004100000410000000000012367660320023147 5ustar www-datawww-dataawesome_nested_set-3.0.0/lib/awesome_nested_set/model/prunable.rb0000644000004100000410000000471012367660320025306 0ustar www-datawww-datamodule CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: module Model module Prunable # Prunes a branch off of the tree, shifting all of the elements on the right # back to the left so the counts still work. def destroy_descendants return if right.nil? || left.nil? || skip_before_destroy in_tenacious_transaction do reload_nested_set # select the rows in the model that extend past the deletion point and apply a lock nested_set_scope.right_of(left).select(primary_id).lock(true) return false unless destroy_or_delete_descendants # update lefts and rights for remaining nodes update_siblings_for_remaining_nodes # Reload is needed because children may have updated their parent (self) during deletion. reload # Don't allow multiple calls to destroy to corrupt the set self.skip_before_destroy = true end end def destroy_or_delete_descendants if acts_as_nested_set_options[:dependent] == :destroy descendants.each do |model| model.skip_before_destroy = true model.destroy end elsif acts_as_nested_set_options[:dependent] == :restrict_with_exception raise ActiveRecord::DeleteRestrictionError.new(:children) unless leaf? elsif acts_as_nested_set_options[:dependent] == :restrict_with_error unless leaf? record = self.class.human_attribute_name(:children).downcase errors.add(:base, :"restrict_dependent_destroy.many", record: record) return false end return true else descendants.delete_all end end def update_siblings_for_remaining_nodes update_siblings(:left) update_siblings(:right) end def update_siblings(direction) full_column_name = send("quoted_#{direction}_column_full_name") column_name = send("quoted_#{direction}_column_name") nested_set_scope.where(["#{full_column_name} > ?", right]). update_all(["#{column_name} = (#{column_name} - ?)", diff]) end def diff right - left + 1 end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/model/relatable.rb0000644000004100000410000000711312367660320025431 0ustar www-datawww-datamodule CollectiveIdea module Acts module NestedSet module Model module Relatable # Returns an collection of all parents def ancestors without_self self_and_ancestors end # Returns the collection of all parents and self def self_and_ancestors nested_set_scope. where(arel_table[left_column_name].lteq(left)). where(arel_table[right_column_name].gteq(right)) end # Returns the collection of all children of the parent, except self def siblings without_self self_and_siblings end # Returns the collection of all children of the parent, including self def self_and_siblings nested_set_scope.children_of parent_id end # Returns a set of all of its nested children which do not have children def leaves descendants.where( "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1" ) end # Returns the level of this object in the tree # root level is 0 def level parent_id.nil? ? 0 : compute_level end # Returns a collection including all of its children and nested children def descendants without_self self_and_descendants end # Returns a collection including itself and all of its nested children def self_and_descendants # using _left_ for both sides here lets us benefit from an index on that column if one exists nested_set_scope.right_of(left).left_of(right) end def is_descendant_of?(other) within_node?(other, self) && same_scope?(other) end def is_or_is_descendant_of?(other) (other == self || within_node?(other, self)) && same_scope?(other) end def is_ancestor_of?(other) within_node?(self, other) && same_scope?(other) end def is_or_is_ancestor_of?(other) (self == other || within_node?(self, other)) && same_scope?(other) end # Check if other model is in the same scope def same_scope?(other) Array(acts_as_nested_set_options[:scope]).all? do |attr| self.send(attr) == other.send(attr) end end # Find the first sibling to the left def left_sibling siblings.left_of(left).last end # Find the first sibling to the right def right_sibling siblings.right_of(left).first end def root return self_and_ancestors.children_of(nil).first if persisted? if parent_id && current_parent = nested_set_scope.where(primary_column_name => parent_id).first! current_parent.root else self end end protected def compute_level node, nesting = determine_depth node == self ? ancestors.count : node.level + nesting end def determine_depth(node = self, nesting = 0) while (association = node.association(:parent)).loaded? && association.target nesting += 1 node = node.parent end if node.respond_to?(:association) [node, nesting] end def within_node?(node, within) node.left < within.left && within.left < node.right end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/model/rebuildable.rb0000644000004100000410000000232312367660320025746 0ustar www-datawww-datarequire 'awesome_nested_set/tree' module CollectiveIdea module Acts module NestedSet module Model module Rebuildable # Rebuilds the left & rights if unset or invalid. # Also very useful for converting from acts_as_tree. def rebuild!(validate_nodes = true) # default_scope with order may break database queries so we do all operation without scope unscoped do Tree.new(self, validate_nodes).rebuild! end end def scope_for_rebuild scope = proc {} if acts_as_nested_set_options[:scope] scope = proc {|node| scope_column_names.inject("") {|str, column_name| column_value = node.send(column_name) cond = column_value.nil? ? "IS NULL" : "= #{connection.quote(column_value)}" str << "AND #{connection.quote_column_name(column_name)} #{cond} " } } end scope end def order_for_rebuild "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{primary_key}" end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/model/movable.rb0000644000004100000410000001152612367660320025126 0ustar www-datawww-datarequire '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) move_to node, :child end # Move the node to the child of another node with specify index def move_to_child_with_index(node, index) if node.children.empty? move_to_child_of(node) elsif node.children.count == index move_to_right_of(node.children.last) else my_position = node.children.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(node.children[index]) elsif my_position && my_position == index # do nothing. already there. else move_to_left_of(node.children[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 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 awesome_nested_set-3.0.0/lib/awesome_nested_set/model/validatable.rb0000644000004100000410000000407112367660320025746 0ustar www-datawww-datarequire 'awesome_nested_set/set_validator' module CollectiveIdea module Acts module NestedSet module Model module Validatable def valid? left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid? end def left_and_rights_valid? SetValidator.new(self).valid? end def no_duplicates_for_columns? [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column| # No duplicates select("#{scope_string}#{column}, COUNT(#{column}) as _count"). group("#{scope_string}#{column}", quoted_primary_key_column_full_name). having("COUNT(#{column}) > 1"). order(quoted_primary_key_column_full_name). first.nil? end end # Wrapper for each_root_valid? that can deal with scope. def all_roots_valid? if acts_as_nested_set_options[:scope] all_roots_valid_by_scope?(roots) else each_root_valid?(roots) end end def all_roots_valid_by_scope?(roots_to_validate) roots_grouped_by_scope(roots_to_validate).all? do |scope, grouped_roots| each_root_valid?(grouped_roots) end end def each_root_valid?(roots_to_validate) left = right = 0 roots_to_validate.all? do |root| (root.left > left && root.right > right).tap do left = root.left right = root.right end end end private def roots_grouped_by_scope(roots_to_group) roots_to_group.group_by {|record| scope_column_names.collect {|col| record.send(col) } } end def scope_string Array(acts_as_nested_set_options[:scope]).map do |c| connection.quote_column_name(c) end.push(nil).join(", ") end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/model/transactable.rb0000644000004100000410000000146012367660320026140 0ustar www-datawww-datamodule CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: module Model module Transactable protected def in_tenacious_transaction(&block) retry_count = 0 begin transaction(&block) rescue ActiveRecord::StatementInvalid => error raise unless self.class.connection.open_transactions.zero? raise unless error.message =~ /[Dd]eadlock|Lock wait timeout exceeded/ raise unless retry_count < 10 retry_count += 1 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction" sleep(rand(retry_count)*0.1) # Aloha protocol retry end end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/columns.rb0000644000004100000410000000453412367660320024062 0ustar www-datawww-data# Mixed into both classes and instances to provide easy access to the column names module CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: module Columns def left_column_name acts_as_nested_set_options[:left_column] end def right_column_name acts_as_nested_set_options[:right_column] end def depth_column_name acts_as_nested_set_options[:depth_column] end def parent_column_name acts_as_nested_set_options[:parent_column] end def primary_column_name acts_as_nested_set_options[:primary_column] end def order_column acts_as_nested_set_options[:order_column] || left_column_name end def scope_column_names Array(acts_as_nested_set_options[:scope]) end def quoted_left_column_name ActiveRecord::Base.connection.quote_column_name(left_column_name) end def quoted_right_column_name ActiveRecord::Base.connection.quote_column_name(right_column_name) end def quoted_depth_column_name ActiveRecord::Base.connection.quote_column_name(depth_column_name) end def quoted_primary_column_name ActiveRecord::Base.connection.quote_column_name(primary_column_name) end def quoted_parent_column_name ActiveRecord::Base.connection.quote_column_name(parent_column_name) end def quoted_scope_column_names scope_column_names.collect {|column_name| connection.quote_column_name(column_name) } end def quoted_order_column_name ActiveRecord::Base.connection.quote_column_name(order_column) end def quoted_primary_key_column_full_name "#{quoted_table_name}.#{quoted_primary_column_name}" end def quoted_order_column_full_name "#{quoted_table_name}.#{quoted_order_column_name}" end def quoted_left_column_full_name "#{quoted_table_name}.#{quoted_left_column_name}" end def quoted_right_column_full_name "#{quoted_table_name}.#{quoted_right_column_name}" end def quoted_parent_column_full_name "#{quoted_table_name}.#{quoted_parent_column_name}" end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/move.rb0000644000004100000410000001042112367660320023340 0ustar www-datawww-datamodule CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: class Move attr_reader :target, :position, :instance def initialize(target, position, instance) @target = target @position = position @instance = instance end def move prevent_impossible_move bound, other_bound = get_boundaries # there would be no change return if bound == right || bound == left # we have defined the boundaries of two non-overlapping intervals, # so sorting puts both the intervals and their boundaries in order a, b, c, d = [left, right, bound, other_bound].sort lock_nodes_between! a, d nested_set_scope.where(where_statement(a, d)).update_all( conditions(a, b, c, d) ) end private delegate :left, :right, :left_column_name, :right_column_name, :quoted_left_column_name, :quoted_right_column_name, :quoted_parent_column_name, :parent_column_name, :nested_set_scope, :primary_column_name, :quoted_primary_column_name, :primary_id, :to => :instance delegate :arel_table, :class, :to => :instance, :prefix => true delegate :base_class, :to => :instance_class, :prefix => :instance def where_statement(left_bound, right_bound) instance_arel_table[left_column_name].in(left_bound..right_bound). or(instance_arel_table[right_column_name].in(left_bound..right_bound)) end def conditions(a, b, c, d) _conditions = case_condition_for_direction(:quoted_left_column_name) + case_condition_for_direction(:quoted_right_column_name) + case_condition_for_parent # We want the record to be 'touched' if it timestamps. if @instance.respond_to?(:updated_at) _conditions << ", updated_at = :timestamp" end [ _conditions, { :a => a, :b => b, :c => c, :d => d, :primary_id => instance.primary_id, :new_parent_id => new_parent_id, :timestamp => Time.now.utc } ] end def case_condition_for_direction(column_name) column = send(column_name) "#{column} = CASE " + "WHEN #{column} BETWEEN :a AND :b " + "THEN #{column} + :d - :b " + "WHEN #{column} BETWEEN :c AND :d " + "THEN #{column} + :a - :c " + "ELSE #{column} END, " end def case_condition_for_parent "#{quoted_parent_column_name} = CASE " + "WHEN #{quoted_primary_column_name} = :primary_id THEN :new_parent_id " + "ELSE #{quoted_parent_column_name} END" end def lock_nodes_between!(left_bound, right_bound) # select the rows in the model between a and d, and apply a lock instance_base_class.right_of(left_bound).left_of_right_side(right_bound). select(primary_column_name).lock(true) end def root position == :root end def new_parent_id case position when :child then target.primary_id when :root then nil else target[parent_column_name] end end def get_boundaries if (bound = target_bound) > right bound -= 1 other_bound = right + 1 else other_bound = left - 1 end [bound, other_bound] end def prevent_impossible_move if !root && !instance.move_possible?(target) raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree." end end def target_bound case position when :child then right(target) when :left then left(target) when :right then right(target) + 1 when :root then nested_set_scope.pluck(right_column_name).max + 1 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)." end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/tree.rb0000644000004100000410000000330612367660320023335 0ustar www-datawww-datamodule CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: class Tree attr_reader :model, :validate_nodes attr_accessor :indices delegate :left_column_name, :right_column_name, :quoted_parent_column_full_name, :order_for_rebuild, :scope_for_rebuild, :to => :model def initialize(model, validate_nodes) @model = model @validate_nodes = validate_nodes @indices = {} end def rebuild! # Don't rebuild a valid tree. return true if model.valid? root_nodes.each do |root_node| # setup index for this scope indices[scope_for_rebuild.call(root_node)] ||= 0 set_left_and_rights(root_node) end end private def increment_indice!(node) indices[scope_for_rebuild.call(node)] += 1 end def set_left_and_rights(node) set_left!(node) # find node_children(node).each { |n| set_left_and_rights(n) } set_right!(node) node.save!(:validate => validate_nodes) end def node_children(node) model.where(["#{quoted_parent_column_full_name} = ? #{scope_for_rebuild.call(node)}", node.primary_id]). order(order_for_rebuild) end def root_nodes model.where("#{quoted_parent_column_full_name} IS NULL").order(order_for_rebuild) end def set_left!(node) node[left_column_name] = increment_indice!(node) end def set_right!(node) node[right_column_name] = increment_indice!(node) end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/version.rb0000644000004100000410000000013512367660320024060 0ustar www-datawww-datamodule AwesomeNestedSet VERSION = '3.0.0' unless defined?(::AwesomeNestedSet::VERSION) end awesome_nested_set-3.0.0/lib/awesome_nested_set/iterator.rb0000644000004100000410000000133212367660320024224 0ustar www-datawww-datamodule CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: class Iterator attr_reader :objects def initialize(objects) @objects = objects end def each_with_level path = [nil] objects.each do |o| if o.parent_id != path.last # we are on a new level, did we descend or ascend? if path.include?(o.parent_id) # remove wrong tailing paths elements path.pop while path.last != o.parent_id else path << o.parent_id end end yield(o, path.length - 1) end end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/awesome_nested_set.rb0000644000004100000410000001401712367660320026254 0ustar www-datawww-datarequire 'awesome_nested_set/columns' require 'awesome_nested_set/model' module CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: # This acts provides Nested Set functionality. Nested Set is a smart way to implement # an _ordered_ tree, with the added feature that you can select the children and all of their # descendants with a single query. The drawback is that insertion or move need some complex # sql queries. But everything is done here by this module! # # Nested sets are appropriate each time you want either an orderd tree (menus, # commercial categories) or an efficient way of querying big trees (threaded posts). # # == API # # Methods names are aligned with acts_as_tree as much as possible to make replacment from one # by another easier. # # item.children.create(:name => "child1") # # Configuration options are: # # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) # * +:primary_column+ - specifies the column name to use as the inverse of the parent column (default: id) # * +:left_column+ - column name for left boundry data, default "lft" # * +:right_column+ - column name for right boundry data, default "rgt" # * +:depth_column+ - column name for the depth data, default "depth" # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" # (if it hasn't been already) and use that as the foreign key restriction. You # can also pass an array to scope by multiple attributes. # Example: acts_as_nested_set :scope => [:notable_id, :notable_type] # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the # child objects are destroyed alongside this object by calling their destroy # method. If set to :delete_all (default), all the child objects are deleted # without calling their destroy method. # * +:counter_cache+ adds a counter cache for the number of children. # defaults to false. # Example: acts_as_nested_set :counter_cache => :children_count # * +:order_column+ on which column to do sorting, by default it is the left_column_name # Example: acts_as_nested_set :order_column => :position # # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added # to acts_as_nested_set models def acts_as_nested_set(options = {}) acts_as_nested_set_parse_options! options include Model include Columns extend Columns acts_as_nested_set_relate_parent! acts_as_nested_set_relate_children! attr_accessor :skip_before_destroy acts_as_nested_set_prevent_assignment_to_reserved_columns! acts_as_nested_set_define_callbacks! end private def acts_as_nested_set_define_callbacks! # on creation, set automatically lft and rgt to the end of the tree before_create :set_default_left_and_right before_save :store_new_parent after_save :move_to_new_parent, :set_depth! before_destroy :destroy_descendants define_model_callbacks :move end def acts_as_nested_set_relate_children! has_many_children_options = { :class_name => self.base_class.to_s, :foreign_key => parent_column_name, :primary_key => primary_column_name, :inverse_of => (:parent unless acts_as_nested_set_options[:polymorphic]), } # Add callbacks, if they were supplied.. otherwise, we don't want them. [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback| has_many_children_options.update( ar_callback => acts_as_nested_set_options[ar_callback] ) if acts_as_nested_set_options[ar_callback] end has_many :children, -> { order(quoted_order_column_name) }, has_many_children_options end def acts_as_nested_set_relate_parent! belongs_to :parent, :class_name => self.base_class.to_s, :foreign_key => parent_column_name, :primary_key => primary_column_name, :counter_cache => acts_as_nested_set_options[:counter_cache], :inverse_of => (:children unless acts_as_nested_set_options[:polymorphic]), :polymorphic => acts_as_nested_set_options[:polymorphic], :touch => acts_as_nested_set_options[:touch] end def acts_as_nested_set_default_options { :parent_column => 'parent_id', :primary_column => 'id', :left_column => 'lft', :right_column => 'rgt', :depth_column => 'depth', :dependent => :delete_all, # or :destroy :polymorphic => false, :counter_cache => false, :touch => false }.freeze end def acts_as_nested_set_parse_options!(options) options = acts_as_nested_set_default_options.merge(options) if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/ options[:scope] = "#{options[:scope]}_id".intern end class_attribute :acts_as_nested_set_options self.acts_as_nested_set_options = options end def acts_as_nested_set_prevent_assignment_to_reserved_columns! # no assignment to structure fields [left_column_name, right_column_name, depth_column_name].each do |column| module_eval <<-"end_eval", __FILE__, __LINE__ def #{column}=(x) raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead." end end_eval end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set/set_validator.rb0000644000004100000410000000370512367660320025241 0ustar www-datawww-datamodule CollectiveIdea #:nodoc: module Acts #:nodoc: module NestedSet #:nodoc: class SetValidator def initialize(model) @model = model @scope = model.all @parent = arel_table.alias('parent') end def valid? query.count == 0 end private attr_reader :model, :parent attr_accessor :scope delegate :parent_column_name, :primary_column_name, :primary_key, :left_column_name, :right_column_name, :arel_table, :quoted_table_name, :quoted_parent_column_full_name, :quoted_left_column_full_name, :quoted_right_column_full_name, :quoted_left_column_name, :quoted_right_column_name, :quoted_primary_column_name, :to => :model def query join_scope filter_scope end def join_scope join_arel = arel_table.join(parent, Arel::Nodes::OuterJoin).on(parent[primary_column_name].eq(arel_table[parent_column_name])) self.scope = scope.joins(join_arel.join_sql) end def filter_scope self.scope = scope.where( bound_is_null(left_column_name). or(bound_is_null(right_column_name)). or(left_bound_greater_than_right). or(parent_not_null.and(bounds_outside_parent)) ) end def bound_is_null(column_name) arel_table[column_name].eq(nil) end def left_bound_greater_than_right arel_table[left_column_name].gteq(arel_table[right_column_name]) end def parent_not_null arel_table[parent_column_name].not_eq(nil) end def bounds_outside_parent arel_table[left_column_name].lteq(parent[left_column_name]).or(arel_table[right_column_name].gteq(parent[right_column_name])) end end end end end awesome_nested_set-3.0.0/lib/awesome_nested_set.rb0000644000004100000410000000042612367660320022376 0ustar www-datawww-datarequire 'awesome_nested_set/awesome_nested_set' require 'active_record' ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet if defined?(ActionView) require 'awesome_nested_set/helper' ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper end awesome_nested_set-3.0.0/metadata.yml0000644000004100000410000000722212367660320017732 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: awesome_nested_set version: !ruby/object:Gem::Version version: 3.0.0 platform: ruby authors: - Brandon Keepers - Daniel Morrison - Philip Arndt autorequire: bindir: bin cert_chain: [] date: 2014-07-29 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: activerecord requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 4.0.0 - - "<" - !ruby/object:Gem::Version version: '5' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 4.0.0 - - "<" - !ruby/object:Gem::Version version: '5' - !ruby/object:Gem::Dependency name: rspec-rails requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.12' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.12' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '10' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '10' - !ruby/object:Gem::Dependency name: combustion requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 0.3.3 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 0.3.3 - !ruby/object:Gem::Dependency name: database_cleaner requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: An awesome nested set implementation for Active Record email: info@collectiveidea.com executables: [] extensions: [] extra_rdoc_files: - README.md files: - CHANGELOG - MIT-LICENSE - README.md - lib/awesome_nested_set.rb - lib/awesome_nested_set/awesome_nested_set.rb - lib/awesome_nested_set/columns.rb - lib/awesome_nested_set/helper.rb - lib/awesome_nested_set/iterator.rb - lib/awesome_nested_set/model.rb - lib/awesome_nested_set/model/movable.rb - lib/awesome_nested_set/model/prunable.rb - lib/awesome_nested_set/model/rebuildable.rb - lib/awesome_nested_set/model/relatable.rb - lib/awesome_nested_set/model/transactable.rb - lib/awesome_nested_set/model/validatable.rb - lib/awesome_nested_set/move.rb - lib/awesome_nested_set/set_validator.rb - lib/awesome_nested_set/tree.rb - lib/awesome_nested_set/version.rb homepage: http://github.com/collectiveidea/awesome_nested_set licenses: - MIT metadata: {} post_install_message: rdoc_options: - "--main" - README.md - "--inline-source" - "--line-numbers" require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.2.2 signing_key: specification_version: 4 summary: An awesome nested set implementation for Active Record test_files: [] awesome_nested_set-3.0.0/checksums.yaml.gz0000444000004100000410000000041412367660320020711 0ustar www-datawww-dataΰSeO9@si~ff hL<5(o{^߾9|?| :]jY)Yp._4ޥD2ݢx&H6([ @lB9g Fʭ#r ;]䎭kc +t-7i᪴(ht%4P9MqXIF!a]ukp=97ddϮz`Fx.Iawesome_nested_set-3.0.0/README.md0000644000004100000410000001653712367660320016717 0ustar www-datawww-data# Awesome Nested Set [![Build Status](https://travis-ci.org/collectiveidea/awesome_nested_set.png?branch=master)](https://travis-ci.org/collectiveidea/awesome_nested_set) [![Code Climate](https://codeclimate.com/github/collectiveidea/awesome_nested_set.png)](https://codeclimate.com/github/collectiveidea/awesome_nested_set) [![Dependency Status](https://gemnasium.com/collectiveidea/awesome_nested_set.svg)](https://gemnasium.com/collectiveidea/awesome_nested_set) Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is a replacement for acts_as_nested_set and BetterNestedSet, but more awesome. Version 3 supports Rails 4. Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2. ## What makes this so awesome? This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support. ## Installation Add to your Gemfile: ```ruby gem 'awesome_nested_set' ``` ## Usage To make use of `awesome_nested_set`, your model needs to have 3 fields: `lft`, `rgt`, and `parent_id`. The names of these fields are configurable. You can also have an optional field, `depth`: ```ruby class CreateCategories < ActiveRecord::Migration def self.up create_table :categories do |t| t.string :name t.integer :parent_id t.integer :lft t.integer :rgt t.integer :depth # this is optional. end end def self.down drop_table :categories end end ``` Enable the nested set functionality by declaring `acts_as_nested_set` on your model ```ruby class Category < ActiveRecord::Base acts_as_nested_set end ``` Run `rake rdoc` to generate the API docs and see [CollectiveIdea::Acts::NestedSet](lib/awesome_nested_set/awesome_nested_set.rb) for more information. ## Options You can pass various options to `acts_as_nested_set` macro. Configuration options are: * `parent_column`: specifies the column name to use for keeping the position integer (default: parent_id) * `left_column`: column name for left boundry data (default: lft) * `right_column`: column name for right boundry data (default: rgt) * `depth_column`: column name for the depth data default (default: depth) * `scope`: restricts what is to be considered a list. Given a symbol, it'll attach “_id” (if it hasn't been already) and use that as the foreign key restriction. You can also pass an array to scope by multiple attributes. Example: `acts_as_nested_set :scope => [:notable_id, :notable_type]` * `dependent`: behavior for cascading destroy. If set to :destroy, all the child objects are destroyed alongside this object by calling their destroy method. If set to :delete_all (default), all the child objects are deleted without calling their destroy method. * `counter_cache`: adds a counter cache for the number of children. defaults to false. Example: `acts_as_nested_set :counter_cache => :children_count` * `order_column`: on which column to do sorting, by default it is the left_column_name. Example: `acts_as_nested_set :order_column => :position` See [CollectiveIdea::Acts::NestedSet::Model::ClassMethods](/lib/awesome_nested_set/model.rb#L26) for a list of class methods and [CollectiveIdea::Acts::NestedSet::Model](lib/awesome_nested_set/model.rb#L13) for a list of instance methods added to acts_as_nested_set models ## Indexes It is highly recommended that you add an index to the `rgt` column on your models. Every insertion requires finding the next `rgt` value to use and this can be slow for large tables without an index. It is probably best to index the other fields as well (`parent_id`, `lft`, `depth`). ## Callbacks There are three callbacks called when moving a node: `before_move`, `after_move` and `around_move`. ```ruby class Category < ActiveRecord::Base acts_as_nested_set after_move :rebuild_slug around_move :da_fancy_things_around private def rebuild_slug # do whatever end def da_fancy_things_around # do something... yield # actually moves # do something else... end end ``` Beside this there are also hooks to act on the newly added or removed children. ```ruby class Category < ActiveRecord::Base acts_as_nested_set :before_add => :do_before_add_stuff, :after_add => :do_after_add_stuff, :before_remove => :do_before_remove_stuff, :after_remove => :do_after_remove_stuff private def do_before_add_stuff(child_node) # do whatever with the child end def do_after_add_stuff(child_node) # do whatever with the child end def do_before_remove_stuff(child_node) # do whatever with the child end def do_after_remove_stuff(child_node) # do whatever with the child end end ``` ## Protecting attributes from mass assignment It's generally best to "whitelist" the attributes that can be used in mass assignment: ```ruby class Category < ActiveRecord::Base acts_as_nested_set attr_accessible :name, :parent_id end ``` If for some reason that is not possible, you will probably want to protect the `lft` and `rgt` attributes: ```ruby class Category < ActiveRecord::Base acts_as_nested_set attr_protected :lft, :rgt end ``` ## Add to your existing project To make use of `awesome_nested_set`, your model needs to have 3 fields: `lft`, `rgt`, and `parent_id`. The names of these fields are configurable. You can also have an optional field, `depth`. Create a migration to add fields: ```ruby class AddNestedToCategories < ActiveRecord::Migration def self.up add_column :categories, :parent_id, :integer # Comment this line if your project already has this column # Category.where(parent_id: 0).update_all(parent_id: nil) # Uncomment this line if your project already has :parent_id add_column :categories, :lft , :integer add_column :categories, :rgt , :integer add_column :categories, :depth , :integer # this is optional. # This is necessary to update :lft and :rgt columns Category.rebuild! end def self.down remove_column :categories, :parent_id remove_column :categories, :lft remove_column :categories, :rgt remove_column :categories, :depth # this is optional. end end ``` Enable the nested set functionality by declaring `acts_as_nested_set` on your model ```ruby class Category < ActiveRecord::Base acts_as_nested_set end ``` Your project is now ready to run with the `awesome_nested_set` gem! ## Conversion from other trees Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run: ```ruby Category.rebuild! ``` Your tree will be converted to a valid nested set. Awesome! ## View Helper The view helper is called #nested_set_options. Example usage: ```erb <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %> <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %> ``` See [CollectiveIdea::Acts::NestedSet::Helper](lib/awesome_nested_set/helper.rb) for more information about the helpers. ## References You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html ## How to contribute Please see the ['Contributing' document](CONTRIBUTING.md). Copyright © 2008 - 2014 Collective Idea, released under the MIT license