]> source.dussan.org Git - redmine.git/commitdiff
Adds a setting to allow subtasks to belong to other projects (#5487).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Wed, 10 Oct 2012 17:38:17 +0000 (17:38 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Wed, 10 Oct 2012 17:38:17 +0000 (17:38 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@10587 e93f8b46-1217-0410-a6f0-8f06a7374b81

12 files changed:
app/helpers/issues_helper.rb
app/helpers/settings_helper.rb
app/models/issue.rb
app/views/issues/_attributes.html.erb
app/views/settings/_issues.html.erb
config/locales/en.yml
config/locales/fr.yml
config/settings.yml
test/fixtures/projects_trackers.yml
test/test_helper.rb
test/unit/issue_nested_set_test.rb
test/unit/issue_test.rb

index 1ba4c4635937e52dafaf3396cbd73bcd70e30ac2..2cc387ff82cbae413092353ff1776e9f32a16b27 100644 (file)
@@ -65,7 +65,7 @@ module IssuesHelper
     s = ''
     ancestors = issue.root? ? [] : issue.ancestors.visible.all
     ancestors.each do |ancestor|
-      s << '<div>' + content_tag('p', link_to_issue(ancestor))
+      s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
     end
     s << '<div>'
     subject = h(issue.subject)
@@ -82,7 +82,7 @@ module IssuesHelper
     issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
       s << content_tag('tr',
              content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
-             content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
+             content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
              content_tag('td', h(child.status)) +
              content_tag('td', link_to_user(child.assigned_to)) +
              content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
index dd20f24c644226b1743643d755f5f7ac0915356a..c1fa8ea64980416643d2c9714b23300eb5649fe6 100644 (file)
@@ -91,4 +91,16 @@ module SettingsHelper
                          l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
                        :class => notifiable.parent.present? ? "parent" : '').html_safe
   end
+
+  def cross_project_subtasks_options
+    options = [
+      [:label_disabled, ''],
+      [:label_cross_project_system, 'system'],
+      [:label_cross_project_tree, 'tree'],
+      [:label_cross_project_hierarchy, 'hierarchy'],
+      [:label_cross_project_descendants, 'descendants']
+    ]
+
+    options.map {|label, value| [l(label), value.to_s]}
+  end
 end
index 37a19a9c4adc8fc11b811951a1b049006e7ffee3..520d2c3dcebabe629146cfe2899e0f9706262d57 100644 (file)
@@ -285,7 +285,8 @@ class Issue < ActiveRecord::Base
       if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
         self.fixed_version = nil
       end
-      if parent && parent.project_id != project_id
+      # Clear the parent task if it's no longer valid
+      unless valid_parent_project?
         self.parent_issue_id = nil
       end
       @custom_field_values = nil
@@ -550,8 +551,8 @@ class Issue < ActiveRecord::Base
 
     # Checks parent issue assignment
     if @parent_issue
-      if @parent_issue.project_id != project_id
-        errors.add :parent_issue_id, :not_same_project
+      if !valid_parent_project?(@parent_issue)
+        errors.add :parent_issue_id, :invalid
       elsif !new_record?
         # moving an existing issue
         if @parent_issue.root_id != root_id
@@ -559,7 +560,7 @@ class Issue < ActiveRecord::Base
         elsif move_possible?(@parent_issue)
           # move accepted inside tree
         else
-          errors.add :parent_issue_id, :not_a_valid_parent
+          errors.add :parent_issue_id, :invalid
         end
       end
     end
@@ -963,6 +964,25 @@ class Issue < ActiveRecord::Base
     end
   end
 
+       # Returns true if issue's project is a valid
+       # parent issue project
+  def valid_parent_project?(issue=parent)
+    return true if issue.nil? || issue.project_id == project_id
+
+    case Setting.cross_project_subtasks
+    when 'system'
+      true
+    when 'tree'
+      issue.project.root == project.root
+    when 'hierarchy'
+      issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
+    when 'descendants'
+      issue.project.is_or_is_ancestor_of?(project)
+    else
+      false
+    end
+  end
+
   # Extracted from the ReportsController.
   def self.by_tracker(project)
     count_and_group_by(:project => project,
@@ -1042,8 +1062,9 @@ class Issue < ActiveRecord::Base
       relations_to.clear
     end
 
-    # Move subtasks
+    # Move subtasks that were in the same project
     children.each do |child|
+      next unless child.project_id == project_id_was
       # Change project and keep project
       child.send :project=, project, true
       unless child.save
index 2b73623ea6a7f066b52679a9b7f942fe2d612a36..9b4c508cdbb4f334b5bab786aa3ab3ec3124c34d 100644 (file)
@@ -43,7 +43,7 @@
 <div class="splitcontentright">
 <% if @issue.safe_attribute? 'parent_issue_id' %>
 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
-<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project)}')" %>
+<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path}')" %>
 <% end %>
 
 <% if @issue.safe_attribute? 'start_date' %>
index 6c9bf4553ce77c114d8c8e63d83a8a4fd056c69f..a8d004daa98acd398ebbc2c3bc0427d4c7d85204 100644 (file)
@@ -3,6 +3,8 @@
 <div class="box tabular settings">
 <p><%= setting_check_box :cross_project_issue_relations %></p>
 
+<p><%= setting_select :cross_project_subtasks, cross_project_subtasks_options %></p>
+
 <p><%= setting_check_box :issue_group_assignment %></p>
 
 <p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
index 50373ed8311d8665e8596165d1f994d515d9c31b..050ee02790f01bc0d196ab9285aaf4c5b1624ed1 100644 (file)
@@ -357,6 +357,7 @@ en:
   setting_date_format: Date format
   setting_time_format: Time format
   setting_cross_project_issue_relations: Allow cross-project issue relations
+  setting_cross_project_subtasks: Allow cross-project subtasks
   setting_issue_list_default_columns: Default columns displayed on the issue list
   setting_repositories_encodings: Attachments and repositories encodings
   setting_emails_header: Emails header
index 2aa9ab1ee5b5e7d84de9e79183882cba72d1a9ad..6c6067b84cf44615766e55c27afaa40676925d1e 100644 (file)
@@ -356,6 +356,7 @@ fr:
   setting_date_format: Format de date
   setting_time_format: Format d'heure
   setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
+  setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
   setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
   setting_emails_footer: Pied-de-page des emails
   setting_protocol: Protocole
index 01906609fb8933f0bf2a6bd1efef14d517dd35b5..6aa22416636859f9211cfa1b507f4d29f2cde6c6 100644 (file)
@@ -133,6 +133,9 @@ user_format:
   format: symbol
 cross_project_issue_relations:
   default: 0
+# Enables subtasks to be in other projects
+cross_project_subtasks:
+  default: 'tree'
 issue_group_assignment:
   default: 0
 default_issue_start_date_to_creation_date:
index 4766a9f82d095b9f11b6b724a1c0cd00ed55a98e..2c5707c5e0f1da3be821ba53097813cf33469f08 100644 (file)
@@ -41,4 +41,7 @@ projects_trackers_013:
 projects_trackers_014: 
   project_id: 1
   tracker_id: 3
+projects_trackers_015: 
+  project_id: 6
+  tracker_id: 1
   
\ No newline at end of file
index 12e70b9bf1e4014ebedab9942085e578b2368228..66a6f26462936f001c9d978434f3bca071f735d1 100644 (file)
@@ -156,6 +156,14 @@ class ActiveSupport::TestCase
     hs
   end
 
+  def assert_save(object)
+    saved = object.save
+    message = "#{object.class} could not be saved"
+    errors = object.errors.full_messages.map {|m| "- #{m}"}
+    message << ":\n#{errors.join("\n")}" if errors.any?
+    assert_equal true, saved, message
+  end
+
   def assert_error_tag(options={})
     assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
   end
index 6716759b5e7df8bf27e91c5c008603869854a776..921cddc99c76b7a4ba3ac0d6a6185fd9aa10f1b5 100644 (file)
@@ -49,7 +49,15 @@ class IssueNestedSetTest < ActiveSupport::TestCase
     assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
   end
 
-  def test_creating_a_child_in_different_project_should_not_validate
+  def test_creating_a_child_in_a_subproject_should_validate
+    issue = create_issue!
+    child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1,
+                      :subject => 'child', :parent_issue_id => issue.id)
+    assert_save child
+    assert_equal issue, child.reload.parent
+  end
+
+  def test_creating_a_child_in_an_invalid_project_should_not_validate
     issue = create_issue!
     child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
                       :subject => 'child', :parent_issue_id => issue.id)
index 7fdd9cf0f1d36f8bc3d5dbfe0527f53e92557249..4bd718de199e42c80347ddaec6b9b92b93b25d83 100644 (file)
@@ -906,6 +906,24 @@ class IssueTest < ActiveSupport::TestCase
     assert_equal 7, issue.fixed_version_id
   end
 
+  def test_move_to_another_project_should_keep_parent_if_valid
+    issue = Issue.find(1)
+    issue.update_attribute(:parent_issue_id, 2)
+    issue.project = Project.find(3)
+    assert issue.save
+    issue.reload
+    assert_equal 2, issue.parent_id
+  end
+
+  def test_move_to_another_project_should_clear_parent_if_not_valid
+    issue = Issue.find(1)
+    issue.update_attribute(:parent_issue_id, 2)
+    issue.project = Project.find(2)
+    assert issue.save
+    issue.reload
+    assert_nil issue.parent_id
+  end
+
   def test_move_to_another_project_with_disabled_tracker
     issue = Issue.find(1)
     target = Project.find(2)
@@ -996,6 +1014,48 @@ class IssueTest < ActiveSupport::TestCase
     end
   end
 
+  def test_valid_parent_project
+    issue = Issue.find(1)
+    issue_in_same_project = Issue.find(2)
+    issue_in_child_project = Issue.find(5)
+    issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
+    issue_in_other_child_project = Issue.find(6)
+    issue_in_different_tree = Issue.find(4)
+
+    with_settings :cross_project_subtasks => '' do
+      assert_equal true, issue.valid_parent_project?(issue_in_same_project)
+      assert_equal false, issue.valid_parent_project?(issue_in_child_project)
+      assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
+      assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
+    end
+
+    with_settings :cross_project_subtasks => 'system' do
+      assert_equal true, issue.valid_parent_project?(issue_in_same_project)
+      assert_equal true, issue.valid_parent_project?(issue_in_child_project)
+      assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
+    end
+
+    with_settings :cross_project_subtasks => 'tree' do
+      assert_equal true, issue.valid_parent_project?(issue_in_same_project)
+      assert_equal true, issue.valid_parent_project?(issue_in_child_project)
+      assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
+      assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
+
+      assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
+      assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
+    end
+
+    with_settings :cross_project_subtasks => 'descendants' do
+      assert_equal true, issue.valid_parent_project?(issue_in_same_project)
+      assert_equal false, issue.valid_parent_project?(issue_in_child_project)
+      assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
+      assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
+
+      assert_equal true, issue_in_child_project.valid_parent_project?(issue)
+      assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
+    end
+  end
+
   def test_recipients_should_include_previous_assignee
     user = User.find(3)
     user.members.update_all ["mail_notification = ?", false]