Conflicts: app/models/issue.rbpull/261/head
commit
990794d111
@ -0,0 +1,164 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
# When included, it adds the ability to rebuild nested sets, thus fixing |
||||
# corrupted trees. |
||||
# |
||||
# AwesomeNestedSet has this functionality as well but it fixes the sets with |
||||
# running the callbacks defined in the model. This has two drawbacks: |
||||
# |
||||
# * It is prone to fail when a validation fails that has nothing to do with |
||||
# nested sets. |
||||
# * It is slow. |
||||
# |
||||
# The methods included are purely sql based. The code in here is partly copied |
||||
# over from awesome_nested_set's non sql methods. |
||||
|
||||
module OpenProject::NestedSet::RebuildPatch |
||||
def self.included(base) |
||||
base.class_eval do |
||||
scope :invalid_left_and_rights, |
||||
:joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " + |
||||
"#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}", |
||||
:conditions => |
||||
"#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " + |
||||
"#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " + |
||||
"#{quoted_table_name}.#{quoted_left_column_name} >= " + |
||||
"#{quoted_table_name}.#{quoted_right_column_name} OR " + |
||||
"(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " + |
||||
"(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " + |
||||
"#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))" |
||||
|
||||
scope :invalid_duplicates_in_columns, lambda { |
||||
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c| |
||||
"#{quoted_table_name}.#{connection.quote_column_name(c)} = duplicates.#{connection.quote_column_name(c)}" |
||||
end.join(" AND ") |
||||
|
||||
scope_string = scope_string.size > 0 ? scope_string + " AND " : "" |
||||
|
||||
{ :joins => "LEFT OUTER JOIN #{quoted_table_name} AS duplicates ON " + |
||||
scope_string + |
||||
"#{quoted_table_name}.#{primary_key} != duplicates.#{primary_key} AND " + |
||||
"(#{quoted_table_name}.#{quoted_left_column_name} = duplicates.#{quoted_left_column_name} OR " + |
||||
"#{quoted_table_name}.#{quoted_right_column_name} = duplicates.#{quoted_right_column_name})", |
||||
:conditions => "duplicates.#{primary_key} IS NOT NULL" } |
||||
} |
||||
|
||||
scope :invalid_roots, lambda { |
||||
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c| |
||||
"#{quoted_table_name}.#{connection.quote_column_name(c)} = other.#{connection.quote_column_name(c)}" |
||||
end.join(" AND ") |
||||
|
||||
scope_string = scope_string.size > 0 ? scope_string + " AND " : "" |
||||
|
||||
{ :joins => "LEFT OUTER JOIN #{quoted_table_name} AS other ON " + |
||||
"#{quoted_table_name}.#{primary_key} != other.#{primary_key} AND " + |
||||
"#{quoted_table_name}.#{parent_column_name} IS NULL AND " + |
||||
"other.#{parent_column_name} IS NULL AND " + |
||||
scope_string + |
||||
"#{quoted_table_name}.#{quoted_left_column_name} <= other.#{quoted_right_column_name} AND " + |
||||
"#{quoted_table_name}.#{quoted_right_column_name} >= other.#{quoted_left_column_name}", |
||||
:conditions => "other.#{primary_key} IS NOT NULL", |
||||
:order => quoted_left_column_name } |
||||
} |
||||
|
||||
extend(ClassMethods) |
||||
end |
||||
end |
||||
|
||||
module ClassMethods |
||||
def selectively_rebuild_silently! |
||||
all_invalid |
||||
|
||||
invalid_roots, invalid_descendants = all_invalid.partition{ |node| node.send(parent_column_name).nil? } |
||||
|
||||
while invalid_descendants.size > 0 do |
||||
invalid_descendants_parents = invalid_descendants.map{ |node| find(node.send(parent_column_name)) } |
||||
|
||||
new_invalid_roots, invalid_descendants = invalid_descendants_parents.partition{ |node| node.send(parent_column_name).nil? } |
||||
|
||||
invalid_roots += new_invalid_roots |
||||
|
||||
invalid_descendants.uniq! |
||||
end |
||||
|
||||
rebuild_silently!(invalid_roots.uniq) |
||||
end |
||||
|
||||
# Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree. |
||||
# Very similar to original nested_set implementation but uses update_all so that callbacks are not triggered |
||||
def rebuild_silently!(roots = nil) |
||||
# Don't rebuild a valid tree. |
||||
return true if valid? |
||||
|
||||
scope = lambda{ |node| } |
||||
if acts_as_nested_set_options[:scope] |
||||
scope = lambda{ |node| |
||||
scope_column_names.inject(""){|str, column_name| |
||||
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} " |
||||
} |
||||
} |
||||
end |
||||
|
||||
# setup index |
||||
|
||||
indices = Hash.new do |h, k| |
||||
h[k] = 0 |
||||
end |
||||
|
||||
set_left_and_rights = lambda do |node| |
||||
|
||||
# set left |
||||
node[left_column_name] = indices[scope.call(node)] += 1 |
||||
# find |
||||
children = all(:conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], |
||||
:order => [quoted_left_column_name, |
||||
quoted_right_column_name, |
||||
acts_as_nested_set_options[:order]].compact.join(", ")) |
||||
|
||||
children.each{ |n| set_left_and_rights.call(n) } |
||||
|
||||
# set right |
||||
node[right_column_name] = indices[scope.call(node)] += 1 |
||||
|
||||
changes = node.changes.inject({}) do |hash, (attribute, values)| |
||||
hash[attribute] = node.send(attribute.to_s) |
||||
hash |
||||
end |
||||
|
||||
update_all(changes, { :id => node.id }) unless changes.empty? |
||||
end |
||||
|
||||
# Find root node(s) |
||||
# or take provided |
||||
root_nodes = if roots.is_a? Array |
||||
roots |
||||
elsif roots.present? |
||||
[roots] |
||||
else |
||||
all(:conditions => "#{quoted_parent_column_name} IS NULL", |
||||
:order => [quoted_left_column_name, |
||||
quoted_right_column_name, |
||||
acts_as_nested_set_options[:order]].compact.join(", ")) |
||||
end |
||||
|
||||
root_nodes.each do |root_node| |
||||
set_left_and_rights.call(root_node) |
||||
end |
||||
end |
||||
|
||||
def all_invalid |
||||
invalid = invalid_roots + invalid_left_and_rights + invalid_duplicates_in_columns |
||||
invalid.uniq |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,221 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
# When included it adds the nested_set behaviour scoped by the attribute |
||||
# 'root_id' |
||||
# |
||||
# AwesomeNestedSet offers beeing scoped but does not handle inserting and |
||||
# updating with the scoped beeing set right. This module adds this. |
||||
# |
||||
# When beeing scoped, we no longer have one big set over the the entire table |
||||
# but a forest of sets instead. |
||||
# |
||||
# The idea of this extension is to always place the node in the correct set |
||||
# before standard awesome_nested_set does something. This is necessary as all |
||||
# awesome_nested_set methods check for the scope. Operations crossing the |
||||
# border of a set are not supported. |
||||
# |
||||
# One goal of this implementation is to avoid using move_to of |
||||
# awesome_nested_set so that the callbacks defined for move_to (:before_move, |
||||
# :after_move and :around_move) can safely be used. |
||||
|
||||
module OpenProject::NestedSet |
||||
module RootIdHandling |
||||
def self.included(base) |
||||
base.class_eval do |
||||
after_save :manage_root_id |
||||
acts_as_nested_set :scope => 'root_id', :dependent => :destroy |
||||
|
||||
# callback from awesome_nested_set |
||||
# we call it by hand as we have to set the scope first |
||||
skip_callback :create, :before, :set_default_left_and_right |
||||
|
||||
validate :validate_correct_parent |
||||
|
||||
include InstanceMethods |
||||
end |
||||
end |
||||
|
||||
module InstanceMethods |
||||
|
||||
# The number of "items" this issue spans in it's nested set |
||||
# |
||||
# A parent issue would span all of it's children + 1 left + 1 right (3) |
||||
# |
||||
# | parent | |
||||
# || child || |
||||
# |
||||
# A child would span only itself (1) |
||||
# |
||||
# |child| |
||||
def nested_set_span |
||||
rgt - lft |
||||
end |
||||
|
||||
# Does this issue have children? |
||||
def children? |
||||
!leaf? |
||||
end |
||||
|
||||
def validate_correct_parent |
||||
# Checks parent issue assignment |
||||
if parent |
||||
if !Setting.cross_project_issue_relations? && parent.project_id != self.project_id |
||||
errors.add :parent_id, :not_a_valid_parent |
||||
elsif !new_record? |
||||
# moving an existing issue |
||||
if parent.root_id != root_id |
||||
# we can always move to another tree |
||||
elsif move_possible?(parent) |
||||
# move accepted inside tree |
||||
else |
||||
errors.add :parent_id, :not_a_valid_parent |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
def parent_issue_id=(arg) |
||||
warn "[DEPRECATION] No longer use parent_issue_id= - Use parent_id= instead." |
||||
|
||||
self.parent_id = arg |
||||
end |
||||
|
||||
def parent_issue_id |
||||
warn "[DEPRECATION] No longer use parent_issue_id - Use parent_id instead." |
||||
|
||||
parent_id |
||||
end |
||||
|
||||
private |
||||
|
||||
def manage_root_id |
||||
if root_id.nil? # new node |
||||
initial_root_id |
||||
elsif parent_id_changed? |
||||
update_root_id |
||||
end |
||||
end |
||||
|
||||
# Places the node in the correct set upon creation. |
||||
# |
||||
# If a parent is provided on creation, the new node is placed in the set |
||||
# of the parent. If no parent is provided, the new node defines it's own |
||||
# set. |
||||
def initial_root_id |
||||
if parent_id |
||||
self.root_id = parent.root_id |
||||
else |
||||
self.root_id = id |
||||
end |
||||
|
||||
set_default_left_and_right |
||||
persist_nested_set_attributes |
||||
end |
||||
|
||||
# Places the node in a new set when necessary, so that it can be assigned |
||||
# to a different parent. |
||||
# |
||||
# This method does nothing if the new parent is within the same set. The |
||||
# method puts the node and all it's descendants in the set of the |
||||
# designated parent if the designated parent is within another set. |
||||
def update_root_id |
||||
new_root_id = parent_id.nil? ? id : parent.root_id |
||||
|
||||
if new_root_id != root_id |
||||
# as the following actions depend on the |
||||
# node having current values, we reload them here |
||||
self.reload_nested_set |
||||
|
||||
# and save them in order to be save between removing the node from |
||||
# the set and fixing the former set's attributes |
||||
old_root_id = root_id |
||||
old_rgt = rgt |
||||
|
||||
moved_span = nested_set_span + 1 |
||||
|
||||
move_subtree_to_new_set(new_root_id) |
||||
correct_former_set_attributes(old_root_id, moved_span, old_rgt) |
||||
end |
||||
end |
||||
|
||||
def persist_nested_set_attributes |
||||
self.class.update_all("root_id = #{root_id}, " + |
||||
"#{quoted_left_column_name} = #{lft}, " + |
||||
"#{quoted_right_column_name} = #{rgt}", |
||||
["id = ?", id]) |
||||
end |
||||
|
||||
# Moves the node and all it's descendants to the set with the provided |
||||
# root_id. It does not change the parent/child relationships. |
||||
# |
||||
# The subtree is placed to the right of the existing tree. All the |
||||
# subtree's nodes receive new lft/rgt values that are higher than the |
||||
# maximum rgt value of the set. |
||||
# |
||||
# The set than has two roots. As such this method should only be used |
||||
# internally and the results should only be persisted for a short time. |
||||
def move_subtree_to_new_set(new_root_id) |
||||
old_root_id = self.root_id |
||||
self.root_id = new_root_id |
||||
|
||||
target_maxright = nested_set_scope.maximum(right_column_name) || 0 |
||||
offset = target_maxright + 1 - lft |
||||
|
||||
# update all the sutree's nodes. The lft and right values are incremented |
||||
# by the maximum of the set's right value. |
||||
self.class.update_all("root_id = #{root_id}, " + |
||||
"#{quoted_left_column_name} = lft + #{offset}, " + |
||||
"#{quoted_right_column_name} = rgt + #{offset}", |
||||
["root_id = ? AND " + |
||||
"#{quoted_left_column_name} >= ? AND " + |
||||
"#{quoted_right_column_name} <= ? ", old_root_id, lft, rgt]) |
||||
|
||||
self[left_column_name] = lft + offset |
||||
self[right_column_name] = rgt + offset |
||||
end |
||||
|
||||
# Update all nodes left and right values in the former set having a right |
||||
# value larger than self's former right value. |
||||
# |
||||
# It calculates what will have to be subtracted from the left and right |
||||
# values of the nodes in question. Then it will always subtract this |
||||
# value from the right value of every node. It will only subtract the |
||||
# value from the left value if the left value is larger than the removed |
||||
# node's right value. |
||||
# |
||||
# Given a set: |
||||
# 1*6 |
||||
# / \ |
||||
# 2*3 4*5 |
||||
# for wich the node with lft = 2 and rgt = 3 is self and was removed, the |
||||
# resulting set will be: |
||||
# 1*4 |
||||
# | |
||||
# 2*3 |
||||
|
||||
def correct_former_set_attributes(old_root_id, removed_span, rgt_offset) |
||||
# As every node takes two integers we can multiply the amount of |
||||
# removed_nodes by 2 to calculate the value by which right and left |
||||
# will have to be reduced. |
||||
#removed_span = removed_nodes * 2 |
||||
|
||||
self.class.update_all("#{quoted_right_column_name} = #{quoted_right_column_name} - #{removed_span}, " + |
||||
"#{quoted_left_column_name} = CASE " + |
||||
"WHEN #{quoted_left_column_name} > #{rgt_offset} " + |
||||
"THEN #{quoted_left_column_name} - #{removed_span} " + |
||||
"ELSE #{quoted_left_column_name} END", |
||||
["root_id = ? AND #{quoted_right_column_name} > ?", old_root_id, rgt_offset]) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,104 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
# This module, when included, adds the ability to rebuild nested sets that are |
||||
# scoped by a root_id attribute. |
||||
# |
||||
# For the details of rebuilding see the included RebuildPatch. |
||||
|
||||
module OpenProject::NestedSet |
||||
module RootIdRebuilding |
||||
def self.included(base) |
||||
base.class_eval do |
||||
|
||||
include RebuildPatch |
||||
|
||||
# find all nodes |
||||
# * having set a parent_id where the root_id |
||||
# 1) points to self |
||||
# 2) points to a node with a parent |
||||
# 3) points to a node having a different root_id |
||||
# * having not set a parent_id but a root_id |
||||
# This unfortunately does not find the node with the id 3 in the following example |
||||
# | id | parent_id | root_id | |
||||
# | 1 | | 1 | |
||||
# | 2 | 1 | 2 | |
||||
# | 3 | 2 | 2 | |
||||
# This would only be possible using recursive statements |
||||
scope :invalid_root_ids, { :conditions => "(#{quoted_parent_column_full_name} IS NOT NULL AND " + |
||||
"(#{quoted_table_name}.root_id = #{quoted_table_name}.id OR " + |
||||
"(#{quoted_table_name}.root_id = parents.#{quoted_primary_key} AND parents.#{quoted_parent_column_name} IS NOT NULL) OR " + |
||||
"(#{quoted_table_name}.root_id != parents.root_id))" + |
||||
") OR " + |
||||
"(#{quoted_table_name}.parent_id IS NULL AND #{quoted_table_name}.root_id != #{quoted_table_name}.#{quoted_primary_key})", |
||||
:joins => "LEFT OUTER JOIN #{quoted_table_name} parents ON parents.#{quoted_primary_key} = #{quoted_parent_column_full_name}" } |
||||
|
||||
extend ClassMethods |
||||
end |
||||
end |
||||
|
||||
module ClassMethods |
||||
# method from acts_as_nested_set |
||||
def valid? |
||||
super && invalid_root_ids.empty? |
||||
end |
||||
|
||||
def all_invalid |
||||
(super + invalid_root_ids).uniq |
||||
end |
||||
|
||||
def rebuild_silently!(roots = nil) |
||||
|
||||
invalid_root_ids_to_fix = if roots.is_a? Array |
||||
roots |
||||
elsif roots.present? |
||||
[roots] |
||||
else |
||||
[] |
||||
end |
||||
|
||||
known_node_parents = Hash.new do |hash, ancestor_id| |
||||
hash[ancestor_id] = find_by_id(ancestor_id) |
||||
end |
||||
|
||||
fix_known_invalid_root_ids = lambda do |
||||
invalid_nodes = invalid_root_ids |
||||
|
||||
invalid_roots = [] |
||||
|
||||
invalid_nodes.each do |node| |
||||
# At this point we can not trust nested set methods as the root_id is invalid. |
||||
# Therefore we trust the parent_id to fetch all ancestors until we find the root |
||||
ancestor = node |
||||
|
||||
while ancestor.parent_id do |
||||
ancestor = known_node_parents[ancestor.parent_id] |
||||
end |
||||
|
||||
invalid_roots << ancestor |
||||
|
||||
if invalid_root_ids_to_fix.empty? || invalid_root_ids_to_fix.map(&:id).include?(ancestor.id) |
||||
update_all({ :root_id => ancestor.id }, |
||||
{ :id => node.id }) |
||||
end |
||||
end |
||||
|
||||
fix_known_invalid_root_ids.call unless (invalid_roots.map(&:id) & invalid_root_ids_to_fix.map(&:id)).empty? |
||||
end |
||||
|
||||
fix_known_invalid_root_ids.call |
||||
|
||||
super |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,22 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module OpenProject::NestedSet |
||||
module WithRootIdScope |
||||
def self.included(base) |
||||
base.class_eval do |
||||
include RootIdHandling |
||||
include RootIdRebuilding |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,240 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
# TODO: this spec is for now targeting each WorkPackage subclass |
||||
# independently. Once only WorkPackage exist, this can safely be consolidated. |
||||
describe WorkPackage do |
||||
let(:project) { FactoryGirl.build(:project_with_types) } |
||||
let(:issue) { FactoryGirl.build(:issue, :project => project, :type => project.types.first) } |
||||
let(:issue2) { FactoryGirl.build(:issue, :project => project, :type => project.types.first) } |
||||
let(:issue3) { FactoryGirl.build(:issue, :project => project, :type => project.types.first) } |
||||
let(:planning_element) { FactoryGirl.build(:planning_element, :project => project) } |
||||
let(:planning_element2) { FactoryGirl.build(:planning_element, :project => project) } |
||||
let(:planning_element3) { FactoryGirl.build(:planning_element, :project => project) } |
||||
|
||||
[:issue, :planning_element].each do |subclass| |
||||
|
||||
describe "(#{subclass})" do |
||||
let(:instance) { send(subclass) } |
||||
let(:parent) { send(:"#{subclass}2") } |
||||
let(:parent2) { send(:"#{subclass}3") } |
||||
|
||||
shared_examples_for "root" do |
||||
it "should set root_id to the id of the #{subclass}" do |
||||
instance.root_id.should == instance.id |
||||
end |
||||
|
||||
it "should set lft to 1" do |
||||
instance.lft.should == 1 |
||||
end |
||||
|
||||
it "should set rgt to 2" do |
||||
instance.rgt.should == 2 |
||||
end |
||||
end |
||||
|
||||
shared_examples_for "first child" do |
||||
it "should set root_id to the id of the parent #{subclass}" do |
||||
instance.root_id.should == parent.id |
||||
end |
||||
|
||||
it "should set lft to 2" do |
||||
instance.lft.should == 2 |
||||
end |
||||
|
||||
it "should set rgt to 3" do |
||||
instance.rgt.should == 3 |
||||
end |
||||
end |
||||
|
||||
describe "creating a new instance without a parent" do |
||||
|
||||
before do |
||||
instance.save! |
||||
end |
||||
|
||||
it_should_behave_like "root" |
||||
end |
||||
|
||||
describe "creating a new instance with a parent" do |
||||
|
||||
before do |
||||
parent.save! |
||||
instance.parent = parent |
||||
|
||||
instance.save! |
||||
end |
||||
|
||||
it_should_behave_like "first child" |
||||
end |
||||
|
||||
describe "an existant instance receives a parent" do |
||||
|
||||
before do |
||||
parent.save! |
||||
instance.save! |
||||
instance.parent = parent |
||||
instance.save! |
||||
end |
||||
|
||||
it_should_behave_like "first child" |
||||
end |
||||
|
||||
describe "an existant instance becomes a root" do |
||||
|
||||
before do |
||||
parent.save! |
||||
instance.parent = parent |
||||
instance.save! |
||||
instance.parent_id = nil |
||||
instance.save! |
||||
end |
||||
|
||||
it_should_behave_like "root" |
||||
|
||||
it "should set parent_id to nil" do |
||||
instance.parent_id.should == nil |
||||
end |
||||
end |
||||
|
||||
describe "an existant instance receives a new parent (new tree)" do |
||||
|
||||
before do |
||||
parent.save! |
||||
parent2.save! |
||||
instance.parent_id = parent2.id |
||||
instance.save! |
||||
|
||||
instance.parent = parent |
||||
instance.save! |
||||
end |
||||
|
||||
it_should_behave_like "first child" |
||||
|
||||
it "should set parent_id to new parent" do |
||||
instance.parent_id.should == parent.id |
||||
end |
||||
end |
||||
|
||||
describe "an existant instance |
||||
with a right sibling receives a new parent" do |
||||
|
||||
let(:other_child) { send(:"#{subclass}3") } |
||||
|
||||
before do |
||||
parent.save! |
||||
instance.parent = parent |
||||
instance.save! |
||||
other_child.parent = parent |
||||
other_child.save! |
||||
|
||||
instance.parent_id = nil |
||||
instance.save! |
||||
end |
||||
|
||||
it "former roots's root_id should be unchanged" do |
||||
parent.reload |
||||
parent.root_id.should == parent.id |
||||
end |
||||
|
||||
it "former roots's lft should be 1" do |
||||
parent.reload |
||||
parent.lft.should == 1 |
||||
end |
||||
|
||||
it "former roots's rgt should be 4" do |
||||
parent.reload |
||||
parent.rgt.should == 4 |
||||
end |
||||
|
||||
it "former right siblings's root_id should be unchanged" do |
||||
other_child.reload |
||||
other_child.root_id.should == parent.id |
||||
end |
||||
|
||||
it "former right siblings's left should be 2" do |
||||
other_child.reload |
||||
other_child.lft.should == 2 |
||||
end |
||||
|
||||
it "former right siblings's rgt should be 3" do |
||||
other_child.reload |
||||
other_child.rgt.should == 3 |
||||
end |
||||
end |
||||
|
||||
describe "an existant instance receives a new parent (same tree)" do |
||||
|
||||
before do |
||||
parent.save! |
||||
parent2.save! |
||||
instance.parent_id = parent2.id |
||||
instance.save! |
||||
|
||||
instance.parent = parent |
||||
instance.save! |
||||
end |
||||
|
||||
it_should_behave_like "first child" |
||||
end |
||||
|
||||
describe "an existant instance with children receives a new parent (itself)" do |
||||
let(:child) { send(:"#{subclass}3") } |
||||
|
||||
before do |
||||
parent.save! |
||||
instance.parent = parent |
||||
instance.save! |
||||
child.parent_id = instance.id |
||||
child.save! |
||||
|
||||
# reloading as instance's nested set attributes (lft, rgt) where |
||||
# updated by adding child to the set |
||||
instance.reload |
||||
instance.parent_id = nil |
||||
instance.save! |
||||
end |
||||
|
||||
it "former parent's root_id should be unchanged" do |
||||
parent.reload |
||||
parent.root_id.should == parent.id |
||||
end |
||||
|
||||
it "former parent's left should be 1" do |
||||
parent.reload |
||||
parent.lft.should == 1 |
||||
end |
||||
|
||||
it "former parent's right should be 2" do |
||||
parent.reload |
||||
parent.rgt.should == 2 |
||||
end |
||||
|
||||
it "the child should have the root_id of the parent #{subclass}" do |
||||
child.reload |
||||
child.root_id.should == instance.id |
||||
end |
||||
|
||||
it "the child should have a lft of 2" do |
||||
child.reload |
||||
child.lft.should == 2 |
||||
end |
||||
|
||||
it "the child should have a rgt of 3" do |
||||
child.reload |
||||
child.rgt.should == 3 |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,938 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# |
||||
# Copyright (C) 2012-2013 the OpenProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require File.dirname(__FILE__) + '/../spec_helper' |
||||
|
||||
describe WorkPackage, "rebuilding nested set" do |
||||
let(:project) { FactoryGirl.create(:valid_project) } |
||||
let(:status) { FactoryGirl.create(:issue_status) } |
||||
let(:priority) { FactoryGirl.create(:priority) } |
||||
let(:type) { project.types.first } |
||||
let(:author) { FactoryGirl.create(:user) } |
||||
|
||||
def issue_factory(parent = nil) |
||||
FactoryGirl.create(:issue, :status => status, |
||||
:project => project, |
||||
:priority => priority, |
||||
:author => author, |
||||
:type => type, |
||||
:parent => parent) |
||||
end |
||||
|
||||
let(:root_1) { issue_factory } |
||||
let(:root_2) { issue_factory } |
||||
let(:child_1_1) { issue_factory(root_1) } |
||||
let(:child_1_2) { issue_factory(root_1) } |
||||
let(:child_2_1) { issue_factory(root_2) } |
||||
let(:gchild_1_1_1) { issue_factory(child_1_1) } |
||||
let(:ggchild_1_1_1_1) { issue_factory(gchild_1_1_1) } |
||||
let(:gchild_1_1_2) { issue_factory(child_1_1) } |
||||
let(:gchild_1_2_1) { issue_factory(child_1_2) } |
||||
let(:gchild_2_1_1) { issue_factory(child_2_1) } |
||||
|
||||
describe :valid? do |
||||
describe "WITH one root issue" do |
||||
before do |
||||
root_1 |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH two one node trees" do |
||||
before do |
||||
root_1 |
||||
root_2 |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree" do |
||||
before do |
||||
child_1_1 |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a three issue deep tree" do |
||||
before do |
||||
gchild_1_1_1 |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH the left value of the child beeing invalid" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => root_1.lft }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.should_not be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH the right value of the child beeing invalid" do |
||||
|
||||
before do |
||||
Issue.update_all({ :rgt => 18 }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.should_not be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH the root_id of the child pointing to itself" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.should_not be_valid } |
||||
end |
||||
|
||||
describe "WITH a three issue deep tree |
||||
WITH the root_id of the grand child pointing to the child" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => gchild_1_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.should_not be_valid } |
||||
end |
||||
end |
||||
|
||||
describe :rebuild! do |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the left value of the child beeing invalid" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => root_1.lft }, { :id => child_1_1.id }) |
||||
|
||||
Issue.rebuild! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
end |
||||
|
||||
describe :rebuild_silently! do |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the left value of the child beeing invalid" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => root_1.lft }, { :id => child_1_1.id }) |
||||
|
||||
Issue.rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the left value of the root beeing invalid |
||||
WITH an estimated_hours values set for the root after the tree got broken" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => child_1_1.lft }, { :id => root_1.id }) |
||||
Issue.update_all({ :estimated_hours => 1.0 }, { :id => root_1.id }) |
||||
|
||||
Issue.rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the right value of the root beeing invalid |
||||
WITH an estimated_hours values set for the root after the tree got broken" do |
||||
|
||||
before do |
||||
Issue.update_all({ :rgt => child_1_1.lft }, { :id => root_1.id }) |
||||
Issue.update_all({ :estimated_hours => 1.0 }, { :id => root_1.id }) |
||||
|
||||
Issue.rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the root_id value of the child pointing to itself" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => child_1_1.id }) |
||||
|
||||
Issue.rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a three issues deep tree |
||||
WITH the root_id value of the grandchild pointing to itself" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => gchild_1_1_1.id }, { :id => gchild_1_1_1.id }) |
||||
|
||||
Issue.rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a three issues deep tree |
||||
WITH the root_id value of the grandchild pointing to the child" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => gchild_1_1_1.id }) |
||||
|
||||
Issue.rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH two three issues deep trees |
||||
WITH the root_id value of each grandchildren pointing to the children |
||||
WITH selecting to fix only one tree" do |
||||
|
||||
before do |
||||
gchild_1_1_1 |
||||
gchild_2_1_1 |
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => gchild_1_1_1.id }) |
||||
Issue.update_all({ :root_id => child_2_1.id }, { :id => gchild_2_1_1.id }) |
||||
|
||||
Issue.rebuild_silently!(root_1) |
||||
end |
||||
|
||||
it { gchild_1_1_1.reload.root_id.should == root_1.id } |
||||
it { gchild_2_1_1.reload.root_id.should == child_2_1.id } |
||||
end |
||||
|
||||
describe "WITH two three issues deep trees |
||||
WITH the right value of each grandchildren beeing equal to the left value |
||||
WITH selecting to fix only one tree" do |
||||
|
||||
before do |
||||
gchild_1_1_1 |
||||
gchild_2_1_1 |
||||
Issue.update_all({ :rgt => gchild_1_1_1.lft }, { :id => gchild_1_1_1.id }) |
||||
Issue.update_all({ :rgt => gchild_2_1_1.lft }, { :id => gchild_2_1_1.id }) |
||||
|
||||
Issue.rebuild_silently!(root_1) |
||||
end |
||||
|
||||
it { gchild_1_1_1.reload.rgt.should == gchild_1_1_1.lft + 1 } |
||||
it { gchild_2_1_1.reload.rgt.should == gchild_2_1_1.lft } |
||||
end |
||||
end |
||||
|
||||
describe :selectively_rebuild_silently! do |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the left value of the child beeing invalid" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => root_1.lft }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the left value of the root beeing invalid |
||||
WITH an estimated_hours values set for the root after the tree got broken" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => child_1_1.lft }, { :id => root_1.id }) |
||||
Issue.update_all({ :estimated_hours => 1.0 }, { :id => root_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the right value of the root beeing invalid |
||||
WITH an estimated_hours values set for the root after the tree got broken" do |
||||
|
||||
before do |
||||
Issue.update_all({ :rgt => child_1_1.lft }, { :id => root_1.id }) |
||||
Issue.update_all({ :estimated_hours => 1.0 }, { :id => root_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the root_id value of the child pointing to itself" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a three issues deep tree |
||||
WITH the root_id value of the grandchild pointing to itself" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => gchild_1_1_1.id }, { :id => gchild_1_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a three issues deep tree |
||||
WITH the root_id value of the grandchild pointing to the child" do |
||||
|
||||
before do |
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => gchild_1_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a one issue deep tree |
||||
WITH the root_id beeing null" do |
||||
before do |
||||
root_1 |
||||
|
||||
Issue.update_all({ :root_id => nil }, { :id => root_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH two one issue deep trees |
||||
WITH the root_id beeing of one pointing to the other" do |
||||
before do |
||||
root_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :root_id => root_2.id }, { :id => root_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH the root_id of the child pointing to itself" do |
||||
before do |
||||
child_1_1 |
||||
|
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a tree issue deep tree |
||||
WITH the root_id of the child pointing to another tree |
||||
WITH the root_id of the grandchild pointing to the same other tree" do |
||||
before do |
||||
gchild_1_1_1 |
||||
|
||||
Issue.update_all({ :root_id => 0 }, { :id => child_1_1.id }) |
||||
Issue.update_all({ :root_id => 0 }, { :id => gchild_1_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH a one issue deep tree |
||||
WITH the root_id of the child pointing to the other tree" do |
||||
before do |
||||
child_1_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :root_id => root_2.id }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a one issue deep tree |
||||
WITH right > left" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => 2, :rgt => 1 }, { :id => root_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH everything ok" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right > left" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => 4, :rgt => 3 }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right = left" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => 3, :rgt => 3 }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing null" do |
||||
|
||||
before do |
||||
Issue.update_all({ :rgt => nil }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing null" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => nil }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing equal to the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.rgt }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing larger than the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.rgt + 1 }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing equal to the root's left" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :lft => root_1.reload.lft }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing less than the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.lft - 1 }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing equal to the root's left" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :lft => root_1.reload.lft }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing equal to the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.rgt }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH a three issues deep tree |
||||
WITH the child's right beeing equal to the grandchild's right" do |
||||
|
||||
before do |
||||
gchild_1_1_1 |
||||
Issue.update_all({ :rgt => gchild_1_1_1.reload.rgt }, { :id => child_1_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH two one issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the left of the one being the right of the other" do |
||||
|
||||
before do |
||||
root_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :lft => root_1.lft, :root_id => root_1.id }, { :id => root_2.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH two one issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the right of the one being the lft of the other" do |
||||
|
||||
before do |
||||
root_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :rgt => root_2.lft, :root_id => root_2.id }, { :id => root_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH one one issue deep tree |
||||
WITH one two issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the left of the one between left and right of the other" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :lft => child_1_1.lft, :root_id => root_1.id }, { :id => root_2.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
|
||||
describe "WITH one one issue deep tree |
||||
WITH one two issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the right of the one between left and right of the other" do |
||||
|
||||
before do |
||||
root_1 |
||||
child_2_1 |
||||
|
||||
Issue.update_all({ :rgt => child_2_1.rgt, :root_id => root_2.id}, { :id => root_1.id }) |
||||
|
||||
Issue.selectively_rebuild_silently! |
||||
end |
||||
|
||||
it { Issue.should be_valid } |
||||
end |
||||
end |
||||
|
||||
describe :invalid_left_and_rights do |
||||
describe "WITH a one issue deep tree |
||||
WITH right > left" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => 2, :rgt => 1 }, { :id => root_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [root_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH everything ok" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right > left" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => 4, :rgt => 3 }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right = left" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => 3, :rgt => 3 }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing null" do |
||||
|
||||
before do |
||||
Issue.update_all({ :rgt => nil }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing null" do |
||||
|
||||
before do |
||||
Issue.update_all({ :lft => nil }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing equal to the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.rgt }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing larger than the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.rgt + 1 }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing equal to the root's left" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :lft => root_1.reload.lft }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing less than the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.lft - 1 }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_left_and_rights.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
end |
||||
|
||||
describe :invalid_duplicates_in_columns do |
||||
describe "WITH a two issues deep tree |
||||
WITH the child's left beeing equal to the root's left" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :lft => root_1.reload.lft }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_duplicates_in_columns.map(&:id).should =~ [root_1.id, child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issues deep tree |
||||
WITH the child's right beeing equal to the root's right" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
Issue.update_all({ :rgt => root_1.reload.rgt }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_duplicates_in_columns.map(&:id).should =~ [root_1.id, child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH two one issue deep tree |
||||
WITH everything ok" do |
||||
|
||||
before do |
||||
root_1 |
||||
root_2 |
||||
end |
||||
|
||||
it { Issue.invalid_duplicates_in_columns.map(&:id).should =~ [] } |
||||
end |
||||
|
||||
describe "WITH a three issues deep tree |
||||
WITH the child's right beeing equal to the grandchild's right" do |
||||
|
||||
before do |
||||
gchild_1_1_1 |
||||
Issue.update_all({ :rgt => gchild_1_1_1.reload.rgt }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_duplicates_in_columns.map(&:id).should =~ [child_1_1.id, gchild_1_1_1.id] } |
||||
end |
||||
end |
||||
|
||||
describe :invalid_roots do |
||||
describe "WITH two one issues deep tree |
||||
WITH everything ok" do |
||||
|
||||
before do |
||||
root_1 |
||||
root_2 |
||||
end |
||||
|
||||
it { Issue.invalid_roots.should be_empty } |
||||
end |
||||
|
||||
describe "WITH two one issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the left of the one being the right of the other" do |
||||
|
||||
before do |
||||
root_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :lft => root_1.lft, :root_id => root_1.id }, { :id => root_2.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_roots.map(&:id).should =~ [root_1.id, root_2.id] } |
||||
end |
||||
|
||||
describe "WITH two one issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the right of the one being the lft of the other" do |
||||
|
||||
before do |
||||
root_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :rgt => root_2.lft, :root_id => root_2.id }, { :id => root_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_roots.map(&:id).should =~ [root_1.id, root_2.id] } |
||||
end |
||||
|
||||
describe "WITH one one issue deep tree |
||||
WITH one two issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the left of the one between left and right of the other" do |
||||
|
||||
before do |
||||
child_1_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :lft => child_1_1.lft, :root_id => root_1.id }, { :id => root_2.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_roots.map(&:id).should =~ [root_1.id, root_2.id] } |
||||
end |
||||
|
||||
describe "WITH one one issue deep tree |
||||
WITH one two issues deep tree |
||||
WITH the two trees in the same scope (should not happen for issues) |
||||
WITH the right of the one between left and right of the other" do |
||||
|
||||
before do |
||||
root_1 |
||||
child_2_1 |
||||
|
||||
Issue.update_all({ :rgt => child_2_1.rgt, :root_id => root_2.id}, { :id => root_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_roots.map(&:id).should =~ [root_1.id, root_2.id] } |
||||
end |
||||
end |
||||
|
||||
describe :invalid_root_ids do |
||||
describe "WITH a one issue deep tree |
||||
WITH everything ok" do |
||||
before do |
||||
root_1 |
||||
end |
||||
|
||||
it { Issue.invalid_root_ids.should be_empty } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH everything ok" do |
||||
before do |
||||
child_1_1 |
||||
end |
||||
|
||||
it { Issue.invalid_root_ids.should be_empty } |
||||
end |
||||
|
||||
describe "WITH a three issue deep tree |
||||
WITH everything ok" do |
||||
before do |
||||
gchild_1_1_1 |
||||
end |
||||
|
||||
it { Issue.invalid_root_ids.should be_empty } |
||||
end |
||||
|
||||
describe "WITH a one issue deep tree |
||||
WITH the root_id beeing null" do |
||||
before do |
||||
root_1 |
||||
|
||||
Issue.update_all({ :root_id => nil }, { :id => root_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_root_ids.should be_empty } |
||||
end |
||||
|
||||
describe "WITH two one issue deep trees |
||||
WITH the root_id beeing of one pointing to the other" do |
||||
before do |
||||
root_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :root_id => root_2.id }, { :id => root_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_root_ids.map(&:id).should =~ [root_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH the root_id of the child pointing to itself" do |
||||
before do |
||||
child_1_1 |
||||
|
||||
Issue.update_all({ :root_id => child_1_1.id }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_root_ids.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
describe "WITH a two issue deep tree |
||||
WITH a one issue deep tree |
||||
WITH the root_id of the child pointing to the other tree" do |
||||
before do |
||||
child_1_1 |
||||
root_2 |
||||
|
||||
Issue.update_all({ :root_id => root_2.id }, { :id => child_1_1.id }) |
||||
end |
||||
|
||||
it { Issue.invalid_root_ids.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
|
||||
|
||||
describe "WITH a three issue deep tree |
||||
WITH the root_id of the child pointing to another tree |
||||
WITH the root_id of the grandchild pointing to the same other tree" do |
||||
before do |
||||
gchild_1_1_1 |
||||
|
||||
Issue.update_all({ :root_id => 0 }, { :id => child_1_1.id }) |
||||
Issue.update_all({ :root_id => 0 }, { :id => gchild_1_1_1.id }) |
||||
end |
||||
|
||||
# As the sql statements do not work recursively |
||||
# we are currently only able to spot the child |
||||
# this is not how it should be |
||||
it { Issue.invalid_root_ids.map(&:id).should =~ [child_1_1.id] } |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue