Merge branch 'feature/rails3' into feature/changesets_on_work_packages

Conflicts:
	app/models/issue.rb
pull/261/head
Jens Ulferts 11 years ago
commit 990794d111
  1. 8
      app/assets/javascripts/application.js
  2. 2
      app/controllers/issues_controller.rb
  3. 2
      app/helpers/work_packages_helper.rb
  4. 204
      app/models/issue.rb
  5. 2
      app/models/permitted_params.rb
  6. 85
      app/models/planning_element.rb
  7. 2
      app/models/project.rb
  8. 2
      app/models/setting.rb
  9. 187
      app/models/work_package.rb
  10. 2
      app/views/issues/_form.html.erb
  11. 2
      app/views/issues/_list.html.erb
  12. 2
      app/views/issues/_subissues_paragraph.html.erb
  13. 4
      app/views/issues/bulk_edit.html.erb
  14. 6
      app/views/planning_elements/_form.html.erb
  15. 2
      app/views/work_packages/_subwork_packages_paragraph.html.erb
  16. 7
      config/application.rb
  17. 3
      config/environments/development.rb
  18. 3
      config/environments/test.rb
  19. 1
      doc/CHANGELOG.md
  20. 10
      doc/RUNNING_TESTS.md
  21. 175
      extra/svn/reposman.rb
  22. 4
      features/step_definitions/work_package_steps.rb
  23. 164
      lib/open_project/nested_set/rebuild_patch.rb
  24. 221
      lib/open_project/nested_set/root_id_handling.rb
  25. 104
      lib/open_project/nested_set/root_id_rebuilding.rb
  26. 22
      lib/open_project/nested_set/with_root_id_scope.rb
  27. 4
      lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/creation.rb
  28. 6
      spec/controllers/work_packages_controller_spec.rb
  29. 1
      spec/models/issue_spec.rb
  30. 12
      spec/models/permitted_params_spec.rb
  31. 8
      spec/models/planning_element_spec.rb
  32. 240
      spec/models/work_package_nested_set_spec.rb
  33. 938
      spec/models/work_package_rebuild_nested_set.rb
  34. 12
      test/functional/issues_controller_test.rb
  35. 6
      test/integration/api_test/issues_test.rb
  36. 192
      test/unit/issue_nested_set_test.rb
  37. 4
      test/unit/version_test.rb

@ -374,28 +374,28 @@ function observeProjectIdentifier() {
}
function observeParentIssueField(url) {
new Ajax.Autocompleter('issue_parent_issue_id',
new Ajax.Autocompleter('issue_parent_id',
'parent_issue_candidates',
url,
{ minChars: 1,
frequency: 0.5,
paramName: 'q',
updateElement: function(value) {
document.getElementById('issue_parent_issue_id').value = value.id;
document.getElementById('issue_parent_id').value = value.id;
},
parameters: 'scope=all'
});
}
function observeWorkPackageParentField(url) {
new Ajax.Autocompleter('work_package_parent_issue_id',
new Ajax.Autocompleter('work_package_parent_id',
'parent_issue_candidates',
url,
{ minChars: 1,
frequency: 0.5,
paramName: 'q',
updateElement: function(value) {
document.getElementById('work_package_parent_issue_id').value = value.id;
document.getElementById('work_package_parent_id').value = value.id;
},
parameters: 'scope=all'
});

@ -145,7 +145,7 @@ class IssuesController < ApplicationController
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
respond_to do |format|
format.html {
redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:type_id => @issue.type, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:type_id => @issue.type, :parent_id => @issue.parent_id}.reject {|k,v| v.nil?} } :
{ :action => 'show', :id => @issue })
}
end

@ -269,7 +269,7 @@ module WorkPackagesHelper
def work_package_form_parent_attribute(form, work_package, locals = {})
if User.current.allowed_to?(:manage_subtasks, locals[:project])
field = if work_package.is_a?(Issue)
form.text_field :parent_issue_id, :size => 10, :title => l(:description_autocomplete)
form.text_field :parent_id, :size => 10, :title => l(:description_autocomplete)
else
form.text_field :parent_id, :size => 10, :title => l(:description_autocomplete)
end

@ -33,7 +33,6 @@ class Issue < WorkPackage
validate :validate_fixed_version_is_assignable
validate :validate_fixed_version_is_still_open
validate :validate_enabled_type
validate :validate_correct_parent
scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
@ -66,30 +65,8 @@ class Issue < WorkPackage
title << ')'
end
# find all issues
# * having set a parent_id where the root_id
# 1) points to self
# 2) points to an issue with a parent
# 3) points to an issue having a different root_id
# * having not set a parent_id but a root_id
# This unfortunately does not find the issue 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 => "(issues.parent_id IS NOT NULL AND " +
# "(issues.root_id = issues.id OR " +
# "(issues.root_id = parent_issues.id AND parent_issues.parent_id IS NOT NULL) OR " +
# "(issues.root_id != parent_issues.root_id))" +
# ") OR " +
# "(issues.parent_id IS NULL AND issues.root_id != issues.id)",
# :joins => "LEFT OUTER JOIN issues parent_issues ON parent_issues.id = issues.parent_id" }
before_create :default_assign
before_save :close_duplicates, :update_done_ratio_from_issue_status
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes
after_destroy :update_parent_attributes
before_destroy :remove_attachments
after_initialize :set_default_values
@ -109,7 +86,7 @@ class Issue < WorkPackage
# Moves/copies an issue to a new project and type
# Returns the moved/copied issue on success, false on failure
def move_to_project(*args)
ret = Issue.transaction do
Issue.transaction do
move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
end || false
end
@ -132,7 +109,7 @@ class Issue < WorkPackage
if !Setting.cross_project_issue_relations? &&
parent && parent.project_id != project_id
self.parent_issue_id = nil
self.parent_id = nil
end
end
if new_type
@ -206,7 +183,7 @@ class Issue < WorkPackage
safe_attributes 'type_id',
'status_id',
'parent_issue_id',
'parent_id',
'category_id',
'assigned_to_id',
'priority_id',
@ -251,15 +228,15 @@ class Issue < WorkPackage
end
end
if @parent_issue.present?
if parent.present?
attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
end
if attrs.has_key?('parent_issue_id')
if attrs.has_key?('parent_id')
if !user.allowed_to?(:manage_subtasks, project)
attrs.delete('parent_issue_id')
elsif !attrs['parent_issue_id'].blank?
attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
attrs.delete('parent_id')
elsif !attrs['parent_id'].blank?
attrs.delete('parent_id') unless WorkPackage.visible(user).exists?(attrs['parent_id'].to_i)
end
end
@ -322,24 +299,6 @@ class Issue < WorkPackage
end
end
def validate_correct_parent
# Checks parent issue assignment
if @parent_issue
if !Setting.cross_project_issue_relations? && @parent_issue.project_id != self.project_id
errors.add :parent_issue_id, :not_a_valid_parent
elsif !new_record?
# moving an existing issue
if @parent_issue.root_id != root_id
# we can always move to another tree
elsif move_possible?(@parent_issue)
# move accepted inside tree
else
errors.add :parent_issue_id, :not_a_valid_parent
end
end
end
end
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios
# even if the user turns off the setting later
def update_done_ratio_from_issue_status
@ -379,12 +338,6 @@ class Issue < WorkPackage
return done_date <= Date.today
end
# Does this issue have children?
def children?
!leaf?
end
# Returns an array of status that user is able to apply
def new_statuses_allowed_to(user, include_default=false)
return [] if status.nil?
@ -447,23 +400,6 @@ class Issue < WorkPackage
end
end
# 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
# TODO: remove. This is left here to avoid regression
# but the code was duplicated to work_packages_helper
# and thus should be removed as soon as possible.
# Returns a string of css classes that apply to the issue
def css_classes
s = "issue status-#{status.position} priority-#{priority.position}"
@ -536,26 +472,6 @@ class Issue < WorkPackage
Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
end
def parent_issue_id=(arg)
parent_issue_id = arg.blank? ? nil : arg.to_i
if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
journal_changes["parent_id"] = [self.parent_id, @parent_issue.id]
@parent_issue.id
else
@parent_issue = nil
journal_changes["parent_id"] = [self.parent_id, nil]
nil
end
end
def parent_issue_id
if instance_variable_defined? :@parent_issue
@parent_issue.nil? ? nil : @parent_issue.id
else
parent_id
end
end
# Extracted from the ReportsController.
def self.by_type(project)
count_and_group_by(:project => project,
@ -623,110 +539,9 @@ class Issue < WorkPackage
projects
end
# method from acts_as_nested_set
def self.valid?
super && invalid_root_ids.empty?
end
def self.all_invalid
(super + invalid_root_ids).uniq
end
def self.rebuild_silently!(roots = nil)
invalid_root_ids_to_fix = if roots.is_a? Array
roots
elsif roots.present?
[roots]
else
[]
end
known_issue_parents = Hash.new do |hash, ancestor_id|
hash[ancestor_id] = Issue.find_by_id(ancestor_id)
end
fix_known_invalid_root_ids = lambda do
issues = invalid_root_ids
issues_roots = []
issues.each do |issue|
# At this point we can not trust nested set methods as the root_id is invalid.
# Therefore we trust the parent_issue_id to fetch all ancestors until we find the root
ancestor = issue
while ancestor.parent_issue_id do
ancestor = known_issue_parents[ancestor.parent_issue_id]
end
issues_roots << ancestor
if invalid_root_ids_to_fix.empty? || invalid_root_ids_to_fix.map(&:id).include?(ancestor.id)
Issue.update_all({ :root_id => ancestor.id },
{ :id => issue.id })
end
end
fix_known_invalid_root_ids.call unless (issues_roots.map(&:id) & invalid_root_ids_to_fix.map(&:id)).empty?
end
fix_known_invalid_root_ids.call
super
end
private
def update_nested_set_attributes
if root_id.nil?
# issue was just created
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
set_default_left_and_right
Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
if @parent_issue
move_to_child_of(@parent_issue)
end
reload
elsif parent_issue_id != parent_id
former_parent_id = parent_id
# moving an existing issue
if @parent_issue && @parent_issue.root_id == root_id
# inside the same tree
move_to_child_of(@parent_issue)
else
# to another tree
unless root?
move_to_right_of(root)
reload
end
old_root_id = root_id
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
target_maxright = nested_set_scope.maximum(right_column_name) || 0
offset = target_maxright + 1 - lft
Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
self[left_column_name] = lft + offset
self[right_column_name] = rgt + offset
if @parent_issue
move_to_child_of(@parent_issue)
end
end
reload
# delete invalid relations of all descendants
self_and_descendants.each do |issue|
issue.relations.each do |relation|
relation.destroy unless relation.valid?
end
end
# update former parent
recalculate_attributes_for(former_parent_id) if former_parent_id
end
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
end
# this removes all attachments separately before destroying the issue
# avoids getting a ActiveRecord::StaleObjectError when deleting an issue
def remove_attachments
@ -735,9 +550,6 @@ class Issue < WorkPackage
reload # important
end
def update_parent_attributes
recalculate_attributes_for(parent_id) if parent_id
end
# Update issues so their versions are not pointing to a
# fixed_version that is not shared with the issue's project

@ -147,7 +147,7 @@ class PermittedParams < Struct.new(:params, :user)
:due_date,
:planning_element_type_id,
:parent_id,
:parent_issue_id,
:parent_id,
:assigned_to_id,
:responsible_id,
:type_id,

@ -61,9 +61,6 @@ class PlanningElement < WorkPackage
}
scope :visible, lambda {|*args| { :include => :project,
:conditions => PlanningElement.visible_condition(args.first || User.current) } }
scope :at_time, lambda { |time|
{:select => SQL_FOR_AT[:select],
:conditions => ["(#{PlanningElement.quoted_table_name}.deleted_at IS NULL
@ -119,9 +116,6 @@ class PlanningElement < WorkPackage
before_save :append_scenario_dates_to_journal
after_save :update_parent_attributes
after_save :create_alternate_date
validates_presence_of :subject, :project
validates_length_of :subject, :maximum => 255, :unless => lambda { |e| e.subject.blank? }
@ -134,10 +128,6 @@ class PlanningElement < WorkPackage
end
end
def is_milestone?
planning_element_type && planning_element_type.is_milestone?
end
validate do
if self.due_date and self.start_date and self.due_date < self.start_date
errors.add :due_date, :greater_than_start_date
@ -159,10 +149,6 @@ class PlanningElement < WorkPackage
end
def leaf?
self.children.count == 0
end
def all_scenarios
project.scenarios.sort_by(&:id).map do |scenario|
alternate_date = alternate_dates.to_a.find { |a| a.scenario_id.to_s == scenario.id.to_s }
@ -216,75 +202,4 @@ class PlanningElement < WorkPackage
end
end
end
def note
@journal_notes
end
def note=(text)
@journal_notes = text
end
def trash
unless new_record? or self.deleted_at
self.children.each{|child| child.trash}
self.reload
self.deleted_at = Time.now
self.save!
end
freeze
end
def restore!
unless parent && parent.deleted?
self.deleted_at = nil
self.save
else
raise "You cannot restore an element whose parent is deleted. Restore the parent first!"
end
end
def deleted?
!!read_attribute(:deleted_at)
end
# Aliasing the parent_issue_id methods here in order
# to improve compatibility between
# planning elments and issues
alias_method :parent_issue_id, :parent_id
# I am not sure why it is not possible to
# alias_method :parent_issue_id=, :parent_id=
def parent_issue_id=(arg)
parent_id = arg
end
protected
def update_parent_attributes
if parent.present?
parent.reload
unless parent.children.without_deleted.empty?
children = parent.children.without_deleted
parent.start_date = [children.minimum(:start_date), children.minimum(:due_date)].reject(&:nil?).min
parent.due_date = [children.maximum(:start_date), children.maximum(:due_date)].reject(&:nil?).max
if parent.changes.present?
parent.note = I18n.t('timelines.planning_element_updated_automatically_by_child_changes', :child => "*#{id}")
# Ancestors will be updated by parent's after_save hook.
parent.save(:validate => false)
end
end
end
end
def create_alternate_date
if start_date_changed? or due_date_changed?
alternate_dates.create(:start_date => start_date, :due_date => due_date)
end
end
end

@ -976,7 +976,7 @@ class Project < ActiveRecord::Base
# Parent issue
if issue.parent_id
if copied_parent = work_packages_map[issue.parent_id]
new_issue.parent_issue_id = copied_parent.id
new_issue.parent_id = copied_parent.id
end
end

@ -129,7 +129,7 @@ class Setting < ActiveRecord::Base
def self.[]=(name, v)
setting = find_or_default(name)
setting.value = (v ? v : "")
Rails.cache.delete_matched(Regexp.compile(base_cache_key(name, '.+')))
Rails.cache.delete(cache_key(name))
setting.save
setting.value
end

@ -53,7 +53,17 @@ class WorkPackage < ActiveRecord::Base
acts_as_watchable
acts_as_nested_set :scope => 'root_id', :dependent => :destroy
before_save :store_former_parent_id
include OpenProject::NestedSet::WithRootIdScope
after_save :reschedule_following_issues,
:update_parent_attributes,
:create_alternate_date
after_move :remove_invalid_relations,
:recalculate_attributes_for_former_parent
after_destroy :update_parent_attributes
acts_as_customizable
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
@ -111,6 +121,17 @@ class WorkPackage < ActiveRecord::Base
:responsible_id
register_on_journal_formatter :scenario_date, /^scenario_(\d+)_(start|due)_date$/
# acts_as_journalized will create an initial journal on wp creation
# and touch the journaled object:
# journal.rb:47
#
# This will result in optimistic locking increasing the lock_version attribute to 1.
# In order to avoid stale object errors we reload the attributes in question
# after the wp is created.
# As after_create is run before after_save, and journal creation is triggered by an
# after_save hook, we rely on after_save and a specific version, here.
after_save :reload_lock_and_timestamps, :if => Proc.new { |wp| wp.lock_version == 0 }
# Returns a SQL conditions string used to find all work units visible by the specified user
def self.visible_condition(user, options={})
Project.allowed_to_condition(user, :view_work_packages, options)
@ -153,7 +174,7 @@ class WorkPackage < ActiveRecord::Base
# attributes don't come from form, so it's save to force assign
self.force_attributes = work_package.attributes.dup.except(*merged_options[:exclude])
self.parent_issue_id = work_package.parent_id if work_package.parent_id
self.parent_id = work_package.parent_id if work_package.parent_id
self.custom_field_values = work_package.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
self.status = work_package.status
self
@ -243,6 +264,30 @@ class WorkPackage < ActiveRecord::Base
end
end
def trash
unless new_record? or self.deleted_at
self.children.each{|child| child.trash}
self.reload
self.deleted_at = Time.now
self.save!
end
freeze
end
def restore!
unless parent && parent.deleted?
self.deleted_at = nil
self.save
else
raise "You cannot restore an element whose parent is deleted. Restore the parent first!"
end
end
def deleted?
!!read_attribute(:deleted_at)
end
# Users the work_package can be assigned to
delegate :assignable_users, :to => :project
@ -290,68 +335,122 @@ class WorkPackage < ActiveRecord::Base
end
end
def is_milestone?
planning_element_type && planning_element_type.is_milestone?
end
# This is a dummy implementation that is currently overwritten
# by issue
# Adapt once tracker/type is migrated
def new_statuses_allowed_to(user, include_default = false)
IssueStatus.all
end
def self.use_status_for_done_ratio?
Setting.issue_done_ratio == 'issue_status'
end
# Returns the total number of hours spent on this issue and its descendants
#
# Example:
# spent_hours => 0.0
# spent_hours => 50.2
def spent_hours
@spent_hours ||= self_and_descendants.joins(:time_entries)
.sum("#{TimeEntry.table_name}.hours").to_f || 0.0
end
protected
def recalculate_attributes_for(work_package_id)
if work_package_id.is_a? WorkPackage
p = work_package_id
p = if work_package_id.is_a? WorkPackage
work_package_id
else
p = WorkPackage.find_by_id(work_package_id)
WorkPackage.find_by_id(work_package_id)
end
if p
return unless p
p.inherit_priority_from_children
p.inherit_dates_from_children
p.inherit_done_ratio_from_leaves
p.inherit_estimated_hours_from_leaves
# ancestors will be recursively updated
if p.changed?
p.journal_notes = I18n.t('timelines.planning_element_updated_automatically_by_child_changes', :child => "*#{id}")
# Ancestors will be updated by parent's after_save hook.
p.save(:validate => false)
end
end
def update_parent_attributes
recalculate_attributes_for(parent_id) if parent_id.present?
end
def inherit_priority_from_children
# priority = highest priority of children
if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
p.priority = IssuePriority.find_by_position(priority_position)
if priority_position = children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
self.priority = IssuePriority.find_by_position(priority_position)
end
end
def inherit_dates_from_children
active_children = children.without_deleted
# start/due dates = lowest/highest dates of children
p.start_date = p.children.minimum(:start_date)
p.due_date = p.children.maximum(:due_date)
if p.start_date && p.due_date && p.due_date < p.start_date
p.start_date, p.due_date = p.due_date, p.start_date
unless active_children.empty?
self.start_date = [active_children.minimum(:start_date), active_children.minimum(:due_date)].compact.min
self.due_date = [active_children.maximum(:start_date), active_children.maximum(:due_date)].compact.max
end
end
def inherit_done_ratio_from_leaves
# done ratio = weighted average ratio of leaves
unless WorkPackage.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
leaves_count = p.leaves.count
unless WorkPackage.use_status_for_done_ratio? && status && status.default_done_ratio
leaves_count = leaves.count
if leaves_count > 0
average = p.leaves.average(:estimated_hours).to_f
average = leaves.average(:estimated_hours).to_f
if average == 0
average = 1
end
done = p.leaves.joins(:status).sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
done = leaves.joins(:status).sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
progress = done / (average * leaves_count)
p.done_ratio = progress.round
self.done_ratio = progress.round
end
end
end
def inherit_estimated_hours_from_leaves
# estimate = sum of leaves estimates
p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
p.estimated_hours = nil if p.estimated_hours == 0.0
# ancestors will be recursively updated
p.save(:validate => false) if p.changed?
self.estimated_hours = leaves.sum(:estimated_hours).to_f
self.estimated_hours = nil if estimated_hours == 0.0
end
def store_former_parent_id
@former_parent_id = parent_id_changed? ? parent_id_was : false
true # force callback to return true
end
# This is a dummy implementation that is currently overwritten
# by issue
# Adapt once tracker/type is migrated
def new_statuses_allowed_to(user, include_default = false)
IssueStatus.all
def remove_invalid_relations
# delete invalid relations of all descendants
self_and_descendants.each do |issue|
issue.relations.each do |relation|
relation.destroy unless relation.valid?
end
end
end
def self.use_status_for_done_ratio?
Setting.issue_done_ratio == 'issue_status'
def recalculate_attributes_for_former_parent
recalculate_attributes_for(@former_parent_id) if @former_parent_id
end
# Returns the total number of hours spent on this issue and its descendants
#
# Example:
# spent_hours => 0.0
# spent_hours => 50.2
def spent_hours
@spent_hours ||= self_and_descendants.joins(:time_entries)
.sum("#{TimeEntry.table_name}.hours").to_f || 0.0
def reload_lock_and_timestamps
reload(:select => [:lock_version, :created_at, :updated_at])
end
private
@ -364,4 +463,16 @@ class WorkPackage < ActiveRecord::Base
time_entries.build(attributes)
end
def create_alternate_date
# This is a hack.
# It is required as long as alternate dates exist/are not moved up to work_packages.
# Its purpose is to allow for setting the after_save filter in the correct order
# before acts as journalized and the cleanup method reload_lock_and_timestamps.
return true unless respond_to?(:alternate_dates)
if start_date_changed? or due_date_changed?
alternate_dates.create(:start_date => start_date, :due_date => due_date)
end
end
end

@ -22,7 +22,7 @@ See doc/COPYRIGHT.rdoc for more details.
<p><%= f.text_field :subject, :size => 80, :required => true %></p>
<% if User.current.allowed_to?(:manage_subtasks, @project) %>
<p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :title => l(:description_autocomplete) %></p>
<p id="parent_issue"><%= f.text_field :parent_id, :size => 10, :title => l(:description_autocomplete) %></p>
<div id="parent_issue_candidates" class="autocomplete"></div>
<%= javascript_tag "observeParentIssueField('#{issues_auto_complete_path(:id => @issue, :project_id => @project, :escape => false) }')" %>
<% end %>

@ -45,7 +45,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= check_box_tag("ids[]", issue.id, false, :id => "issue#{issue.id}") %>
</td>
<td class="id">
<% if parent_issue = issue.parent_issue_id %>
<% if parent_issue = issue.parent_id %>
<span class='hidden-for-sighted'><%=l(:description_subissue) + ' ' + l(:label_issue) + ' #' + parent_issue.to_s %></span>
<% end -%>
<%= link_to issue.id, :controller => '/issues', :action => 'show', :id => issue %>

@ -11,7 +11,7 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<strong><%= l(:label_issue_hierarchy) %></strong>
(<%= link_to(l(:label_add_subtask), {:controller => '/issues', :action => 'new', :project_id => @project, :issue => {:parent_issue_id => @issue}}) if User.current.allowed_to?(:manage_subtasks, @project) %>)
(<%= link_to(l(:label_add_subtask), {:controller => '/issues', :action => 'new', :project_id => @project, :issue => {:parent_id => @issue}}) if User.current.allowed_to?(:manage_subtasks, @project) %>)
<% if !@issue.leaf? || @issue.parent %>
<% indent = 0 %>

@ -72,8 +72,8 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="splitcontentright">
<% if @project && User.current.allowed_to?(:manage_subtasks, @project) %>
<p>
<label for='issue_parent_issue_id'><%= Issue.human_attribute_name(:parent_issue) %></label>
<%= text_field_tag 'issue[parent_issue_id]', '', :size => 10 %>
<label for='issue_parent_id'><%= Issue.human_attribute_name(:parent_issue) %></label>
<%= text_field_tag 'issue[parent_id]', '', :size => 10 %>
</p>
<div id="parent_issue_candidates" class="autocomplete"></div>
<%= javascript_tag "observeParentIssueField('#{issues_auto_complete_path }')" %>

@ -146,11 +146,11 @@ See doc/COPYRIGHT.rdoc for more details.
<% unless planning_element.new_record? %>
<h3><%= l(:field_notes) %></h3>
<label for="planning_element_note" class="hidden-for-sighted">
<label for="planning_element_journal_notes" class="hidden-for-sighted">
<%= l(:field_notes) %>
</label>
<%= f.text_area 'note', :cols => 60, :rows => 3, :class => 'wiki-edit' %>
<%= wikitoolbar_for 'planning_element_note' %>
<%= f.text_area 'journal_notes', :cols => 60, :rows => 3, :class => 'wiki-edit' %>
<%= wikitoolbar_for 'planning_element_journal_notes' %>
<% end %>
<%= submit_tag l(:button_save) %>

@ -15,7 +15,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% if controller.work_package.is_a? Issue %>
(<%= link_to(l(:label_add_subtask), new_project_work_package_path(:project_id => @project,
:sti_type => Issue.to_s,
:work_package => { :parent_issue_id => work_package })) %>)
:work_package => { :parent_id => work_package })) %>)
<% elsif controller.work_package.is_a? PlanningElement %>
(<%= link_to(l(:label_add_subtask), new_project_work_package_path(:project_id => @project,
:sti_type => PlanningElement.to_s,

@ -86,12 +86,11 @@ module OpenProject
# like if you have constraints or database-specific column types
# config.active_record.schema_format = :sql
# needed?
# Load any local configuration that is kept out of source control
# (e.g. patches).
#if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
# instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
#end
if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
end
# Enforce whitelist mode for mass assignment.
# This will create an empty whitelist of attributes available for mass-assignment for all models

@ -48,4 +48,7 @@ OpenProject::Application.configure do
# Send mails to browser window
config.action_mailer.delivery_method = :letter_opener
# we use per process memory for caching in development
config.cache_store = :memory_store
end

@ -48,4 +48,7 @@ OpenProject::Application.configure do
# Print deprecation notices to the stderr
config.active_support.deprecation = :stderr
# we use per process memory for caching in the test environment
config.cache_store = :memory_store
end

@ -1,5 +1,6 @@
# Changelog
* `#1520` PlanningElements are created without the root_id attribute being set
* `#1246` Implement uniform "edit" action/view for pe & issues
* `#1247` Implement uniform "update" action for pe & issues

@ -63,6 +63,16 @@ You can run the specs with the following commands:
TODO: how to run plugins specs.
## Test Unit
You can run a single test with the following command:
* ``rake test:units TEST=path/to/test.rb TESTOPTS="--name=test_name_of_test_to_run"``
You let test unit display test names instead of anonymous dots with the following command:
* ``rake test:units TESTOPTS="--verbose=verbose"``
## For the fancy programmer
* We are testing on travis-ci. Look there for your pull requests.<br />

@ -11,102 +11,13 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
# == Synopsis
#
# reposman: manages your repositories with Redmine
#
# == Usage
#
# reposman [OPTIONS...] -s [DIR] -r [HOST]
#
# Examples:
# reposman --svn-dir=/var/svn --redmine-host=redmine.example.net --scm subversion
# reposman -s /var/git -r redmine.example.net -u http://svn.example.net --scm git
#
# == Arguments (mandatory)
#
# -s, --svn-dir=DIR use DIR as base directory for svn repositories
# -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples:
# -r redmine.example.net
# -r http://redmine.example.net
# -r https://example.net/redmine
# -k, --key=KEY use KEY as the Redmine API key
#
# == Options
#
# -o, --owner=OWNER owner of the repository. using the rails login
# allow user to browse the repository within
# Redmine even for private project. If you want to
# share repositories through Redmine.pm, you need
# to use the apache owner.
# -g, --group=GROUP group of the repository. (default: root)
# --public-mode=MODE file mode for new public repositories
# (default: 0775)
# --private-mode=MODE file mode for new private repositories
# (default: 0770)
# --scm=SCM the kind of SCM repository you want to create (and
# register) in Redmine (default: Subversion).
# reposman is able to create Git and Subversion
# repositories. For all other kind, you must specify
# a --command option
# -u, --url=URL the base url Redmine will use to access your
# repositories. This option is used to automatically
# register the repositories in Redmine. The project
# identifier will be appended to this url. Examples:
# -u https://example.net/svn
# -u file:///var/svn/
# if this option isn't set, reposman won't register
# the repositories in Redmine
# -c, --command=COMMAND use this command instead of "svnadmin create" to
# create a repository. This option can be used to
# create repositories other than subversion and git
# kind.
# This command override the default creation for git
# and subversion.
# -f, --force force repository creation even if the project
# repository is already declared in Redmine
# -t, --test only show what should be done
# -h, --help show help and exit
# -v, --verbose verbose
# -V, --version print version and exit
# -q, --quiet no log
#
# == References
#
# You can find more information on the redmine's wiki : http://www.redmine.org/wiki/redmine/HowTos
require 'getoptlong'
require 'rdoc/usage'
require 'optparse'
require 'find'
require 'etc'
# working around deprecation in RubyGems 1.6
# needed for rails <2.3.9 only, don't merge to unstable!
require 'thread'
Version = "1.3"
SUPPORTED_SCM = %w( Subversion Git Filesystem )
opts = GetoptLong.new(
['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT],
['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT],
['--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT],
['--group', '-g', GetoptLong::REQUIRED_ARGUMENT],
['--url', '-u', GetoptLong::REQUIRED_ARGUMENT],
['--public-mode', GetoptLong::REQUIRED_ARGUMENT],
['--private-mode', GetoptLong::REQUIRED_ARGUMENT],
['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT],
['--scm', GetoptLong::REQUIRED_ARGUMENT],
['--test', '-t', GetoptLong::NO_ARGUMENT],
['--force', '-f', GetoptLong::NO_ARGUMENT],
['--verbose', '-v', GetoptLong::NO_ARGUMENT],
['--version', '-V', GetoptLong::NO_ARGUMENT],
['--help' , '-h', GetoptLong::NO_ARGUMENT],
['--quiet' , '-q', GetoptLong::NO_ARGUMENT]
)
$verbose = 0
$quiet = false
$redmine_host = ''
@ -151,30 +62,63 @@ module SCM
end
begin
opts.each do |opt, arg|
case opt
when '--svn-dir'; $repos_base = arg.dup
when '--redmine-host'; $redmine_host = arg.dup
when '--key'; $api_key = arg.dup
when '--owner'; $svn_owner = arg.dup; $use_groupid = false;
when '--group'; $svn_group = arg.dup; $use_groupid = false;
when '--public-mode'; $public_mode = arg.dup;
when '--private-mode'; $private_mode = arg.dup;
when '--url'; $svn_url = arg.dup
when '--scm'; $scm = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm)
when '--command'; $command = arg.dup
when '--verbose'; $verbose += 1
when '--test'; $test = true
when '--force'; $force = true
when '--version'; puts Version; exit
when '--help'; RDoc::usage
when '--quiet'; $quiet = true
end
end
rescue
exit 1
end
OptionParser.new do |opts|
opts.banner = "Usage: reposman.rb [OPTIONS...] -s [DIR] -r [HOST]"
opts.separator("")
opts.separator("Manages your repositories with OpenProject.")
opts.separator("")
opts.separator("Required arguments:")
opts.on("-s", "--svn-dir DIR", "use DIR as base directory for svn repositories") {|v| $repos_base = v}
opts.on("-r", "--redmine-host HOST", "assume Redmine is hosted on HOST. Examples:",
" -r redmine.example.net",
" -r http://redmine.example.net",
" -r https://redmine.example.net") {|v| $redmine_host = v}
opts.on("-k", "--key KEY", "use KEY as the Redmine API key") {|v| $api_key = v}
opts.separator("")
opts.separator("Options:")
opts.on("-o", "--owner OWNER", "owner of the repository. using the rails login",
"allows users to browse the repository within",
"Redmine even for private projects. If you want to",
"share repositories through Redmine.pm, you need",
"to use the apache owner.") {|v| $svn_owner = v; $use_groupid = false}
opts.on("-g", "--group GROUP", "group of the repository (default: root)") {|v| $svn_group = v; $use_groupid = false}
opts.on( "--public-mode MODE", "file mode for new public repositories (default: 0775)") {|v| $public_mode = v}
opts.on( "--private-mode MODE", "file mode for new private repositories (default: 0770)") {|v| $public_mode = v}
opts.on( "--scm SCM", "the kind of SCM repository you want to create",
"(and register) in Redmine (default: Subversion).",
"reposman is able to create Git and Subversion",
"repositories.",
"For all other kind, you must specify a --command",
"option") {|v| v.capitalize; log("Invalid SCM: #{v}", :exit => true) unless SUPPORTED_SCM.include?(v)}
opts.on("-u", "--url URL", "the base url Redmine will use to access your",
"repositories. This option is used to automatically",
"register the repositories in Redmine. The project ",
"identifier will be appended to this url.",
"Examples:",
" -u https://example.net/svn",
" -u file:///var/svn/",
"if this option isn't set, reposman won't register",
"the repositories in Redmine") {|v| $svn_url = v}
opts.on("-c", "--command COMMAND", "use this command instead of 'svnadmin create' to",
"create a repository. This option can be used to",
"create repositories other than subversion and git",
"kind.",
"This command override the default creation for git",
"and subversion.") {|v| $command = v}
opts.on("-f", "--force", "force repository creation even if the project",
"repository is already declared in Redmine") {$force = true}
opts.on("-t", "--test", "only show what should be done") {$test = true}
opts.on("-h", "--help", "show help and exit") {puts opts; exit 1}
opts.on("-v", "--verbose", "verbose") {$verbose += 1}
opts.on("-V", "--version", "print version and exit") {puts Version; exit}
opts.on("-q", "--quiet", "no log") {$quiet = true}
opts.separator("")
opts.separator("Examples:")
opts.separator(" reposman.rb --svn-dir=/var/svn --redmine-host=redmine.example.net --scm Subversion")
opts.separator(" reposman.rb -s /var/git -r redmine.example.net -u http://svn.example.net --scm Git")
opts.separator("")
opts.separator("You can find more information on the redmine's wiki:\nhttp://www.redmine.org/projects/redmine/wiki/HowTos")
end.parse!
if $test
log("running in test mode")
@ -192,7 +136,8 @@ end
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
if ($redmine_host.empty? or $repos_base.empty?)
RDoc::usage
puts "Required argument missing. Type 'reposman.rb --help' for usage."
exit 1
end
unless File.directory?($repos_base)

@ -24,11 +24,7 @@ Given /^the work package "(.*?)" has the following children:$/ do |work_package_
table.raw.flatten.each do |child_subject|
child = WorkPackage.find_by_subject(child_subject)
if child.is_a? Issue
child.parent_issue_id = parent.id
elsif child.is_a? PlanningElement
child.parent_id = parent.id
end
child.save
end

@ -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

@ -110,10 +110,6 @@ module Redmine::Acts::Journalized
fill_object = self.class.new
# The parent id is set via awesome_nested set
# we need to use the correct accessor which is 'parent_issue_id' for issues
initial_changes["parent_issue_id"] = initial_changes.delete("parent_id") if self.class == Issue and initial_changes.has_key?("parent_id")
# Force the gathered attributes onto the fill object
# FIX ME: why not just call the method directly on fill_object?
attributes_setter = ActiveRecord::Base.instance_method(:assign_attributes)

@ -720,13 +720,13 @@ describe WorkPackagesController do
describe :ancestors do
let(:project) { FactoryGirl.create(:project_with_types) }
let(:ancestor_issue) { FactoryGirl.create(:issue, :project => project) }
let(:issue) { FactoryGirl.create(:issue, :project => project, :parent_issue_id => ancestor_issue.id) }
let(:issue) { FactoryGirl.create(:issue, :project => project, :parent_id => ancestor_issue.id) }
become_member_with_view_planning_element_permissions
describe "when work_package is an issue" do
let(:ancestor_issue) { FactoryGirl.create(:issue, :project => project) }
let(:issue) { FactoryGirl.create(:issue, :project => project, :parent_issue_id => ancestor_issue.id) }
let(:issue) { FactoryGirl.create(:issue, :project => project, :parent_id => ancestor_issue.id) }
it "should return the work_packages ancestors" do
controller.stub!(:work_package).and_return(issue)
@ -737,7 +737,7 @@ describe WorkPackagesController do
describe "when work_package is a planning element" do
let(:descendant_planning_element) { FactoryGirl.create(:planning_element, :project => project,
:parent => planning_element) }
:parent_id => planning_element.id) }
it "should return the work_packages ancestors" do
controller.stub!(:work_package).and_return(descendant_planning_element)

@ -81,7 +81,6 @@ describe Issue do
@issue.status = @status_rejected
@issue.priority = @priority_low
@issue.estimated_hours = 3
@issue.remaining_hours = 43 if Redmine::Plugin.all.collect(&:id).include?(:backlogs)
@issue.save!
initial_journal = @issue.journals.first

@ -330,7 +330,7 @@ describe PermittedParams do
PermittedParams.new(params, user).new_work_package.should == hash
end
it "should permit parent_issue_id" do
it "should permit parent_id" do
hash = { "parent_id" => "1" }
params = ActionController::Parameters.new(:work_package => hash)
@ -338,8 +338,8 @@ describe PermittedParams do
PermittedParams.new(params, user).new_work_package.should == hash
end
it "should permit parent_issue_id" do
hash = { "parent_issue_id" => "1" }
it "should permit parent_id" do
hash = { "parent_id" => "1" }
params = ActionController::Parameters.new(:work_package => hash)
@ -484,7 +484,7 @@ describe PermittedParams do
PermittedParams.new(params, user).update_work_package.should == hash
end
it "should permit parent_issue_id" do
it "should permit parent_id" do
hash = { "parent_id" => "1" }
params = ActionController::Parameters.new(:work_package => hash)
@ -492,8 +492,8 @@ describe PermittedParams do
PermittedParams.new(params, user).update_work_package.should == hash
end
it "should permit parent_issue_id" do
hash = { "parent_issue_id" => "1" }
it "should permit parent_id" do
hash = { "parent_id" => "1" }
params = ActionController::Parameters.new(:work_package => hash)

@ -642,7 +642,7 @@ describe PlanningElement do
let(:journal_scenario) { FactoryGirl.create(:scenario) }
# PlanningElements create a alternate_date on save by default, so just use that
it "should create a journal entry if a scenario gets assigned to an alternate date" do
alternate_date = journal_planning_element.alternate_dates.first.tap do |ad|
alternate_date = journal_planning_element.alternate_dates(true).first.tap do |ad|
ad.scenario = journal_scenario
end
journal_planning_element.save!
@ -662,7 +662,7 @@ describe PlanningElement do
it "should create a journal entry if a scenario gets removed from an alternate date" do
# PlanningElements create a alternate_date on save by default, so just use that
alternate_date = journal_planning_element.alternate_dates.first.tap do |ad|
alternate_date = journal_planning_element.alternate_dates(true).first.tap do |ad|
ad.scenario = journal_scenario
end
journal_planning_element.save!
@ -682,8 +682,8 @@ describe PlanningElement do
end
it "should create seperate journal entries for start_date and due_date if only one of 'em is modified" do
# PlanningElements create a alternate_date on save by default, so just use that
alternate_date = journal_planning_element.alternate_dates.first.tap do |ad|
# PlanningElements create an alternate_date on save by default, so just use that
alternate_date = journal_planning_element.alternate_dates(true).first.tap do |ad|
ad.start_date = Date.today
ad.due_date = Date.today + 1.month
ad.scenario = journal_scenario

@ -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

@ -521,7 +521,7 @@ class IssuesControllerTest < ActionController::TestCase
post :create, :project_id => 1,
:issue => {:type_id => 1,
:subject => 'This is a child issue',
:parent_issue_id => 2}
:parent_id => 2}
end
issue = Issue.find_by_subject('This is a child issue')
assert_not_nil issue
@ -535,7 +535,7 @@ class IssuesControllerTest < ActionController::TestCase
post :create, :project_id => 1,
:issue => {:type_id => 1,
:subject => 'This is a child issue',
:parent_issue_id => 'ABC'}
:parent_id => 'ABC'}
end
issue = Issue.find_by_subject('This is a child issue')
assert_not_nil issue
@ -1088,7 +1088,7 @@ class IssuesControllerTest < ActionController::TestCase
assert_response :success
assert_template 'bulk_edit'
assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
assert_tag :input, :attributes => {:name => 'issue[parent_id]'}
# Project specific custom field, date type
field = CustomField.find(9)
@ -1108,7 +1108,7 @@ class IssuesControllerTest < ActionController::TestCase
assert_template 'bulk_edit'
# Can not set issues from different projects as children of an issue
assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
assert_no_tag :input, :attributes => {:name => 'issue[parent_id]'}
# Project specific custom field, date type
field = CustomField.find(9)
@ -1223,7 +1223,7 @@ class IssuesControllerTest < ActionController::TestCase
@request.session[:user_id] = 2
put :bulk_update, :ids => [1, 3],
:notes => 'Bulk editing parent',
:issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
:issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_id => '2'}
assert_response 302
parent = Issue.find(2)
@ -1341,7 +1341,7 @@ class IssuesControllerTest < ActionController::TestCase
def test_destroy_parent_and_child_issues
parent = Issue.generate!(:project_id => 1, :type_id => 1)
child = Issue.generate!(:project_id => 1, :type_id => 1, :parent_issue_id => parent.id)
child = Issue.generate!(:project_id => 1, :type_id => 1, :parent_id => parent.id)
assert child.is_descendant_of?(parent.reload)
@request.session[:user_id] = 2

@ -179,9 +179,9 @@ class ApiTest::IssuesTest < ActionDispatch::IntegrationTest
context "with subtasks" do
setup do
@c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :type_id => 1, :project_id => 1, :parent_issue_id => 1)
@c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :type_id => 1, :project_id => 1, :parent_issue_id => 1)
@c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :type_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
@c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :type_id => 1, :project_id => 1, :parent_id => 1)
@c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :type_id => 1, :project_id => 1, :parent_id => 1)
@c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :type_id => 1, :project_id => 1, :parent_id => @c1.id)
end
context ".xml" do

@ -23,28 +23,6 @@ class IssueNestedSetTest < ActiveSupport::TestCase
Issue.delete_all
end
def test_create_root_issue
issue1 = create_issue!
issue2 = create_issue!
issue1.reload
issue2.reload
assert_equal issue1.id, issue1.root_id
assert issue1.leaf?
assert_equal issue2.id, issue2.root_id
assert issue2.leaf?
end
def test_create_child_issue
parent = create_issue!
child = create_issue!(:parent_issue_id => parent.id)
parent.reload
child.reload
assert_equal [parent.id, nil, 3], [parent.root_id, parent.parent_id, parent.rgt - parent.lft]
assert_equal [parent.id, parent.id, 1], [child.root_id, child.parent_id, child.rgt - child.lft]
end
def test_creating_a_child_in_different_project_should_not_validate_unless_allowed
Setting.cross_project_issue_relations = "0"
issue = create_issue!
@ -53,10 +31,10 @@ class IssueNestedSetTest < ActiveSupport::TestCase
:type_id => 1,
:author_id => 1,
:subject => 'child',
:parent_issue_id => issue.id }
:parent_id => issue.id }
end
assert !child.save
refute_empty child.errors[:parent_issue_id]
refute_empty child.errors[:parent_id]
end
def test_creating_a_child_in_different_project_should_validate_if_allowed
@ -67,95 +45,19 @@ class IssueNestedSetTest < ActiveSupport::TestCase
:type_id => 1,
:author_id => 1,
:subject => 'child',
:parent_issue_id => issue.id }
:parent_id => issue.id }
end
assert child.save
assert_empty child.errors[:parent_issue_id]
end
def test_move_a_root_to_child
parent1 = create_issue!
parent2 = create_issue!
child = create_issue!(:parent_issue_id => parent1.id)
parent2.parent_issue_id = parent1.id
parent2.save!
child.reload
parent1.reload
parent2.reload
assert_equal [parent1.id, 5], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent1.id, 1], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent1.id, 1], [child.root_id, child.nested_set_span]
end
def test_move_a_child_to_root
parent1 = create_issue!
parent2 = create_issue!
child = create_issue!(:parent_issue_id => parent1.id)
child.parent_issue_id = nil
child.save!
child.reload
parent1.reload
parent2.reload
assert_equal [parent1.id, 1], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 1], [parent2.root_id, parent2.nested_set_span]
assert_equal [child.id, 1], [child.root_id, child.nested_set_span]
assert_empty child.errors[:parent_id]
end
def test_move_a_child_to_another_issue
parent1 = create_issue!
parent2 = create_issue!
child = create_issue!(:parent_issue_id => parent1.id)
child.parent_issue_id = parent2.id
child.save!
child.reload
parent1.reload
parent2.reload
assert_equal [parent1.id, 1], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 3], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent2.id, 1], [child.root_id, child.nested_set_span]
end
def test_move_a_child_with_descendants_to_another_issue
parent1 = create_issue!
parent2 = create_issue!
child = create_issue!(:parent_issue_id => parent1.id)
grandchild = create_issue!(:parent_issue_id => child.id)
parent1.reload
parent2.reload
child.reload
grandchild.reload
assert_equal [parent1.id, 5], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 1], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent1.id, 3], [child.root_id, child.nested_set_span]
assert_equal [parent1.id, 1], [grandchild.root_id, grandchild.nested_set_span]
child.reload.parent_issue_id = parent2.id
child.save!
child.reload
grandchild.reload
parent1.reload
parent2.reload
assert_equal [parent1.id, 1], [parent1.root_id, parent1.nested_set_span]
assert_equal [parent2.id, 5], [parent2.root_id, parent2.nested_set_span]
assert_equal [parent2.id, 3], [child.root_id, child.nested_set_span]
assert_equal [parent2.id, 1], [grandchild.root_id, grandchild.nested_set_span]
end
def test_move_a_child_with_descendants_to_another_project
Setting.cross_project_issue_relations = "0"
parent1 = create_issue!
child = create_issue!(:parent_issue_id => parent1.id)
grandchild = create_issue!(:parent_issue_id => child.id)
child = create_issue!(:parent_id => parent1.id)
grandchild = create_issue!(:parent_id => child.id)
assert child.reload.move_to_project(Project.find(2))
child.reload
@ -169,8 +71,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_invalid_move_to_another_project
parent1 = create_issue!
child = create_issue!(:parent_issue_id => parent1.id)
grandchild = create_issue!(:parent_issue_id => child.id, :type_id => 2)
child = create_issue!(:parent_id => parent1.id)
grandchild = create_issue!(:parent_id => child.id, :type_id => 2)
Project.find(2).type_ids = [1]
parent1.reload
@ -191,19 +93,19 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_moving_an_issue_to_a_descendant_should_not_validate
parent1 = create_issue!
parent2 = create_issue!
child = create_issue!(:parent_issue_id => parent1.id)
grandchild = create_issue!(:parent_issue_id => child.id)
child = create_issue!(:parent_id => parent1.id)
grandchild = create_issue!(:parent_id => child.id)
child.reload
child.parent_issue_id = grandchild.id
child.parent_id = grandchild.id
assert !child.save
refute_empty child.errors[:parent_issue_id]
refute_empty child.errors[:parent_id]
end
def test_moving_an_issue_should_keep_valid_relations_only
issue1 = create_issue!
issue2 = create_issue!
issue3 = create_issue!(:parent_issue_id => issue2.id)
issue3 = create_issue!(:parent_id => issue2.id)
issue4 = create_issue!
(r1 = IssueRelation.new.tap do |i|
i.force_attributes = { :issue_from => issue1,
@ -221,7 +123,7 @@ class IssueNestedSetTest < ActiveSupport::TestCase
:relation_type => IssueRelation::TYPE_PRECEDES }
end).save!
issue2.reload
issue2.parent_issue_id = issue1.id
issue2.parent_id = issue1.id
issue2.save!
assert !IssueRelation.exists?(r1.id)
assert !IssueRelation.exists?(r2.id)
@ -231,8 +133,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_destroy_should_destroy_children
issue1 = create_issue!
issue2 = create_issue!
issue3 = create_issue!(:parent_issue_id => issue2.id)
issue4 = create_issue!(:parent_issue_id => issue1.id)
issue3 = create_issue!(:parent_id => issue2.id)
issue4 = create_issue!(:parent_id => issue1.id)
issue3.init_journal(User.find(2))
issue3.subject = 'child with journal'
@ -254,8 +156,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_destroy_parent_issue_updated_during_children_destroy
parent = create_issue!
create_issue!(:start_date => Date.today, :parent_issue_id => parent.id)
create_issue!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
create_issue!(:start_date => Date.today, :parent_id => parent.id)
create_issue!(:start_date => 2.days.from_now, :parent_id => parent.id)
assert_difference 'Issue.count', -3 do
Issue.find(parent.id).destroy
@ -264,8 +166,8 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_destroy_child_issue_with_children
root = create_issue!(:project_id => 1, :author_id => 2, :type_id => 1, :subject => 'root').reload
child = create_issue!(:project_id => 1, :author_id => 2, :type_id => 1, :subject => 'child', :parent_issue_id => root.id).reload
leaf = create_issue!(:project_id => 1, :author_id => 2, :type_id => 1, :subject => 'leaf', :parent_issue_id => child.id).reload
child = create_issue!(:project_id => 1, :author_id => 2, :type_id => 1, :subject => 'child', :parent_id => root.id).reload
leaf = create_issue!(:project_id => 1, :author_id => 2, :type_id => 1, :subject => 'leaf', :parent_id => child.id).reload
leaf.init_journal(User.find(2))
leaf.subject = 'leaf with journal'
leaf.save!
@ -283,10 +185,10 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_destroy_issue_with_grand_child
parent = create_issue!
issue = create_issue!(:parent_issue_id => parent.id)
child = create_issue!(:parent_issue_id => issue.id)
grandchild1 = create_issue!(:parent_issue_id => child.id)
grandchild2 = create_issue!(:parent_issue_id => child.id)
issue = create_issue!(:parent_id => parent.id)
child = create_issue!(:parent_id => issue.id)
grandchild1 = create_issue!(:parent_id => child.id)
grandchild2 = create_issue!(:parent_id => child.id)
assert_difference 'Issue.count', -4 do
Issue.find(issue.id).destroy
@ -298,12 +200,12 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_parent_priority_should_be_the_highest_child_priority
parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
# Create children
child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_id => parent.id)
assert_equal 'High', parent.reload.priority.name
child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_id => child1.id)
assert_equal 'Immediate', child1.reload.priority.name
assert_equal 'Immediate', parent.reload.priority.name
child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_id => parent.id)
assert_equal 'Immediate', parent.reload.priority.name
# Destroy a child
child1.destroy
@ -316,9 +218,9 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_parent_dates_should_be_lowest_start_and_highest_due_dates
parent = create_issue!
create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_id => parent.id)
create_issue!( :due_date => '2010-02-13', :parent_id => parent.id)
create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_id => parent.id)
parent.reload
assert_equal Date.parse('2010-01-25'), parent.start_date
assert_equal Date.parse('2010-02-22'), parent.due_date
@ -326,51 +228,53 @@ class IssueNestedSetTest < ActiveSupport::TestCase
def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
parent = create_issue!
create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
create_issue!(:done_ratio => 20, :parent_id => parent.id)
assert_equal 20, parent.reload.done_ratio
create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
create_issue!(:done_ratio => 70, :parent_id => parent.id)
assert_equal 45, parent.reload.done_ratio
child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
child = create_issue!(:done_ratio => 0, :parent_id => parent.id)
assert_equal 30, parent.reload.done_ratio
create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
create_issue!(:done_ratio => 30, :parent_id => child.id)
assert_equal 30, child.reload.done_ratio
assert_equal 40, parent.reload.done_ratio
end
def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
parent = create_issue!
create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_id => parent.id)
assert_equal 20, parent.reload.done_ratio
create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_id => parent.id)
assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
end
def test_parent_estimate_should_be_sum_of_leaves
parent = create_issue!
create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
create_issue!(:estimated_hours => nil, :parent_id => parent.id)
assert_equal nil, parent.reload.estimated_hours
create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
create_issue!(:estimated_hours => 5, :parent_id => parent.id)
assert_equal 5, parent.reload.estimated_hours
create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
create_issue!(:estimated_hours => 7, :parent_id => parent.id)
assert_equal 12, parent.reload.estimated_hours
end
def test_move_parent_updates_old_parent_attributes
first_parent = create_issue!
second_parent = create_issue!
child = create_issue!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
child = create_issue!(:estimated_hours => 5,
:parent_id => first_parent.id)
assert_equal 5, first_parent.reload.estimated_hours
child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
child.update_attributes(:estimated_hours => 7,
:parent_id => second_parent.id)
assert_equal 7, second_parent.reload.estimated_hours
assert_nil first_parent.reload.estimated_hours
end
def test_reschuling_a_parent_should_reschedule_subtasks
parent = create_issue!
c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_id => parent.id)
c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_id => parent.id)
parent.reload
parent.reschedule_after(Date.parse('2010-06-02'))
c1.reload
@ -385,9 +289,9 @@ class IssueNestedSetTest < ActiveSupport::TestCase
Project.delete_all # make sure unqiue identifiers
p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :type_ids => [1, 2])
i1 = create_issue!(:project_id => p.id, :subject => 'i1')
i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_id => i1.id)
i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_id => i1.id)
i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_id => i2.id)
i5 = create_issue!(:project_id => p.id, :subject => 'i5')
c = Project.new(:name => 'Copy', :identifier => 'copy', :type_ids => [1, 2])
c.copy(p, :only => 'work_packages')

@ -220,8 +220,8 @@ class VersionTest < ActiveSupport::TestCase
should "return the sum of leaves estimated hours" do
parent = add_issue(@version)
add_issue(@version, :estimated_hours => 2.5, :parent_issue_id => parent.id)
add_issue(@version, :estimated_hours => 5, :parent_issue_id => parent.id)
add_issue(@version, :estimated_hours => 2.5, :parent_id => parent.id)
add_issue(@version, :estimated_hours => 5, :parent_id => parent.id)
assert_equal 7.5, @version.estimated_hours
end
end

Loading…
Cancel
Save