summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/helpers/settings_helper.rb25
-rw-r--r--app/models/issue.rb57
-rw-r--r--app/views/issues/_attributes.html.erb10
-rw-r--r--app/views/settings/_issues.html.erb9
-rw-r--r--config/locales/en.yml3
-rw-r--r--config/locales/fr.yml1
-rw-r--r--config/settings.yml4
-rw-r--r--test/object_helpers.rb6
-rw-r--r--test/unit/issue_nested_set_test.rb43
-rw-r--r--test/unit/issue_subtasking_test.rb146
10 files changed, 236 insertions, 68 deletions
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index fcf8ec2e3..0dcdbb040 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -79,7 +79,12 @@ module SettingsHelper
def setting_label(setting, options={})
label = options.delete(:label)
- label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}"), options[:label_options]).html_safe : ''
+ if label == false
+ ''
+ else
+ text = label.is_a?(String) ? label : l(label || "setting_#{setting}")
+ label_tag("settings_#{setting}", text, options[:label_options])
+ end
end
# Renders a notification field for a Redmine::Notifiable option
@@ -126,6 +131,24 @@ module SettingsHelper
options.map {|label, value| [l(label), value.to_s]}
end
+ def parent_issue_dates_options
+ options = [
+ [:label_parent_task_attributes_derived, 'derived'],
+ [:label_parent_task_attributes_independent, 'independent']
+ ]
+
+ options.map {|label, value| [l(label), value.to_s]}
+ end
+
+ def parent_issue_priority_options
+ options = [
+ [:label_parent_task_attributes_derived, 'derived'],
+ [:label_parent_task_attributes_independent, 'independent']
+ ]
+
+ options.map {|label, value| [l(label), value.to_s]}
+ end
+
# Returns the options for the date_format setting
def date_format_setting_options(locale)
Setting::DATE_FORMATS.map do |f|
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 9ea7f6c57..7d04d1e15 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -426,6 +426,15 @@ class Issue < ActiveRecord::Base
# Make sure that project_id can always be set for new issues
names |= %w(project_id)
end
+ if dates_derived?
+ names -= %w(start_date due_date)
+ end
+ if priority_derived?
+ names -= %w(priority_id)
+ end
+ unless leaf?
+ names -= %w(done_ratio estimated_hours)
+ end
names
end
@@ -463,10 +472,6 @@ class Issue < ActiveRecord::Base
attrs = delete_unsafe_attributes(attrs, user)
return if attrs.empty?
- unless leaf?
- attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
- end
-
if attrs['parent_issue_id'].present?
s = attrs['parent_issue_id'].to_s
unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
@@ -1094,11 +1099,15 @@ class Issue < ActiveRecord::Base
end
def soonest_start(reload=false)
- @soonest_start = nil if reload
- @soonest_start ||= (
- relations_to(reload).collect{|relation| relation.successor_soonest_start} +
- [(@parent_issue || parent).try(:soonest_start)]
- ).compact.max
+ if @soonest_start.nil? || reload
+ dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
+ p = @parent_issue || parent
+ if p && Setting.parent_issue_dates == 'derived'
+ dates << p.soonest_start
+ end
+ @soonest_start = dates.compact.max
+ end
+ @soonest_start
end
# Sets start_date on the given date or the next working day
@@ -1114,7 +1123,7 @@ class Issue < ActiveRecord::Base
# If the issue is a parent task, this is done by rescheduling its subtasks.
def reschedule_on!(date)
return if date.nil?
- if leaf?
+ if leaf? || !dates_derived?
if start_date.nil? || start_date != date
if start_date && start_date > date
# Issue can not be moved earlier than its soonest start date
@@ -1144,6 +1153,14 @@ class Issue < ActiveRecord::Base
end
end
+ def dates_derived?
+ !leaf? && Setting.parent_issue_dates == 'derived'
+ end
+
+ def priority_derived?
+ !leaf? && Setting.parent_issue_priority == 'derived'
+ end
+
def <=>(issue)
if issue.nil?
-1
@@ -1430,16 +1447,20 @@ class Issue < ActiveRecord::Base
def recalculate_attributes_for(issue_id)
if issue_id && p = Issue.find_by_id(issue_id)
- # 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 p.priority_derived?
+ # 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)
+ end
end
- # 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
+ if p.dates_derived?
+ # 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
+ end
end
# done ratio = weighted average ratio of leaves
diff --git a/app/views/issues/_attributes.html.erb b/app/views/issues/_attributes.html.erb
index 61ffa8ae3..3d5f85c7b 100644
--- a/app/views/issues/_attributes.html.erb
+++ b/app/views/issues/_attributes.html.erb
@@ -48,25 +48,23 @@
<% if @issue.safe_attribute? 'start_date' %>
<p id="start_date_area">
- <%= f.text_field(:start_date, :size => 10, :disabled => !@issue.leaf?,
- :required => @issue.required_attribute?('start_date')) %>
+ <%= f.text_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
<%= calendar_for('issue_start_date') if @issue.leaf? %>
</p>
<% end %>
<% if @issue.safe_attribute? 'due_date' %>
<p id="due_date_area">
- <%= f.text_field(:due_date, :size => 10, :disabled => !@issue.leaf?,
- :required => @issue.required_attribute?('due_date')) %>
+ <%= f.text_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
<%= calendar_for('issue_due_date') if @issue.leaf? %>
</p>
<% end %>
<% if @issue.safe_attribute? 'estimated_hours' %>
-<p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
+<p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
<% end %>
-<% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
+<% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
<% end %>
</div>
diff --git a/app/views/settings/_issues.html.erb b/app/views/settings/_issues.html.erb
index c1e802238..a0dfd786c 100644
--- a/app/views/settings/_issues.html.erb
+++ b/app/views/settings/_issues.html.erb
@@ -23,6 +23,15 @@
</div>
<fieldset class="box">
+ <legend><%= l(:label_parent_task_attributes) %></legend>
+ <div class="tabular settings">
+ <p><%= setting_select :parent_issue_dates, parent_issue_dates_options, :label => "#{l(:field_start_date)} / #{l(:field_due_date)}" %></p>
+
+ <p><%= setting_select :parent_issue_priority, parent_issue_priority_options, :label => :field_priority %></p>
+ </div>
+</fieldset>
+
+<fieldset class="box">
<legend><%= l(:setting_issue_list_default_columns) %></legend>
<%= render_query_columns_selection(
IssueQuery.new(:column_names => Setting.issue_list_default_columns),
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 1f205b0ff..92b2ffdeb 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -941,6 +941,9 @@ en:
label_enable_notifications: Enable notifications
label_disable_notifications: Disable notifications
label_blank_value: blank
+ label_parent_task_attributes: Parent tasks attributes
+ label_parent_task_attributes_derived: Calculated from subtasks
+ label_parent_task_attributes_independent: Independent of subtasks
button_login: Login
button_submit: Submit
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 54dc13a61..539375277 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -961,6 +961,7 @@ fr:
label_enable_notifications: Activer les notifications
label_disable_notifications: Désactiver les notifications
label_blank_value: non renseigné
+ label_parent_task_attributes: Attributs des tâches parentes
button_login: Connexion
button_submit: Soumettre
diff --git a/config/settings.yml b/config/settings.yml
index e5093dd58..6a163c72f 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -146,6 +146,10 @@ cross_project_issue_relations:
# Enables subtasks to be in other projects
cross_project_subtasks:
default: 'tree'
+parent_issue_dates:
+ default: 'derived'
+parent_issue_priority:
+ default: 'derived'
link_copied_issue:
default: 'ask'
issue_group_assignment:
diff --git a/test/object_helpers.rb b/test/object_helpers.rb
index 2b1fa2f61..7061e75c1 100644
--- a/test/object_helpers.rb
+++ b/test/object_helpers.rb
@@ -105,6 +105,12 @@ module ObjectHelpers
issue.reload
end
+ def Issue.generate_with_child!(attributes={})
+ issue = Issue.generate!(attributes)
+ Issue.generate!(:parent_issue_id => issue.id)
+ issue.reload
+ end
+
def Journal.generate!(attributes={})
journal = Journal.new(attributes)
journal.user ||= User.first
diff --git a/test/unit/issue_nested_set_test.rb b/test/unit/issue_nested_set_test.rb
index 6fb5b0045..632464872 100644
--- a/test/unit/issue_nested_set_test.rb
+++ b/test/unit/issue_nested_set_test.rb
@@ -287,35 +287,6 @@ class IssueNestedSetTest < ActiveSupport::TestCase
end
end
- def test_parent_priority_should_be_the_highest_child_priority
- parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
- # Create children
- child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High'))
- assert_equal 'High', parent.reload.priority.name
- child2 = child1.generate_child!(:priority => IssuePriority.find_by_name('Immediate'))
- assert_equal 'Immediate', child1.reload.priority.name
- assert_equal 'Immediate', parent.reload.priority.name
- child3 = parent.generate_child!(:priority => IssuePriority.find_by_name('Low'))
- assert_equal 'Immediate', parent.reload.priority.name
- # Destroy a child
- child1.destroy
- assert_equal 'Low', parent.reload.priority.name
- # Update a child
- child3.reload.priority = IssuePriority.find_by_name('Normal')
- child3.save!
- assert_equal 'Normal', parent.reload.priority.name
- end
-
- def test_parent_dates_should_be_lowest_start_and_highest_due_dates
- parent = Issue.generate!
- parent.generate_child!(:start_date => '2010-01-25', :due_date => '2010-02-15')
- parent.generate_child!( :due_date => '2010-02-13')
- parent.generate_child!(:start_date => '2010-02-01', :due_date => '2010-02-22')
- parent.reload
- assert_equal Date.parse('2010-01-25'), parent.start_date
- assert_equal Date.parse('2010-02-22'), parent.due_date
- end
-
def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
parent = Issue.generate!
parent.generate_child!(:done_ratio => 20)
@@ -390,20 +361,6 @@ class IssueNestedSetTest < ActiveSupport::TestCase
assert_nil first_parent.reload.estimated_hours
end
- def test_reschuling_a_parent_should_reschedule_subtasks
- parent = Issue.generate!
- c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
- c2 = parent.generate_child!(:start_date => '2010-06-03', :due_date => '2010-06-10')
- parent.reload
- parent.reschedule_on!(Date.parse('2010-06-02'))
- c1.reload
- assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
- c2.reload
- assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
- parent.reload
- assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
- end
-
def test_project_copy_should_copy_issue_tree
p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
i1 = Issue.generate!(:project => p, :subject => 'i1')
diff --git a/test/unit/issue_subtasking_test.rb b/test/unit/issue_subtasking_test.rb
new file mode 100644
index 000000000..621af178a
--- /dev/null
+++ b/test/unit/issue_subtasking_test.rb
@@ -0,0 +1,146 @@
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class IssueSubtaskingTest < ActiveSupport::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :trackers, :projects_trackers,
+ :issue_statuses, :issue_categories, :enumerations,
+ :issues
+
+ def test_leaf_planning_fields_should_be_editable
+ issue = Issue.generate!
+ user = User.find(1)
+ %w(priority_id done_ratio start_date due_date estimated_hours).each do |attribute|
+ assert issue.safe_attribute?(attribute, user)
+ end
+ end
+
+ def test_parent_dates_should_be_read_only_with_parent_issue_dates_set_to_derived
+ with_settings :parent_issue_dates => 'derived' do
+ issue = Issue.generate_with_child!
+ user = User.find(1)
+ %w(start_date due_date).each do |attribute|
+ assert !issue.safe_attribute?(attribute, user)
+ end
+ end
+ end
+
+ def test_parent_dates_should_be_lowest_start_and_highest_due_dates_with_parent_issue_dates_set_to_derived
+ with_settings :parent_issue_dates => 'derived' do
+ parent = Issue.generate!
+ parent.generate_child!(:start_date => '2010-01-25', :due_date => '2010-02-15')
+ parent.generate_child!( :due_date => '2010-02-13')
+ parent.generate_child!(:start_date => '2010-02-01', :due_date => '2010-02-22')
+ parent.reload
+ assert_equal Date.parse('2010-01-25'), parent.start_date
+ assert_equal Date.parse('2010-02-22'), parent.due_date
+ end
+ end
+
+ def test_reschuling_a_parent_should_reschedule_subtasks_with_parent_issue_dates_set_to_derived
+ with_settings :parent_issue_dates => 'derived' do
+ parent = Issue.generate!
+ c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
+ c2 = parent.generate_child!(:start_date => '2010-06-03', :due_date => '2010-06-10')
+ parent.reload.reschedule_on!(Date.parse('2010-06-02'))
+ c1.reload
+ assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
+ c2.reload
+ assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
+ parent.reload
+ assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
+ end
+ end
+
+ def test_parent_priority_should_be_read_only_with_parent_issue_priority_set_to_derived
+ with_settings :parent_issue_priority => 'derived' do
+ issue = Issue.generate_with_child!
+ user = User.find(1)
+ assert !issue.safe_attribute?('priority_id', user)
+ end
+ end
+
+ def test_parent_priority_should_be_the_highest_child_priority
+ with_settings :parent_issue_priority => 'derived' do
+ parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
+ # Create children
+ child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High'))
+ assert_equal 'High', parent.reload.priority.name
+ child2 = child1.generate_child!(:priority => IssuePriority.find_by_name('Immediate'))
+ assert_equal 'Immediate', child1.reload.priority.name
+ assert_equal 'Immediate', parent.reload.priority.name
+ child3 = parent.generate_child!(:priority => IssuePriority.find_by_name('Low'))
+ assert_equal 'Immediate', parent.reload.priority.name
+ # Destroy a child
+ child1.destroy
+ assert_equal 'Low', parent.reload.priority.name
+ # Update a child
+ child3.reload.priority = IssuePriority.find_by_name('Normal')
+ child3.save!
+ assert_equal 'Normal', parent.reload.priority.name
+ end
+ end
+
+ def test_parent_dates_should_be_editable_with_parent_issue_dates_set_to_independent
+ with_settings :parent_issue_dates => 'independent' do
+ issue = Issue.generate_with_child!
+ user = User.find(1)
+ %w(start_date due_date).each do |attribute|
+ assert issue.safe_attribute?(attribute, user)
+ end
+ end
+ end
+
+ def test_parent_dates_should_not_be_updated_with_parent_issue_dates_set_to_independent
+ with_settings :parent_issue_dates => 'independent' do
+ parent = Issue.generate!(:start_date => '2015-07-01', :due_date => '2015-08-01')
+ parent.generate_child!(:start_date => '2015-06-01', :due_date => '2015-09-01')
+ parent.reload
+ assert_equal Date.parse('2015-07-01'), parent.start_date
+ assert_equal Date.parse('2015-08-01'), parent.due_date
+ end
+ end
+
+ def test_reschuling_a_parent_should_not_reschedule_subtasks_with_parent_issue_dates_set_to_independent
+ with_settings :parent_issue_dates => 'independent' do
+ parent = Issue.generate!(:start_date => '2010-05-01', :due_date => '2010-05-20')
+ c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
+ parent.reload.reschedule_on!(Date.parse('2010-06-01'))
+ assert_equal Date.parse('2010-06-01'), parent.reload.start_date
+ c1.reload
+ assert_equal [Date.parse('2010-05-12'), Date.parse('2010-05-18')], [c1.start_date, c1.due_date]
+ end
+ end
+
+ def test_parent_priority_should_be_editable_with_parent_issue_priority_set_to_independent
+ with_settings :parent_issue_priority => 'independent' do
+ issue = Issue.generate_with_child!
+ user = User.find(1)
+ assert issue.safe_attribute?('priority_id', user)
+ end
+ end
+
+ def test_parent_priority_should_not_be_updated_with_parent_issue_priority_set_to_independent
+ with_settings :parent_issue_priority => 'independent' do
+ parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
+ child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High'))
+ assert_equal 'Normal', parent.reload.priority.name
+ end
+ end
+end