]> source.dussan.org Git - redmine.git/commitdiff
Ability to delete multiple attachments while updating an issue (#13072).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Wed, 13 Jul 2016 20:04:14 +0000 (20:04 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Wed, 13 Jul 2016 20:04:14 +0000 (20:04 +0000)
git-svn-id: http://svn.redmine.org/redmine/trunk@15650 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/models/issue.rb
app/views/issues/_edit.html.erb
public/javascripts/attachments.js
public/stylesheets/application.css
test/functional/issues_controller_test.rb

index 5b6ae304185228efa7d8618953bc142ab1ae99e6..d5a78717d0e9d3a6f1ccf9683cf096a0bae156d1 100644 (file)
@@ -59,6 +59,7 @@ class Issue < ActiveRecord::Base
 
   DONE_RATIO_OPTIONS = %w(issue_field issue_status)
 
+  attr_accessor :deleted_attachment_ids
   attr_reader :current_journal
   delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
 
@@ -109,7 +110,7 @@ class Issue < ActiveRecord::Base
               :force_updated_on_change, :update_closed_on, :set_assigned_to_was
   after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
   after_save :reschedule_following_issues, :update_nested_set_attributes,
-             :update_parent_attributes, :create_journal
+             :update_parent_attributes, :delete_selected_attachments, :create_journal
   # Should be after_create but would be called before previous after_save callbacks
   after_save :after_create_from_copy
   after_destroy :update_parent_attributes
@@ -403,6 +404,10 @@ class Issue < ActiveRecord::Base
     write_attribute(:description, arg)
   end
 
+  def deleted_attachment_ids
+    Array(@deleted_attachment_ids).map(&:to_i)
+  end
+
   # Overrides assign_attributes so that project and tracker get assigned first
   def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
     return if new_attributes.nil?
@@ -465,6 +470,9 @@ class Issue < ActiveRecord::Base
     :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
       user.allowed_to?(:manage_subtasks, issue.project)}
 
+  safe_attributes 'deleted_attachment_ids',
+    :if => lambda {|issue, user| issue.attachments_deletable?(user)}
+
   def safe_attribute_names(user=nil)
     names = super
     names -= disabled_core_fields
@@ -1586,6 +1594,13 @@ class Issue < ActiveRecord::Base
     end
   end
 
+  def delete_selected_attachments
+    if deleted_attachment_ids.present?
+      objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
+      attachments.delete(objects)
+    end
+  end
+
   # Callback on file attachment
   def attachment_added(attachment)
     if current_journal && !attachment.new_record?
index 67e33246dc0c3acfcb11b222b482ab5e14a10dad..3291ba73734027aae6b245abed1389aee6b97689 100644 (file)
   
       <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
       </fieldset>
-  
+
       <fieldset><legend><%= l(:label_attachment_plural) %></legend>
-      <p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
+        <% if @issue.attachments.any? && @issue.safe_attribute?('deleted_attachment_ids') %>
+        <div class="contextual"><%= link_to l(:label_edit_attachments), '#', :onclick => "$('#existing-attachments').toggle(); return false;" %></div>
+        <div id="existing-attachments" style="<%= @issue.deleted_attachment_ids.blank? ? 'display:none;' : '' %>">
+          <% @issue.attachments.each do |attachment| %>
+          <span class="existing-attachment">
+            <%= text_field_tag '', attachment.filename, :class => "filename", :disabled => true %>
+            <label>
+              <%= check_box_tag 'issue[deleted_attachment_ids][]',
+                                attachment.id,
+                                @issue.deleted_attachment_ids.include?(attachment.id),
+                                :id => nil, :class => "deleted_attachment" %> <%= l(:button_delete) %>
+            </label>
+          </span>
+          <% end %>
+          <hr />
+        </div>
+        <% end %>
+
+        <div id="new-attachments" style="display:inline-block;">
+          <%= render :partial => 'attachments/form', :locals => {:container => @issue} %>
+        </div>
       </fieldset>
     <% end %>
     </div>
index f33524543d3b0404e3b74be46e84a140248b9db2..c5e6b962a5c78eeacae0a7b1c11672ee3fd635a3 100644 (file)
@@ -189,3 +189,8 @@ function setupFileDrop() {
 }
 
 $(document).ready(setupFileDrop);
+$(document).ready(function(){
+  $("input.deleted_attachment").change(function(){
+    $(this).parents('.existing-attachment').toggleClass('deleted', $(this).is(":checked"));
+  }).change();
+});
index 9816c650816571c74e437d25c479cbccda36a8ac..8c936bd331b9619bcb47248b50a33cd6e13edb76 100644 (file)
@@ -363,7 +363,7 @@ div.square {
   overflow: hidden;
   width: .6em; height: .6em;
 }
-.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
+.contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;}
 .contextual input, .contextual select {font-size:0.9em;}
 .message .contextual { margin-top: 0; }
 
@@ -673,14 +673,16 @@ span.required {color: #bb0000;}
 .check_box_group.bool_cf {border:0; background:inherit;}
 .check_box_group.bool_cf label {display: inline;}
 
-#attachments_fields input.description {margin-left:4px; width:340px;}
-#attachments_fields span {display:block; white-space:nowrap;}
-#attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
+#attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
+#attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
+#attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
+#attachments_fields input.filename {height:1.8em;}
 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
 a.remove-upload:hover {text-decoration:none !important;}
+.existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
 
 div.fileover { background-color: lavender; }
 
@@ -1137,8 +1139,6 @@ a.close-icon:hover {background-image:url('../images/close_hl.png');}
   background-position: 0% 50%;
   background-repeat: no-repeat;
   padding-left: 16px;
-}
-a.icon-only {
   display: inline-block;
   width: 0;
   height: 16px;
@@ -1148,7 +1148,7 @@ a.icon-only {
   font-size: 8px;
   vertical-align: text-bottom;
 }
-a.icon-only::after {
+.icon-only::after {
   content: "&nbsp;";
 }
 
index d571d4bda1e72de00aa0d3729c37dc5fdf3ad003..d878b9608f29287e86bcfc1cb6afdf4465a39ffe 100644 (file)
@@ -3761,6 +3761,44 @@ class IssuesControllerTest < ActionController::TestCase
     end
   end
 
+  def test_put_update_with_attachment_deletion_should_create_a_single_journal
+    set_tmp_attachments_directory
+    @request.session[:user_id] = 2
+
+    journal = new_record(Journal) do
+      assert_difference 'Attachment.count', -2 do
+        put :update,
+            :id => 3,
+            :issue => {
+              :notes => 'Removing attachments',
+              :deleted_attachment_ids => ['1', '5']
+            }
+      end
+    end
+    assert_equal 'Removing attachments', journal.notes
+    assert_equal 2, journal.details.count
+  end
+
+  def test_put_update_with_attachment_deletion_and_failure_should_preserve_selected_attachments
+    set_tmp_attachments_directory
+    @request.session[:user_id] = 2
+
+    assert_no_difference 'Journal.count' do
+      assert_no_difference 'Attachment.count' do
+        put :update,
+            :id => 3,
+            :issue => {
+              :subject => '',
+              :notes => 'Removing attachments',
+              :deleted_attachment_ids => ['1', '5']
+            }
+      end
+    end
+    assert_select 'input[name=?][value="1"][checked=checked]', 'issue[deleted_attachment_ids][]'
+    assert_select 'input[name=?][value="5"][checked=checked]', 'issue[deleted_attachment_ids][]'
+    assert_select 'input[name=?][value="6"]:not([checked])', 'issue[deleted_attachment_ids][]'
+  end
+
   def test_put_update_with_no_change
     issue = Issue.find(1)
     issue.journals.clear