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