Index: trunk/app/helpers/application_helper.rb
===================================================================
--- trunk/app/helpers/application_helper.rb	(revision 15916)
+++ trunk/app/helpers/application_helper.rb	(revision 15917)
@@ -197,6 +197,8 @@
       l(:general_text_No)
     when 'Issue'
       object.visible? && html ? link_to_issue(object) : "##{object.id}"
+    when 'Attachment'
+      html ? link_to_attachment(object, :download => true) : object.filename
     when 'CustomValue', 'CustomFieldValue'
       if object.custom_field
         f = object.custom_field.format.formatted_custom_value(self, object, html)
Index: trunk/app/helpers/issues_helper.rb
===================================================================
--- trunk/app/helpers/issues_helper.rb	(revision 15916)
+++ trunk/app/helpers/issues_helper.rb	(revision 15917)
@@ -329,6 +329,7 @@
   def show_detail(detail, no_html=false, options={})
     multiple = false
     show_diff = false
+    no_details = false
 
     case detail.property
     when 'attr'
@@ -364,7 +365,9 @@
       custom_field = detail.custom_field
       if custom_field
         label = custom_field.name
-        if custom_field.format.class.change_as_diff
+        if custom_field.format.class.change_no_details
+          no_details = true
+        elsif custom_field.format.class.change_as_diff
           show_diff = true
         else
           multiple = custom_field.multiple?
@@ -417,7 +420,9 @@
       end
     end
 
-    if show_diff
+    if no_details
+      s = l(:text_journal_changed_no_detail, :label => label).html_safe
+    elsif show_diff
       s = l(:text_journal_changed_no_detail, :label => label)
       unless no_html
         diff_link = link_to 'diff',
Index: trunk/app/models/custom_field.rb
===================================================================
--- trunk/app/models/custom_field.rb	(revision 15916)
+++ trunk/app/models/custom_field.rb	(revision 15917)
@@ -163,6 +163,10 @@
     end
   end
 
+  def set_custom_field_value(custom_field_value, value)
+    format.set_custom_field_value(self, custom_field_value, value)
+  end
+
   def cast_value(value)
     format.cast_value(self, value)
   end
@@ -254,20 +258,23 @@
   # or an empty array if value is a valid value for the custom field
   def validate_custom_value(custom_value)
     value = custom_value.value
-    errs = []
-    if value.is_a?(Array)
-      if !multiple?
-        errs << ::I18n.t('activerecord.errors.messages.invalid')
+    errs = format.validate_custom_value(custom_value)
+
+    unless errs.any?
+      if value.is_a?(Array)
+        if !multiple?
+          errs << ::I18n.t('activerecord.errors.messages.invalid')
+        end
+        if is_required? && value.detect(&:present?).nil?
+          errs << ::I18n.t('activerecord.errors.messages.blank')
+        end
+      else
+        if is_required? && value.blank?
+          errs << ::I18n.t('activerecord.errors.messages.blank')
+        end
       end
-      if is_required? && value.detect(&:present?).nil?
-        errs << ::I18n.t('activerecord.errors.messages.blank')
-      end
-    else
-      if is_required? && value.blank?
-        errs << ::I18n.t('activerecord.errors.messages.blank')
-      end
     end
-    errs += format.validate_custom_value(custom_value)
+
     errs
   end
 
@@ -281,6 +288,10 @@
     validate_field_value(value).empty?
   end
 
+  def after_save_custom_value(custom_value)
+    format.after_save_custom_value(self, custom_value)
+  end
+
   def format_in?(*args)
     args.include?(field_format)
   end
Index: trunk/app/models/custom_field_value.rb
===================================================================
--- trunk/app/models/custom_field_value.rb	(revision 15916)
+++ trunk/app/models/custom_field_value.rb	(revision 15917)
@@ -48,6 +48,10 @@
     value.to_s
   end
 
+  def value=(v)
+    @value = custom_field.set_custom_field_value(self, v)
+  end
+
   def validate_value
     custom_field.validate_custom_value(self).each do |message|
       customized.errors.add(:base, custom_field.name + ' ' + message)
Index: trunk/app/models/custom_value.rb
===================================================================
--- trunk/app/models/custom_value.rb	(revision 15916)
+++ trunk/app/models/custom_value.rb	(revision 15917)
@@ -20,6 +20,8 @@
   belongs_to :customized, :polymorphic => true
   attr_protected :id
 
+  after_save :custom_field_after_save_custom_value
+
   def initialize(attributes=nil, *args)
     super
     if new_record? && custom_field && !attributes.key?(:value)
@@ -40,6 +42,10 @@
     custom_field.visible?
   end
 
+  def attachments_visible?(user)
+    visible? && customized && customized.visible?(user)
+  end
+
   def required?
     custom_field.is_required?
   end
@@ -47,4 +53,10 @@
   def to_s
     value.to_s
   end
+
+  private
+
+  def custom_field_after_save_custom_value
+    custom_field.after_save_custom_value(self)
+  end
 end
Index: trunk/app/views/attachments/_form.html.erb
===================================================================
--- trunk/app/views/attachments/_form.html.erb	(revision 15916)
+++ trunk/app/views/attachments/_form.html.erb	(revision 15917)
@@ -1,30 +1,46 @@
-<span id="attachments_fields">
-<% if defined?(container) && container && container.saved_attachments %>
-  <% container.saved_attachments.each_with_index do |attachment, i| %>
-    <span id="attachments_p<%= i %>">
-      <%= text_field_tag("attachments[p#{i}][filename]", attachment.filename, :class => 'filename') +
-          text_field_tag("attachments[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') +
-          link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
-      <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.token}" %>
-    </span>
+<% attachment_param ||= 'attachments' %>
+<% saved_attachments ||= container.saved_attachments if defined?(container) && container %>
+<% multiple = true unless defined?(multiple) && multiple == false %>
+<% show_add = multiple || saved_attachments.blank? %>
+<% description = (defined?(description) && description == false ? false : true) %>
+<% css_class = (defined?(filedrop) && filedrop == false ? '' : 'filedrop') %>
+
+<span class="attachments_form">
+  <span class="attachments_fields">
+  <% if saved_attachments.present? %>
+    <% saved_attachments.each_with_index do |attachment, i| %>
+      <span id="attachments_p<%= i %>">
+        <%= text_field_tag("#{attachment_param}[p#{i}][filename]", attachment.filename, :class => 'filename') %>
+        <% if attachment.container_id.present? %>
+          <%= link_to l(:label_delete), "#", :onclick => "$(this).closest('.attachments_form').find('.add_attachment').show(); $(this).parent().remove(); return false;", :class => 'icon-only icon-del' %>
+          <%= hidden_field_tag "#{attachment_param}[p#{i}][id]", attachment.id %>
+        <% else %>
+          <%= text_field_tag("#{attachment_param}[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') if description %>
+          <%= link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
+          <%= hidden_field_tag "#{attachment_param}[p#{i}][token]", attachment.token %>
+        <% end %>
+      </span>
+    <% end %>
   <% end %>
-<% end %>
+  </span>
+  <span class="add_attachment" style="<%= show_add ? nil : 'display:none;' %>">
+    <%= file_field_tag "#{attachment_param}[dummy][file]",
+        :id => nil,
+        :class => "file_selector #{css_class}",
+        :multiple => multiple,
+        :onchange => 'addInputFiles(this);',
+        :data => {
+          :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
+          :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
+          :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
+          :upload_path => uploads_path(:format => 'js'),
+          :param => attachment_param,
+          :description => description,
+          :description_placeholder => l(:label_optional_description)
+        } %>
+    (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
+  </span>
 </span>
-<span class="add_attachment">
-<%= file_field_tag 'attachments[dummy][file]',
-      :id => nil,
-      :class => 'file_selector',
-      :multiple => true,
-      :onchange => 'addInputFiles(this);',
-      :data => {
-        :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
-        :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
-        :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
-        :upload_path => uploads_path(:format => 'js'),
-        :description_placeholder => l(:label_optional_description)
-      } %>
-(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
-</span>
 
 <% content_for :header_tags do %>
   <%= javascript_include_tag 'attachments' %>
Index: trunk/app/views/attachments/destroy.js.erb
===================================================================
--- trunk/app/views/attachments/destroy.js.erb	(revision 15916)
+++ trunk/app/views/attachments/destroy.js.erb	(revision 15917)
@@ -1 +1,2 @@
+$('#attachments_<%= j params[:attachment_id] %>').closest('.attachments_form').find('.add_attachment').show();
 $('#attachments_<%= j params[:attachment_id] %>').remove();
Index: trunk/app/views/attachments/upload.js.erb
===================================================================
--- trunk/app/views/attachments/upload.js.erb	(revision 15916)
+++ trunk/app/views/attachments/upload.js.erb	(revision 15917)
@@ -3,7 +3,7 @@
   fileSpan.hide();
   alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
 <% else %>
-$('<input>', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan);
+fileSpan.find('input.token').val('<%= j @attachment.token %>');
 fileSpan.find('a.remove-upload')
   .attr({
     "data-remote": true,
Index: trunk/app/views/custom_fields/_form.html.erb
===================================================================
--- trunk/app/views/custom_fields/_form.html.erb	(revision 15916)
+++ trunk/app/views/custom_fields/_form.html.erb	(revision 15917)
@@ -28,7 +28,9 @@
 when "IssueCustomField" %>
     <p><%= f.check_box :is_required %></p>
     <p><%= f.check_box :is_for_all, :data => {:disables => '#custom_field_project_ids input'} %></p>
+    <% if @custom_field.format.is_filter_supported %>
     <p><%= f.check_box :is_filter %></p>
+    <% end %>
     <% if @custom_field.format.searchable_supported %>
     <p><%= f.check_box :searchable %></p>
     <% end %>
@@ -57,7 +59,9 @@
     <p><%= f.check_box :is_required %></p>
     <p><%= f.check_box :visible %></p>
     <p><%= f.check_box :editable %></p>
+    <% if @custom_field.format.is_filter_supported %>
     <p><%= f.check_box :is_filter %></p>
+    <% end %>
 
 <% when "ProjectCustomField" %>
     <p><%= f.check_box :is_required %></p>
@@ -65,19 +69,27 @@
     <% if @custom_field.format.searchable_supported %>
     <p><%= f.check_box :searchable %></p>
     <% end %>
+    <% if @custom_field.format.is_filter_supported %>
     <p><%= f.check_box :is_filter %></p>
+    <% end %>
 
 <% when "VersionCustomField" %>
     <p><%= f.check_box :is_required %></p>
+    <% if @custom_field.format.is_filter_supported %>
     <p><%= f.check_box :is_filter %></p>
+    <% end %>
 
 <% when "GroupCustomField" %>
     <p><%= f.check_box :is_required %></p>
+    <% if @custom_field.format.is_filter_supported %>
     <p><%= f.check_box :is_filter %></p>
+    <% end %>
 
 <% when "TimeEntryCustomField" %>
     <p><%= f.check_box :is_required %></p>
+    <% if @custom_field.format.is_filter_supported %>
     <p><%= f.check_box :is_filter %></p>
+    <% end %>
 
 <% else %>
     <p><%= f.check_box :is_required %></p>
Index: trunk/app/views/custom_fields/formats/_attachment.html.erb
===================================================================

Property changes on: trunk/app/views/custom_fields/formats/_attachment.html.erb
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: trunk/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb
===================================================================
--- trunk/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb	(revision 15916)
+++ trunk/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb	(revision 15917)
@@ -34,6 +34,7 @@
                                  options.merge(:as => :container, :dependent => :destroy, :inverse_of => :container)
           send :include, Redmine::Acts::Attachable::InstanceMethods
           before_save :attach_saved_attachments
+          after_rollback :detach_saved_attachments
           validate :warn_about_failed_attachments
         end
       end
@@ -90,7 +91,7 @@
               if file = attachment['file']
                 next unless file.size > 0
                 a = Attachment.create(:file => file, :author => author)
-              elsif token = attachment['token']
+              elsif token = attachment['token'].presence
                 a = Attachment.find_by_token(token)
                 unless a
                   @failed_attachment_count += 1
@@ -117,6 +118,14 @@
           end
         end
 
+        def detach_saved_attachments
+          saved_attachments.each do |attachment|
+            # TODO: use #reload instead, after upgrading to Rails 5
+            # (after_rollback is called when running transactional tests in Rails 4)
+            attachment.container = nil
+          end
+        end
+
         def warn_about_failed_attachments
           if @failed_attachment_count && @failed_attachment_count > 0
             errors.add :base, ::I18n.t('warning_attachments_not_saved', count: @failed_attachment_count)
Index: trunk/lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb
===================================================================
--- trunk/lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb	(revision 15916)
+++ trunk/lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb	(revision 15917)
@@ -68,16 +68,7 @@
           custom_field_values.each do |custom_field_value|
             key = custom_field_value.custom_field_id.to_s
             if values.has_key?(key)
-              value = values[key]
-              if value.is_a?(Array)
-                value = value.reject(&:blank?).map(&:to_s).uniq
-                if value.empty?
-                  value << ''
-                end
-              else
-                value = value.to_s
-              end
-              custom_field_value.value = value
+              custom_field_value.value = values[key]
             end
           end
           @custom_field_values_changed = true
@@ -93,11 +84,11 @@
               if values.empty?
                 values << custom_values.build(:customized => self, :custom_field => field)
               end
-              x.value = values.map(&:value)
+              x.instance_variable_set("@value", values.map(&:value))
             else
               cv = custom_values.detect { |v| v.custom_field == field }
               cv ||= custom_values.build(:customized => self, :custom_field => field)
-              x.value = cv.value
+              x.instance_variable_set("@value", cv.value)
             end
             x.value_was = x.value.dup if x.value
             x
Index: trunk/lib/redmine/field_format.rb
===================================================================
--- trunk/lib/redmine/field_format.rb	(revision 15916)
+++ trunk/lib/redmine/field_format.rb	(revision 15917)
@@ -67,6 +67,10 @@
       class_attribute :multiple_supported
       self.multiple_supported = false
 
+      # Set this to true if the format supports filtering on custom values
+      class_attribute :is_filter_supported
+      self.is_filter_supported = true
+
       # Set this to true if the format supports textual search on custom values
       class_attribute :searchable_supported
       self.searchable_supported = false
@@ -87,6 +91,9 @@
       class_attribute :change_as_diff
       self.change_as_diff = false
 
+      class_attribute :change_no_details
+      self.change_no_details = false
+
       def self.add(name)
         self.format_name = name
         Redmine::FieldFormat.add(name, self)
@@ -107,6 +114,19 @@
         "label_#{name}"
       end
 
+      def set_custom_field_value(custom_field, custom_field_value, value)
+        if value.is_a?(Array)
+          value = value.map(&:to_s).reject{|v| v==''}.uniq
+          if value.empty?
+            value << ''
+          end
+        else
+          value = value.to_s
+        end
+
+        value
+      end
+
       def cast_custom_value(custom_value)
         cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
       end
@@ -169,6 +189,7 @@
 
       # Returns the validation error messages for custom_value
       # Should return an empty array if custom_value is valid
+      # custom_value is a CustomFieldValue.
       def validate_custom_value(custom_value)
         values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
         errors = values.map do |value|
@@ -181,6 +202,10 @@
         []
       end
 
+      # CustomValue after_save callback
+      def after_save_custom_value(custom_field, custom_value)
+      end
+
       def formatted_custom_value(view, custom_value, html=false)
         formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
       end
@@ -830,5 +855,109 @@
         scope.sort.collect{|u| [u.to_s, u.id.to_s] }
       end
     end
+
+    class AttachementFormat < Base
+      add 'attachment'
+      self.form_partial = 'custom_fields/formats/attachment'
+      self.is_filter_supported = false
+      self.change_no_details = true
+
+      def set_custom_field_value(custom_field, custom_field_value, value)
+        attachment_present = false
+
+        if value.is_a?(Hash)
+          attachment_present = true
+          value = value.except(:blank)
+
+          if value.values.any? && value.values.all? {|v| v.is_a?(Hash)}
+            value = value.values.first
+          end
+
+          if value.key?(:id)
+            value = set_custom_field_value_by_id(custom_field, custom_field_value, value[:id])
+          elsif value[:token].present?
+            if attachment = Attachment.find_by_token(value[:token])
+              value = attachment.id.to_s
+            else
+              value = ''
+            end
+          elsif value.key?(:file)
+            attachment = Attachment.new(:file => value[:file], :author => User.current)
+            if attachment.save
+              value = attachment.id.to_s
+            else
+              value = ''
+            end
+          else
+            attachment_present = false
+            value = ''
+          end
+        elsif value.is_a?(String)
+          value = set_custom_field_value_by_id(custom_field, custom_field_value, value)
+        end
+        custom_field_value.instance_variable_set "@attachment_present", attachment_present
+
+        value
+      end
+
+      def set_custom_field_value_by_id(custom_field, custom_field_value, id)
+        attachment = Attachment.find_by_id(id)
+        if attachment && attachment.container.is_a?(CustomValue) && attachment.container.customized == custom_field_value.customized
+          id.to_s
+        else
+          ''
+        end
+      end
+      private :set_custom_field_value_by_id
+
+      def cast_single_value(custom_field, value, customized=nil)
+        Attachment.find_by_id(value.to_i) if value.present? && value.respond_to?(:to_i)
+      end
+
+      def validate_custom_value(custom_value)
+        errors = []
+
+        if custom_value.instance_variable_get("@attachment_present") && custom_value.value.blank?
+          errors << ::I18n.t('activerecord.errors.messages.invalid')
+        end
+
+        errors.uniq
+      end
+
+      def after_save_custom_value(custom_field, custom_value)
+        if custom_value.value_changed?
+          if custom_value.value.present?
+            attachment = Attachment.where(:id => custom_value.value.to_s).first
+            if attachment
+              attachment.container = custom_value
+              attachment.save!
+            end
+          end
+          if custom_value.value_was.present?
+            attachment = Attachment.where(:id => custom_value.value_was.to_s).first
+            if attachment
+              attachment.destroy
+            end
+          end
+        end
+      end
+
+      def edit_tag(view, tag_id, tag_name, custom_value, options={})
+        attachment = nil
+        if custom_value.value.present? #&& custom_value.value == custom_value.value_was
+          attachment = Attachment.find_by_id(custom_value.value)
+        end
+
+        view.hidden_field_tag("#{tag_name}[blank]", "") +
+          view.render(:partial => 'attachments/form',
+            :locals => {
+              :attachment_param => tag_name,
+              :multiple => false,
+              :description => false,
+              :saved_attachments => [attachment].compact,
+              :filedrop => false
+            })
+      end
+    end
   end
 end
Index: trunk/public/javascripts/attachments.js
===================================================================
--- trunk/public/javascripts/attachments.js	(revision 15916)
+++ trunk/public/javascripts/attachments.js	(revision 15917)
@@ -2,23 +2,32 @@
    Copyright (C) 2006-2016  Jean-Philippe Lang */
 
 function addFile(inputEl, file, eagerUpload) {
+  var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
+  var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
+  var maxFiles = ($(inputEl).prop('multiple') == true ? 10 : 1);
 
-  if ($('#attachments_fields').children().length < 10) {
-
+  if (attachmentsFields.children().length < maxFiles) {
     var attachmentId = addFile.nextAttachmentId++;
-
     var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
+    var param = $(inputEl).data('param');
+    if (!param) {param = 'attachments'};
 
     fileSpan.append(
-        $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
-        $('<input>', { type: 'text', 'class': 'description', name: 'attachments[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
+        $('<input>', { type: 'text', 'class': 'filename readonly', name: param +'[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
+        $('<input>', { type: 'text', 'class': 'description', name: param + '[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
+        $('<input>', { type: 'hidden', 'class': 'token', name: param + '[' + attachmentId + '][token]'} ),
         $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
-    ).appendTo('#attachments_fields');
+    ).appendTo(attachmentsFields);
 
+    if ($(inputEl).data('description') == 0) {
+      fileSpan.find('input.description').remove();
+    }
+
     if(eagerUpload) {
       ajaxUpload(file, attachmentId, fileSpan, inputEl);
     }
-
+    
+    addAttachment.toggle(attachmentsFields.children().length < maxFiles);
     return attachmentId;
   }
   return null;
@@ -118,11 +127,16 @@
 }
 
 function addInputFiles(inputEl) {
+  var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
+  var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
   var clearedFileInput = $(inputEl).clone().val('');
+  var sizeExceeded = false;
+  var param = $(inputEl).data('param');
+  if (!param) {param = 'attachments'};
 
   if ($.ajaxSettings.xhr().upload && inputEl.files) {
     // upload files using ajax
-    uploadAndAttachFiles(inputEl.files, inputEl);
+    sizeExceeded = uploadAndAttachFiles(inputEl.files, inputEl);
     $(inputEl).remove();
   } else {
     // browser not supporting the file API, upload on form submission
@@ -130,11 +144,11 @@
     var aFilename = inputEl.value.split(/\/|\\/);
     attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
     if (attachmentId) {
-      $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
+      $(inputEl).attr({ name: param + '[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
     }
   }
 
-  clearedFileInput.insertAfter('#attachments_fields');
+  clearedFileInput.prependTo(addAttachment);
 }
 
 function uploadAndAttachFiles(files, inputEl) {
@@ -151,6 +165,7 @@
   } else {
     $.each(files, function() {addFile(inputEl, this, true);});
   }
+  return sizeExceeded;
 }
 
 function handleFileDropEvent(e) {
@@ -159,7 +174,7 @@
   blockEventPropagation(e);
 
   if ($.inArray('Files', e.dataTransfer.types) > -1) {
-    uploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector'));
+    uploadAndAttachFiles(e.dataTransfer.files, $('input:file.filedrop').first());
   }
 }
 
@@ -178,12 +193,12 @@
 
     $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
 
-    $('form div.box').has('input:file').each(function() {
+    $('form div.box:not(.filedroplistner)').has('input:file.filedrop').each(function() {
       $(this).on({
           dragover: dragOverHandler,
           dragleave: dragOutHandler,
           drop: handleFileDropEvent
-      });
+      }).addClass('filedroplistner');
     });
   }
 }
Index: trunk/public/stylesheets/application.css
===================================================================
--- trunk/public/stylesheets/application.css	(revision 15916)
+++ trunk/public/stylesheets/application.css	(revision 15917)
@@ -600,7 +600,7 @@
   margin: 0;
   padding: 3px 0 3px 0;
   padding-left: 180px; /* width of left column containing the label elements */
-  min-height: 1.8em;
+  line-height: 2em;
   clear:left;
 }
 
@@ -626,13 +626,16 @@
   width: 270px;
 }
 
+label.block {
+  display: block;
+  width: auto !important;
+}
+
 .tabular label.block{
   font-weight: normal;
   margin-left: 0px !important;
   text-align: left;
   float: none;
-  display: block;
-  width: auto !important;
 }
 
 .tabular label.inline{
@@ -687,13 +690,14 @@
 .check_box_group.bool_cf {border:0; background:inherit;}
 .check_box_group.bool_cf label {display: inline;}
 
-#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; }
+.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;}
+.tabular input.filename {max-width:75% !important;}
+.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;}
@@ -1160,7 +1164,7 @@
   padding-top: 0;
   padding-bottom: 0;
   font-size: 8px;
-  vertical-align: text-bottom;
+  vertical-align: middle;
 }
 .icon-only::after {
   content: "&nbsp;";
Index: trunk/test/integration/lib/redmine/field_format/attachment_format_test.rb
===================================================================
--- trunk/test/integration/lib/redmine/field_format/attachment_format_test.rb	(nonexistent)
+++ trunk/test/integration/lib/redmine/field_format/attachment_format_test.rb	(revision 15917)
@@ -0,0 +1,156 @@
+# Redmine - project management software
+# Copyright (C) 2006-2016  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 AttachmentFieldFormatTest < Redmine::IntegrationTest
+  fixtures :projects,
+           :users, :email_addresses,
+           :roles,
+           :members,
+           :member_roles,
+           :trackers,
+           :projects_trackers,
+           :enabled_modules,
+           :issue_statuses,
+           :issues,
+           :enumerations,
+           :custom_fields,
+           :custom_values,
+           :custom_fields_trackers,
+           :attachments
+
+  def setup
+    set_tmp_attachments_directory
+    @field = IssueCustomField.generate!(:name => "File", :field_format => "attachment")
+    log_user "jsmith", "jsmith"
+  end
+
+  def test_new_should_include_inputs
+    get '/projects/ecookbook/issues/new'
+    assert_response :success
+
+    assert_select '[name^=?]', "issue[custom_field_values][#{@field.id}]", 2
+    assert_select 'input[name=?][type=hidden][value=""]', "issue[custom_field_values][#{@field.id}][blank]"
+  end
+
+  def test_create_with_attachment
+    issue = new_record(Issue) do
+      assert_difference 'Attachment.count' do
+        post '/projects/ecookbook/issues', {
+            :issue => {
+              :subject => "Subject",
+              :custom_field_values => {
+                @field.id => {
+                  'blank' => '',
+                  '1' => {:file => uploaded_test_file("testfile.txt", "text/plain")}
+                }
+              }
+            }
+          }
+        assert_response 302
+      end
+    end
+
+    custom_value = issue.custom_value_for(@field)
+    assert custom_value
+    assert custom_value.value.present?
+
+    attachment = Attachment.find_by_id(custom_value.value)
+    assert attachment
+    assert_equal custom_value, attachment.container
+
+    follow_redirect!
+    assert_response :success
+
+    # link to the attachment
+    link = css_select(".cf_#{@field.id} .value a")
+    assert_equal 1, link.size
+    assert_equal "testfile.txt", link.text
+
+    # download the attachment
+    get link.attr('href')
+    assert_response :success
+    assert_equal "text/plain", response.content_type
+  end
+
+  def test_create_without_attachment
+    issue = new_record(Issue) do
+      assert_no_difference 'Attachment.count' do
+        post '/projects/ecookbook/issues', {
+            :issue => {
+              :subject => "Subject",
+              :custom_field_values => {
+                @field.id => {:blank => ''}
+              }
+            }
+          }
+        assert_response 302
+      end
+    end
+
+    custom_value = issue.custom_value_for(@field)
+    assert custom_value
+    assert custom_value.value.blank?
+
+    follow_redirect!
+    assert_response :success
+
+    # no links to the attachment
+    assert_select ".cf_#{@field.id} .value a", 0
+  end
+
+  def test_failure_on_create_should_preserve_attachment
+    attachment = new_record(Attachment) do
+      assert_no_difference 'Issue.count' do
+        post '/projects/ecookbook/issues', {
+            :issue => {
+              :subject => "",
+              :custom_field_values => {
+                @field.id => {:file => uploaded_test_file("testfile.txt", "text/plain")}
+              }
+            }
+          }
+        assert_response :success
+        assert_select_error /Subject cannot be blank/
+      end
+    end
+
+    assert_nil attachment.container_id
+    assert_select 'input[name=?][value=?][type=hidden]', "issue[custom_field_values][#{@field.id}][p0][token]", attachment.token
+    assert_select 'input[name=?][value=?]', "issue[custom_field_values][#{@field.id}][p0][filename]", 'testfile.txt'
+
+    issue = new_record(Issue) do
+      assert_no_difference 'Attachment.count' do
+        post '/projects/ecookbook/issues', {
+            :issue => {
+              :subject => "Subject",
+              :custom_field_values => {
+                @field.id => {:token => attachment.token}
+              }
+            }
+          }
+        assert_response 302
+      end
+    end
+
+    custom_value = issue.custom_value_for(@field)
+    assert custom_value
+    assert_equal attachment.id.to_s, custom_value.value
+    assert_equal custom_value, attachment.reload.container
+  end
+end

Property changes on: trunk/test/integration/lib/redmine/field_format/attachment_format_test.rb
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: trunk/test/unit/lib/redmine/field_format/attachment_format_test.rb
===================================================================
--- trunk/test/unit/lib/redmine/field_format/attachment_format_test.rb	(nonexistent)
+++ trunk/test/unit/lib/redmine/field_format/attachment_format_test.rb	(revision 15917)
@@ -0,0 +1,163 @@
+# Redmine - project management software
+# Copyright (C) 2006-2016  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__)
+require 'redmine/field_format'
+
+class Redmine::AttachmentFieldFormatTest < ActionView::TestCase
+  include ApplicationHelper
+  include Redmine::I18n
+
+  fixtures :users
+
+  def setup
+    set_language_if_valid 'en'
+    set_tmp_attachments_directory
+  end
+
+  def test_should_accept_a_hash_with_upload_on_create
+    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
+    group = Group.new(:name => 'Group')
+    attachment = nil
+
+    custom_value = new_record(CustomValue) do
+      attachment = new_record(Attachment) do
+        group.custom_field_values = {field.id => {:file => mock_file}}
+        assert group.save
+      end
+    end
+
+    assert_equal 'a_file.png', attachment.filename
+    assert_equal custom_value, attachment.container
+    assert_equal field, attachment.container.custom_field
+    assert_equal group, attachment.container.customized
+  end
+
+  def test_should_accept_a_hash_with_no_upload_on_create
+    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
+    group = Group.new(:name => 'Group')
+    attachment = nil
+
+    custom_value = new_record(CustomValue) do
+      assert_no_difference 'Attachment.count' do
+        group.custom_field_values = {field.id => {}}
+        assert group.save
+      end
+    end
+
+    assert_equal '', custom_value.value
+  end
+
+  def test_should_not_validate_with_invalid_upload_on_create
+    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
+    group = Group.new(:name => 'Group')
+
+    with_settings :attachment_max_size => 0 do
+      assert_no_difference 'CustomValue.count' do
+        assert_no_difference 'Attachment.count' do
+          group.custom_field_values = {field.id => {:file => mock_file}}
+          assert_equal false, group.save
+        end
+      end
+    end
+  end
+
+  def test_should_accept_a_hash_with_token_on_create
+    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
+    group = Group.new(:name => 'Group')
+
+    attachment = Attachment.create!(:file => mock_file, :author => User.find(2))
+    assert_nil attachment.container
+
+    custom_value = new_record(CustomValue) do
+      assert_no_difference 'Attachment.count' do
+        group.custom_field_values = {field.id => {:token => attachment.token}}
+        assert group.save
+      end
+    end
+
+    attachment.reload
+    assert_equal custom_value, attachment.container
+    assert_equal field, attachment.container.custom_field
+    assert_equal group, attachment.container.customized
+  end
+
+  def test_should_not_validate_with_invalid_token_on_create
+    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
+    group = Group.new(:name => 'Group')
+
+    assert_no_difference 'CustomValue.count' do
+      assert_no_difference 'Attachment.count' do
+        group.custom_field_values = {field.id => {:token => "123.0123456789abcdef"}}
+        assert_equal false, group.save
+      end
+    end
+  end
+
+  def test_should_replace_attachment_on_update
+    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
+    group = Group.new(:name => 'Group')
+    attachment = nil
+    custom_value = new_record(CustomValue) do
+      attachment = new_record(Attachment) do
+        group.custom_field_values = {field.id => {:file => mock_file}}
+        assert group.save
+      end
+    end
+    group.reload
+
+    assert_no_difference 'Attachment.count' do
+      assert_no_difference 'CustomValue.count' do
+        group.custom_field_values = {field.id => {:file => mock_file}}
+        assert group.save
+      end
+    end
+
+    assert !Attachment.exists?(attachment.id)
+    assert CustomValue.exists?(custom_value.id)
+
+    new_attachment = Attachment.order(:id => :desc).first
+    custom_value.reload
+    assert_equal custom_value, new_attachment.container
+  end
+
+  def test_should_delete_attachment_on_update
+    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
+    group = Group.new(:name => 'Group')
+    attachment = nil
+    custom_value = new_record(CustomValue) do
+      attachment = new_record(Attachment) do
+        group.custom_field_values = {field.id => {:file => mock_file}}
+        assert group.save
+      end
+    end
+    group.reload
+
+    assert_difference 'Attachment.count', -1 do
+      assert_no_difference 'CustomValue.count' do
+        group.custom_field_values = {field.id => {}}
+        assert group.save
+      end
+    end
+
+    assert !Attachment.exists?(attachment.id)
+    assert CustomValue.exists?(custom_value.id)
+
+    custom_value.reload
+    assert_equal '', custom_value.value
+  end
+end

Property changes on: trunk/test/unit/lib/redmine/field_format/attachment_format_test.rb
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
