]> source.dussan.org Git - redmine.git/commitdiff
Bulk addition of related issues (#33418).
authorGo MAEDA <maeda@farend.jp>
Fri, 25 Dec 2020 02:25:41 +0000 (02:25 +0000)
committerGo MAEDA <maeda@farend.jp>
Fri, 25 Dec 2020 02:25:41 +0000 (02:25 +0000)
Patch by Dmitry Makurin and Marius BALTEANU.

git-svn-id: http://svn.redmine.org/redmine/trunk@20689 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/issue_relations_controller.rb
app/helpers/issue_relations_helper.rb
app/views/issue_relations/_form.html.erb
test/functional/issue_relations_controller_test.rb

index 1cceba576f0b11f7b2c316677347b8eab1952715..14ee4c948f892abf7b1735fb82ce045c1c5085d0 100644 (file)
@@ -44,22 +44,29 @@ class IssueRelationsController < ApplicationController
   end
 
   def create
-    @relation = IssueRelation.new
-    @relation.issue_from = @issue
-    @relation.safe_attributes = params[:relation]
-    @relation.init_journals(User.current)
+    saved = false
+    params_relation = params[:relation]
+    unsaved_relations = []
+
+    relation_issues_to_id.each do |issue_to_id|
+      params_relation[:issue_to_id] = issue_to_id
+
+      @relation = IssueRelation.new
+      @relation.issue_from = @issue
+      @relation.safe_attributes = params_relation
+      @relation.init_journals(User.current)
 
-    begin
-      saved = @relation.save
-    rescue ActiveRecord::RecordNotUnique
-      saved = false
-      @relation.errors.add :base, :taken
+      unless saved = @relation.save
+        saved = false
+        unsaved_relations << @relation
+      end
     end
 
     respond_to do |format|
       format.html {redirect_to issue_path(@issue)}
       format.js do
         @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible?}
+        @unsaved_relations = unsaved_relations
       end
       format.api do
         if saved
@@ -98,4 +105,12 @@ class IssueRelationsController < ApplicationController
   rescue ActiveRecord::RecordNotFound
     render_404
   end
+
+  def relation_issues_to_id
+    params[:relation].require(:issue_to_id).split(',').reject(&:blank?)
+  rescue ActionController::ParameterMissing => e
+    # We return a empty array just to loop once and return a validation error
+    # ToDo: Find a better method to return an error if the param is missing.
+    ['']
+  end
 end
index 9c5d2123bceb4c547567c6716ba3b576b8aac227..a1ddad824dcc7330c84392aaadd71e06097e6c62 100644 (file)
@@ -22,4 +22,23 @@ module IssueRelationsHelper
     values = IssueRelation::TYPES
     values.keys.sort_by{|k| values[k][:order]}.collect{|k| [l(values[k][:name]), k]}
   end
+
+  def relation_error_messages(relations)
+    messages = {}
+    relations.each do |item|
+      item.errors.full_messages.each do |message|
+        messages[message] ||= []
+        messages[message] << item
+      end
+    end
+
+    messages.map do |message, items|
+      ids = items.map(&:issue_to_id).compact
+      if ids.empty?
+        message
+      else
+        "#{message}: ##{ids.join(', ')}"
+      end
+    end
+  end
 end
index 3a1018c1b266d859d896cbbf194862ad6aab99f9..5987ac7f835f150d7d2fdcbbe09813caec4e2de1 100644 (file)
@@ -1,7 +1,16 @@
-<%= error_messages_for 'relation' %>
-
+<% unsaved_relations_ids = '' %>
+<% if @unsaved_relations && @unsaved_relations.any? %>
+  <% unsaved_relations_ids = @unsaved_relations.map(&:issue_to_id).compact.join(", ") %>
+  <div id="errorExplanation">
+    <ul>
+      <% relation_error_messages(@unsaved_relations).each do |message| %>
+        <li><%= message %></li>
+      <% end %>
+    </ul>
+  </div>
+<% end %>
 <p><%= f.select :relation_type, collection_for_relation_type_select, {}, :onchange => "setPredecessorFieldsVisibility();" %>
-<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 10 %>
+<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :value => unsaved_relations_ids, :size => 10 %>
 <span id="predecessor_fields" style="display:none;">
 <%= l(:field_delay) %>: <%= f.text_field :delay, :size => 3 %> <%= l(:label_day_plural) %>
 </span>
index 024172adb8a6c50b30abcdb87a440c771a95b12d..1762afdea311e5d4ba781416445cba1a6ada72f8 100644 (file)
@@ -216,6 +216,53 @@ class IssueRelationsControllerTest < Redmine::ControllerTest
     assert_include 'Related issue cannot be blank', response.body
   end
 
+  def test_bulk_create_with_multiple_issue_to_id_issues
+    assert_difference 'IssueRelation.count', +3 do
+      post :create, :params => {
+        :issue_id => 1,
+        :relation => {
+          # js autocomplete adds a comma at the end
+          # issue to id should accept both id and hash with id
+          :issue_to_id => '2,3,#7, ',
+          :relation_type => 'relates',
+          :delay => ''
+        }
+      },
+      :xhr => true
+    end
+
+    assert_response :success
+    relations = IssueRelation.where(:issue_from_id => 1, :issue_to_id => [2, 3, 7])
+    assert_equal 3, relations.count
+    # all relations types should be 'relates'
+    relations.map {|r| assert_equal 'relates', r.relation_type}
+
+    # no error messages should be returned in the response
+    assert_not_include 'id=\"errorExplanation\"', response.body
+  end
+
+  def test_bulk_create_should_show_errors
+    assert_difference 'IssueRelation.count', +3 do
+      post :create, :params => {
+        :issue_id => 1,
+        :relation => {
+          :issue_to_id => '1,2,3,4,5,7',
+          :relation_type => 'relates',
+          :delay => ''
+        }
+      },
+      :xhr => true
+    end
+
+    assert_response :success
+    assert_equal 'text/javascript', response.media_type
+    # issue #1 is invalid
+    assert_include 'Related issue is invalid: #1', response.body
+    # issues #4 and #5 can't be related by default
+    assert_include 'Related issue cannot be blank', response.body
+    assert_include 'Related issue doesn&#39;t belong to the same project', response.body
+  end
+
   def test_destroy
     assert_difference 'IssueRelation.count', -1 do
       delete(:destroy, :params => {:id => '2'})