kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
241 lines
8.7 KiB
241 lines
8.7 KiB
#-- encoding: UTF-8
|
|
#-- copyright
|
|
# OpenProject is a project management system.
|
|
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See doc/COPYRIGHT.rdoc for more details.
|
|
#++
|
|
|
|
# When included it adds the nested_set behaviour scoped by the attribute
|
|
# 'root_id'
|
|
#
|
|
# AwesomeNestedSet offers being scoped but does not handle inserting and
|
|
# updating with the scoped being set right. This module adds this.
|
|
#
|
|
# When being 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
|
|
# calling parent triggers the loading, so if we could not load one, the parent does not exist
|
|
if parent_id && !parent
|
|
errors.add :parent_id, :does_not_exist
|
|
end
|
|
# Checks parent issue assignment
|
|
if parent
|
|
if !Setting.cross_project_work_package_relations? && parent.project_id != project_id
|
|
errors.add :parent_id, :cannot_be_in_another_project
|
|
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
|
|
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
|
|
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.where(['id = ?', id])
|
|
.update_all("root_id = #{root_id}, " +
|
|
"#{quoted_left_column_name} = #{lft}, " +
|
|
"#{quoted_right_column_name} = #{rgt}")
|
|
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 = 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.where(['root_id = ? AND ' +
|
|
"#{quoted_left_column_name} >= ? AND " +
|
|
"#{quoted_right_column_name} <= ? ", old_root_id, lft, rgt])
|
|
.update_all("root_id = #{root_id}, " +
|
|
"#{quoted_left_column_name} = lft + #{offset}, " +
|
|
"#{quoted_right_column_name} = rgt + #{offset}")
|
|
|
|
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 which 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.where(["root_id = ? AND #{quoted_right_column_name} > ?", old_root_id, rgt_offset])
|
|
.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")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|