Project

General

Profile

« Previous | Next » 

Revision 15917

Adds file custom field format (#6719).

View differences:

trunk/app/helpers/application_helper.rb
197 197
      l(:general_text_No)
198 198
    when 'Issue'
199 199
      object.visible? && html ? link_to_issue(object) : "##{object.id}"
200
    when 'Attachment'
201
      html ? link_to_attachment(object, :download => true) : object.filename
200 202
    when 'CustomValue', 'CustomFieldValue'
201 203
      if object.custom_field
202 204
        f = object.custom_field.format.formatted_custom_value(self, object, html)
trunk/app/helpers/issues_helper.rb
329 329
  def show_detail(detail, no_html=false, options={})
330 330
    multiple = false
331 331
    show_diff = false
332
    no_details = false
332 333

  
333 334
    case detail.property
334 335
    when 'attr'
......
364 365
      custom_field = detail.custom_field
365 366
      if custom_field
366 367
        label = custom_field.name
367
        if custom_field.format.class.change_as_diff
368
        if custom_field.format.class.change_no_details
369
          no_details = true
370
        elsif custom_field.format.class.change_as_diff
368 371
          show_diff = true
369 372
        else
370 373
          multiple = custom_field.multiple?
......
417 420
      end
418 421
    end
419 422

  
420
    if show_diff
423
    if no_details
424
      s = l(:text_journal_changed_no_detail, :label => label).html_safe
425
    elsif show_diff
421 426
      s = l(:text_journal_changed_no_detail, :label => label)
422 427
      unless no_html
423 428
        diff_link = link_to 'diff',
trunk/app/models/custom_field.rb
163 163
    end
164 164
  end
165 165

  
166
  def set_custom_field_value(custom_field_value, value)
167
    format.set_custom_field_value(self, custom_field_value, value)
168
  end
169

  
166 170
  def cast_value(value)
167 171
    format.cast_value(self, value)
168 172
  end
......
254 258
  # or an empty array if value is a valid value for the custom field
255 259
  def validate_custom_value(custom_value)
256 260
    value = custom_value.value
257
    errs = []
258
    if value.is_a?(Array)
259
      if !multiple?
260
        errs << ::I18n.t('activerecord.errors.messages.invalid')
261
    errs = format.validate_custom_value(custom_value)
262

  
263
    unless errs.any?
264
      if value.is_a?(Array)
265
        if !multiple?
266
          errs << ::I18n.t('activerecord.errors.messages.invalid')
267
        end
268
        if is_required? && value.detect(&:present?).nil?
269
          errs << ::I18n.t('activerecord.errors.messages.blank')
270
        end
271
      else
272
        if is_required? && value.blank?
273
          errs << ::I18n.t('activerecord.errors.messages.blank')
274
        end
261 275
      end
262
      if is_required? && value.detect(&:present?).nil?
263
        errs << ::I18n.t('activerecord.errors.messages.blank')
264
      end
265
    else
266
      if is_required? && value.blank?
267
        errs << ::I18n.t('activerecord.errors.messages.blank')
268
      end
269 276
    end
270
    errs += format.validate_custom_value(custom_value)
277

  
271 278
    errs
272 279
  end
273 280

  
......
281 288
    validate_field_value(value).empty?
282 289
  end
283 290

  
291
  def after_save_custom_value(custom_value)
292
    format.after_save_custom_value(self, custom_value)
293
  end
294

  
284 295
  def format_in?(*args)
285 296
    args.include?(field_format)
286 297
  end
trunk/app/models/custom_field_value.rb
48 48
    value.to_s
49 49
  end
50 50

  
51
  def value=(v)
52
    @value = custom_field.set_custom_field_value(self, v)
53
  end
54

  
51 55
  def validate_value
52 56
    custom_field.validate_custom_value(self).each do |message|
53 57
      customized.errors.add(:base, custom_field.name + ' ' + message)
trunk/app/models/custom_value.rb
20 20
  belongs_to :customized, :polymorphic => true
21 21
  attr_protected :id
22 22

  
23
  after_save :custom_field_after_save_custom_value
24

  
23 25
  def initialize(attributes=nil, *args)
24 26
    super
25 27
    if new_record? && custom_field && !attributes.key?(:value)
......
40 42
    custom_field.visible?
41 43
  end
42 44

  
45
  def attachments_visible?(user)
46
    visible? && customized && customized.visible?(user)
47
  end
48

  
43 49
  def required?
44 50
    custom_field.is_required?
45 51
  end
......
47 53
  def to_s
48 54
    value.to_s
49 55
  end
56

  
57
  private
58

  
59
  def custom_field_after_save_custom_value
60
    custom_field.after_save_custom_value(self)
61
  end
50 62
end
trunk/app/views/attachments/_form.html.erb
1
<span id="attachments_fields">
2
<% if defined?(container) && container && container.saved_attachments %>
3
  <% container.saved_attachments.each_with_index do |attachment, i| %>
4
    <span id="attachments_p<%= i %>">
5
      <%= text_field_tag("attachments[p#{i}][filename]", attachment.filename, :class => 'filename') +
6
          text_field_tag("attachments[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') +
7
          link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
8
      <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.token}" %>
9
    </span>
1
<% attachment_param ||= 'attachments' %>
2
<% saved_attachments ||= container.saved_attachments if defined?(container) && container %>
3
<% multiple = true unless defined?(multiple) && multiple == false %>
4
<% show_add = multiple || saved_attachments.blank? %>
5
<% description = (defined?(description) && description == false ? false : true) %>
6
<% css_class = (defined?(filedrop) && filedrop == false ? '' : 'filedrop') %>
7

  
8
<span class="attachments_form">
9
  <span class="attachments_fields">
10
  <% if saved_attachments.present? %>
11
    <% saved_attachments.each_with_index do |attachment, i| %>
12
      <span id="attachments_p<%= i %>">
13
        <%= text_field_tag("#{attachment_param}[p#{i}][filename]", attachment.filename, :class => 'filename') %>
14
        <% if attachment.container_id.present? %>
15
          <%= link_to l(:label_delete), "#", :onclick => "$(this).closest('.attachments_form').find('.add_attachment').show(); $(this).parent().remove(); return false;", :class => 'icon-only icon-del' %>
16
          <%= hidden_field_tag "#{attachment_param}[p#{i}][id]", attachment.id %>
17
        <% else %>
18
          <%= text_field_tag("#{attachment_param}[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') if description %>
19
          <%= link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
20
          <%= hidden_field_tag "#{attachment_param}[p#{i}][token]", attachment.token %>
21
        <% end %>
22
      </span>
23
    <% end %>
10 24
  <% end %>
11
<% end %>
25
  </span>
26
  <span class="add_attachment" style="<%= show_add ? nil : 'display:none;' %>">
27
    <%= file_field_tag "#{attachment_param}[dummy][file]",
28
        :id => nil,
29
        :class => "file_selector #{css_class}",
30
        :multiple => multiple,
31
        :onchange => 'addInputFiles(this);',
32
        :data => {
33
          :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
34
          :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
35
          :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
36
          :upload_path => uploads_path(:format => 'js'),
37
          :param => attachment_param,
38
          :description => description,
39
          :description_placeholder => l(:label_optional_description)
40
        } %>
41
    (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
42
  </span>
12 43
</span>
13
<span class="add_attachment">
14
<%= file_field_tag 'attachments[dummy][file]',
15
      :id => nil,
16
      :class => 'file_selector',
17
      :multiple => true,
18
      :onchange => 'addInputFiles(this);',
19
      :data => {
20
        :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
21
        :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
22
        :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
23
        :upload_path => uploads_path(:format => 'js'),
24
        :description_placeholder => l(:label_optional_description)
25
      } %>
26
(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
27
</span>
28 44

  
29 45
<% content_for :header_tags do %>
30 46
  <%= javascript_include_tag 'attachments' %>
trunk/app/views/attachments/destroy.js.erb
1
$('#attachments_<%= j params[:attachment_id] %>').closest('.attachments_form').find('.add_attachment').show();
1 2
$('#attachments_<%= j params[:attachment_id] %>').remove();
trunk/app/views/attachments/upload.js.erb
3 3
  fileSpan.hide();
4 4
  alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
5 5
<% else %>
6
$('<input>', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan);
6
fileSpan.find('input.token').val('<%= j @attachment.token %>');
7 7
fileSpan.find('a.remove-upload')
8 8
  .attr({
9 9
    "data-remote": true,
trunk/app/views/custom_fields/_form.html.erb
28 28
when "IssueCustomField" %>
29 29
    <p><%= f.check_box :is_required %></p>
30 30
    <p><%= f.check_box :is_for_all, :data => {:disables => '#custom_field_project_ids input'} %></p>
31
    <% if @custom_field.format.is_filter_supported %>
31 32
    <p><%= f.check_box :is_filter %></p>
33
    <% end %>
32 34
    <% if @custom_field.format.searchable_supported %>
33 35
    <p><%= f.check_box :searchable %></p>
34 36
    <% end %>
......
57 59
    <p><%= f.check_box :is_required %></p>
58 60
    <p><%= f.check_box :visible %></p>
59 61
    <p><%= f.check_box :editable %></p>
62
    <% if @custom_field.format.is_filter_supported %>
60 63
    <p><%= f.check_box :is_filter %></p>
64
    <% end %>
61 65

  
62 66
<% when "ProjectCustomField" %>
63 67
    <p><%= f.check_box :is_required %></p>
......
65 69
    <% if @custom_field.format.searchable_supported %>
66 70
    <p><%= f.check_box :searchable %></p>
67 71
    <% end %>
72
    <% if @custom_field.format.is_filter_supported %>
68 73
    <p><%= f.check_box :is_filter %></p>
74
    <% end %>
69 75

  
70 76
<% when "VersionCustomField" %>
71 77
    <p><%= f.check_box :is_required %></p>
78
    <% if @custom_field.format.is_filter_supported %>
72 79
    <p><%= f.check_box :is_filter %></p>
80
    <% end %>
73 81

  
74 82
<% when "GroupCustomField" %>
75 83
    <p><%= f.check_box :is_required %></p>
84
    <% if @custom_field.format.is_filter_supported %>
76 85
    <p><%= f.check_box :is_filter %></p>
86
    <% end %>
77 87

  
78 88
<% when "TimeEntryCustomField" %>
79 89
    <p><%= f.check_box :is_required %></p>
90
    <% if @custom_field.format.is_filter_supported %>
80 91
    <p><%= f.check_box :is_filter %></p>
92
    <% end %>
81 93

  
82 94
<% else %>
83 95
    <p><%= f.check_box :is_required %></p>
trunk/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb
34 34
                                 options.merge(:as => :container, :dependent => :destroy, :inverse_of => :container)
35 35
          send :include, Redmine::Acts::Attachable::InstanceMethods
36 36
          before_save :attach_saved_attachments
37
          after_rollback :detach_saved_attachments
37 38
          validate :warn_about_failed_attachments
38 39
        end
39 40
      end
......
90 91
              if file = attachment['file']
91 92
                next unless file.size > 0
92 93
                a = Attachment.create(:file => file, :author => author)
93
              elsif token = attachment['token']
94
              elsif token = attachment['token'].presence
94 95
                a = Attachment.find_by_token(token)
95 96
                unless a
96 97
                  @failed_attachment_count += 1
......
117 118
          end
118 119
        end
119 120

  
121
        def detach_saved_attachments
122
          saved_attachments.each do |attachment|
123
            # TODO: use #reload instead, after upgrading to Rails 5
124
            # (after_rollback is called when running transactional tests in Rails 4)
125
            attachment.container = nil
126
          end
127
        end
128

  
120 129
        def warn_about_failed_attachments
121 130
          if @failed_attachment_count && @failed_attachment_count > 0
122 131
            errors.add :base, ::I18n.t('warning_attachments_not_saved', count: @failed_attachment_count)
trunk/lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb
68 68
          custom_field_values.each do |custom_field_value|
69 69
            key = custom_field_value.custom_field_id.to_s
70 70
            if values.has_key?(key)
71
              value = values[key]
72
              if value.is_a?(Array)
73
                value = value.reject(&:blank?).map(&:to_s).uniq
74
                if value.empty?
75
                  value << ''
76
                end
77
              else
78
                value = value.to_s
79
              end
80
              custom_field_value.value = value
71
              custom_field_value.value = values[key]
81 72
            end
82 73
          end
83 74
          @custom_field_values_changed = true
......
93 84
              if values.empty?
94 85
                values << custom_values.build(:customized => self, :custom_field => field)
95 86
              end
96
              x.value = values.map(&:value)
87
              x.instance_variable_set("@value", values.map(&:value))
97 88
            else
98 89
              cv = custom_values.detect { |v| v.custom_field == field }
99 90
              cv ||= custom_values.build(:customized => self, :custom_field => field)
100
              x.value = cv.value
91
              x.instance_variable_set("@value", cv.value)
101 92
            end
102 93
            x.value_was = x.value.dup if x.value
103 94
            x
trunk/lib/redmine/field_format.rb
67 67
      class_attribute :multiple_supported
68 68
      self.multiple_supported = false
69 69

  
70
      # Set this to true if the format supports filtering on custom values
71
      class_attribute :is_filter_supported
72
      self.is_filter_supported = true
73

  
70 74
      # Set this to true if the format supports textual search on custom values
71 75
      class_attribute :searchable_supported
72 76
      self.searchable_supported = false
......
87 91
      class_attribute :change_as_diff
88 92
      self.change_as_diff = false
89 93

  
94
      class_attribute :change_no_details
95
      self.change_no_details = false
96

  
90 97
      def self.add(name)
91 98
        self.format_name = name
92 99
        Redmine::FieldFormat.add(name, self)
......
107 114
        "label_#{name}"
108 115
      end
109 116

  
117
      def set_custom_field_value(custom_field, custom_field_value, value)
118
        if value.is_a?(Array)
119
          value = value.map(&:to_s).reject{|v| v==''}.uniq
120
          if value.empty?
121
            value << ''
122
          end
123
        else
124
          value = value.to_s
125
        end
126

  
127
        value
128
      end
129

  
110 130
      def cast_custom_value(custom_value)
111 131
        cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
112 132
      end
......
169 189

  
170 190
      # Returns the validation error messages for custom_value
171 191
      # Should return an empty array if custom_value is valid
192
      # custom_value is a CustomFieldValue.
172 193
      def validate_custom_value(custom_value)
173 194
        values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
174 195
        errors = values.map do |value|
......
181 202
        []
182 203
      end
183 204

  
205
      # CustomValue after_save callback
206
      def after_save_custom_value(custom_field, custom_value)
207
      end
208

  
184 209
      def formatted_custom_value(view, custom_value, html=false)
185 210
        formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
186 211
      end
......
830 855
        scope.sort.collect{|u| [u.to_s, u.id.to_s] }
831 856
      end
832 857
    end
858

  
859
    class AttachementFormat < Base
860
      add 'attachment'
861
      self.form_partial = 'custom_fields/formats/attachment'
862
      self.is_filter_supported = false
863
      self.change_no_details = true
864

  
865
      def set_custom_field_value(custom_field, custom_field_value, value)
866
        attachment_present = false
867

  
868
        if value.is_a?(Hash)
869
          attachment_present = true
870
          value = value.except(:blank)
871

  
872
          if value.values.any? && value.values.all? {|v| v.is_a?(Hash)}
873
            value = value.values.first
874
          end
875

  
876
          if value.key?(:id)
877
            value = set_custom_field_value_by_id(custom_field, custom_field_value, value[:id])
878
          elsif value[:token].present?
879
            if attachment = Attachment.find_by_token(value[:token])
880
              value = attachment.id.to_s
881
            else
882
              value = ''
883
            end
884
          elsif value.key?(:file)
885
            attachment = Attachment.new(:file => value[:file], :author => User.current)
886
            if attachment.save
887
              value = attachment.id.to_s
888
            else
889
              value = ''
890
            end
891
          else
892
            attachment_present = false
893
            value = ''
894
          end
895
        elsif value.is_a?(String)
896
          value = set_custom_field_value_by_id(custom_field, custom_field_value, value)
897
        end
898
        custom_field_value.instance_variable_set "@attachment_present", attachment_present
899

  
900
        value
901
      end
902

  
903
      def set_custom_field_value_by_id(custom_field, custom_field_value, id)
904
        attachment = Attachment.find_by_id(id)
905
        if attachment && attachment.container.is_a?(CustomValue) && attachment.container.customized == custom_field_value.customized
906
          id.to_s
907
        else
908
          ''
909
        end
910
      end
911
      private :set_custom_field_value_by_id
912

  
913
      def cast_single_value(custom_field, value, customized=nil)
914
        Attachment.find_by_id(value.to_i) if value.present? && value.respond_to?(:to_i)
915
      end
916

  
917
      def validate_custom_value(custom_value)
918
        errors = []
919

  
920
        if custom_value.instance_variable_get("@attachment_present") && custom_value.value.blank?
921
          errors << ::I18n.t('activerecord.errors.messages.invalid')
922
        end
923

  
924
        errors.uniq
925
      end
926

  
927
      def after_save_custom_value(custom_field, custom_value)
928
        if custom_value.value_changed?
929
          if custom_value.value.present?
930
            attachment = Attachment.where(:id => custom_value.value.to_s).first
931
            if attachment
932
              attachment.container = custom_value
933
              attachment.save!
934
            end
935
          end
936
          if custom_value.value_was.present?
937
            attachment = Attachment.where(:id => custom_value.value_was.to_s).first
938
            if attachment
939
              attachment.destroy
940
            end
941
          end
942
        end
943
      end
944

  
945
      def edit_tag(view, tag_id, tag_name, custom_value, options={})
946
        attachment = nil
947
        if custom_value.value.present? #&& custom_value.value == custom_value.value_was
948
          attachment = Attachment.find_by_id(custom_value.value)
949
        end
950

  
951
        view.hidden_field_tag("#{tag_name}[blank]", "") +
952
          view.render(:partial => 'attachments/form',
953
            :locals => {
954
              :attachment_param => tag_name,
955
              :multiple => false,
956
              :description => false,
957
              :saved_attachments => [attachment].compact,
958
              :filedrop => false
959
            })
960
      end
961
    end
833 962
  end
834 963
end
trunk/public/javascripts/attachments.js
2 2
   Copyright (C) 2006-2016  Jean-Philippe Lang */
3 3

  
4 4
function addFile(inputEl, file, eagerUpload) {
5
  var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
6
  var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
7
  var maxFiles = ($(inputEl).prop('multiple') == true ? 10 : 1);
5 8

  
6
  if ($('#attachments_fields').children().length < 10) {
7

  
9
  if (attachmentsFields.children().length < maxFiles) {
8 10
    var attachmentId = addFile.nextAttachmentId++;
9

  
10 11
    var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
12
    var param = $(inputEl).data('param');
13
    if (!param) {param = 'attachments'};
11 14

  
12 15
    fileSpan.append(
13
        $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
14
        $('<input>', { type: 'text', 'class': 'description', name: 'attachments[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
16
        $('<input>', { type: 'text', 'class': 'filename readonly', name: param +'[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
17
        $('<input>', { type: 'text', 'class': 'description', name: param + '[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
18
        $('<input>', { type: 'hidden', 'class': 'token', name: param + '[' + attachmentId + '][token]'} ),
15 19
        $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
16
    ).appendTo('#attachments_fields');
20
    ).appendTo(attachmentsFields);
17 21

  
22
    if ($(inputEl).data('description') == 0) {
23
      fileSpan.find('input.description').remove();
24
    }
25

  
18 26
    if(eagerUpload) {
19 27
      ajaxUpload(file, attachmentId, fileSpan, inputEl);
20 28
    }
21

  
29
    
30
    addAttachment.toggle(attachmentsFields.children().length < maxFiles);
22 31
    return attachmentId;
23 32
  }
24 33
  return null;
......
118 127
}
119 128

  
120 129
function addInputFiles(inputEl) {
130
  var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
131
  var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
121 132
  var clearedFileInput = $(inputEl).clone().val('');
133
  var sizeExceeded = false;
134
  var param = $(inputEl).data('param');
135
  if (!param) {param = 'attachments'};
122 136

  
123 137
  if ($.ajaxSettings.xhr().upload && inputEl.files) {
124 138
    // upload files using ajax
125
    uploadAndAttachFiles(inputEl.files, inputEl);
139
    sizeExceeded = uploadAndAttachFiles(inputEl.files, inputEl);
126 140
    $(inputEl).remove();
127 141
  } else {
128 142
    // browser not supporting the file API, upload on form submission
......
130 144
    var aFilename = inputEl.value.split(/\/|\\/);
131 145
    attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
132 146
    if (attachmentId) {
133
      $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
147
      $(inputEl).attr({ name: param + '[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
134 148
    }
135 149
  }
136 150

  
137
  clearedFileInput.insertAfter('#attachments_fields');
151
  clearedFileInput.prependTo(addAttachment);
138 152
}
139 153

  
140 154
function uploadAndAttachFiles(files, inputEl) {
......
151 165
  } else {
152 166
    $.each(files, function() {addFile(inputEl, this, true);});
153 167
  }
168
  return sizeExceeded;
154 169
}
155 170

  
156 171
function handleFileDropEvent(e) {
......
159 174
  blockEventPropagation(e);
160 175

  
161 176
  if ($.inArray('Files', e.dataTransfer.types) > -1) {
162
    uploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector'));
177
    uploadAndAttachFiles(e.dataTransfer.files, $('input:file.filedrop').first());
163 178
  }
164 179
}
165 180

  
......
178 193

  
179 194
    $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
180 195

  
181
    $('form div.box').has('input:file').each(function() {
196
    $('form div.box:not(.filedroplistner)').has('input:file.filedrop').each(function() {
182 197
      $(this).on({
183 198
          dragover: dragOverHandler,
184 199
          dragleave: dragOutHandler,
185 200
          drop: handleFileDropEvent
186
      });
201
      }).addClass('filedroplistner');
187 202
    });
188 203
  }
189 204
}
trunk/public/stylesheets/application.css
600 600
  margin: 0;
601 601
  padding: 3px 0 3px 0;
602 602
  padding-left: 180px; /* width of left column containing the label elements */
603
  min-height: 1.8em;
603
  line-height: 2em;
604 604
  clear:left;
605 605
}
606 606

  
......
626 626
  width: 270px;
627 627
}
628 628

  
629
label.block {
630
  display: block;
631
  width: auto !important;
632
}
633

  
629 634
.tabular label.block{
630 635
  font-weight: normal;
631 636
  margin-left: 0px !important;
632 637
  text-align: left;
633 638
  float: none;
634
  display: block;
635
  width: auto !important;
636 639
}
637 640

  
638 641
.tabular label.inline{
......
687 690
.check_box_group.bool_cf {border:0; background:inherit;}
688 691
.check_box_group.bool_cf label {display: inline;}
689 692

  
690
#attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
691
#attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
692
#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;}
693
#attachments_fields input.filename {height:1.8em;}
694
#attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
695
#attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
696
#attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
693
.attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
694
.attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
695
.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;}
696
.tabular input.filename {max-width:75% !important;}
697
.attachments_fields input.filename {height:1.8em;}
698
.attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
699
.attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
700
.attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
697 701
a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
698 702
a.remove-upload:hover {text-decoration:none !important;}
699 703
.existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
......
1160 1164
  padding-top: 0;
1161 1165
  padding-bottom: 0;
1162 1166
  font-size: 8px;
1163
  vertical-align: text-bottom;
1167
  vertical-align: middle;
1164 1168
}
1165 1169
.icon-only::after {
1166 1170
  content: "&nbsp;";
trunk/test/integration/lib/redmine/field_format/attachment_format_test.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2016  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require File.expand_path('../../../../../test_helper', __FILE__)
19

  
20
class AttachmentFieldFormatTest < Redmine::IntegrationTest
21
  fixtures :projects,
22
           :users, :email_addresses,
23
           :roles,
24
           :members,
25
           :member_roles,
26
           :trackers,
27
           :projects_trackers,
28
           :enabled_modules,
29
           :issue_statuses,
30
           :issues,
31
           :enumerations,
32
           :custom_fields,
33
           :custom_values,
34
           :custom_fields_trackers,
35
           :attachments
36

  
37
  def setup
38
    set_tmp_attachments_directory
39
    @field = IssueCustomField.generate!(:name => "File", :field_format => "attachment")
40
    log_user "jsmith", "jsmith"
41
  end
42

  
43
  def test_new_should_include_inputs
44
    get '/projects/ecookbook/issues/new'
45
    assert_response :success
46

  
47
    assert_select '[name^=?]', "issue[custom_field_values][#{@field.id}]", 2
48
    assert_select 'input[name=?][type=hidden][value=""]', "issue[custom_field_values][#{@field.id}][blank]"
49
  end
50

  
51
  def test_create_with_attachment
52
    issue = new_record(Issue) do
53
      assert_difference 'Attachment.count' do
54
        post '/projects/ecookbook/issues', {
55
            :issue => {
56
              :subject => "Subject",
57
              :custom_field_values => {
58
                @field.id => {
59
                  'blank' => '',
60
                  '1' => {:file => uploaded_test_file("testfile.txt", "text/plain")}
61
                }
62
              }
63
            }
64
          }
65
        assert_response 302
66
      end
67
    end
68

  
69
    custom_value = issue.custom_value_for(@field)
70
    assert custom_value
71
    assert custom_value.value.present?
72

  
73
    attachment = Attachment.find_by_id(custom_value.value)
74
    assert attachment
75
    assert_equal custom_value, attachment.container
76

  
77
    follow_redirect!
78
    assert_response :success
79

  
80
    # link to the attachment
81
    link = css_select(".cf_#{@field.id} .value a")
82
    assert_equal 1, link.size
83
    assert_equal "testfile.txt", link.text
84

  
85
    # download the attachment
86
    get link.attr('href')
87
    assert_response :success
88
    assert_equal "text/plain", response.content_type
89
  end
90

  
91
  def test_create_without_attachment
92
    issue = new_record(Issue) do
93
      assert_no_difference 'Attachment.count' do
94
        post '/projects/ecookbook/issues', {
95
            :issue => {
96
              :subject => "Subject",
97
              :custom_field_values => {
98
                @field.id => {:blank => ''}
99
              }
100
            }
101
          }
102
        assert_response 302
103
      end
104
    end
105

  
106
    custom_value = issue.custom_value_for(@field)
107
    assert custom_value
108
    assert custom_value.value.blank?
109

  
110
    follow_redirect!
111
    assert_response :success
112

  
113
    # no links to the attachment
114
    assert_select ".cf_#{@field.id} .value a", 0
115
  end
116

  
117
  def test_failure_on_create_should_preserve_attachment
118
    attachment = new_record(Attachment) do
119
      assert_no_difference 'Issue.count' do
120
        post '/projects/ecookbook/issues', {
121
            :issue => {
122
              :subject => "",
123
              :custom_field_values => {
124
                @field.id => {:file => uploaded_test_file("testfile.txt", "text/plain")}
125
              }
126
            }
127
          }
128
        assert_response :success
129
        assert_select_error /Subject cannot be blank/
130
      end
131
    end
132

  
133
    assert_nil attachment.container_id
134
    assert_select 'input[name=?][value=?][type=hidden]', "issue[custom_field_values][#{@field.id}][p0][token]", attachment.token
135
    assert_select 'input[name=?][value=?]', "issue[custom_field_values][#{@field.id}][p0][filename]", 'testfile.txt'
136

  
137
    issue = new_record(Issue) do
138
      assert_no_difference 'Attachment.count' do
139
        post '/projects/ecookbook/issues', {
140
            :issue => {
141
              :subject => "Subject",
142
              :custom_field_values => {
143
                @field.id => {:token => attachment.token}
144
              }
145
            }
146
          }
147
        assert_response 302
148
      end
149
    end
150

  
151
    custom_value = issue.custom_value_for(@field)
152
    assert custom_value
153
    assert_equal attachment.id.to_s, custom_value.value
154
    assert_equal custom_value, attachment.reload.container
155
  end
156
end
0 157

  
trunk/test/unit/lib/redmine/field_format/attachment_format_test.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2016  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require File.expand_path('../../../../../test_helper', __FILE__)
19
require 'redmine/field_format'
20

  
21
class Redmine::AttachmentFieldFormatTest < ActionView::TestCase
22
  include ApplicationHelper
23
  include Redmine::I18n
24

  
25
  fixtures :users
26

  
27
  def setup
28
    set_language_if_valid 'en'
29
    set_tmp_attachments_directory
30
  end
31

  
32
  def test_should_accept_a_hash_with_upload_on_create
33
    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
34
    group = Group.new(:name => 'Group')
35
    attachment = nil
36

  
37
    custom_value = new_record(CustomValue) do
38
      attachment = new_record(Attachment) do
39
        group.custom_field_values = {field.id => {:file => mock_file}}
40
        assert group.save
41
      end
42
    end
43

  
44
    assert_equal 'a_file.png', attachment.filename
45
    assert_equal custom_value, attachment.container
46
    assert_equal field, attachment.container.custom_field
47
    assert_equal group, attachment.container.customized
48
  end
49

  
50
  def test_should_accept_a_hash_with_no_upload_on_create
51
    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
52
    group = Group.new(:name => 'Group')
53
    attachment = nil
54

  
55
    custom_value = new_record(CustomValue) do
56
      assert_no_difference 'Attachment.count' do
57
        group.custom_field_values = {field.id => {}}
58
        assert group.save
59
      end
60
    end
61

  
62
    assert_equal '', custom_value.value
63
  end
64

  
65
  def test_should_not_validate_with_invalid_upload_on_create
66
    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
67
    group = Group.new(:name => 'Group')
68

  
69
    with_settings :attachment_max_size => 0 do
70
      assert_no_difference 'CustomValue.count' do
71
        assert_no_difference 'Attachment.count' do
72
          group.custom_field_values = {field.id => {:file => mock_file}}
73
          assert_equal false, group.save
74
        end
75
      end
76
    end
77
  end
78

  
79
  def test_should_accept_a_hash_with_token_on_create
80
    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
81
    group = Group.new(:name => 'Group')
82

  
83
    attachment = Attachment.create!(:file => mock_file, :author => User.find(2))
84
    assert_nil attachment.container
85

  
86
    custom_value = new_record(CustomValue) do
87
      assert_no_difference 'Attachment.count' do
88
        group.custom_field_values = {field.id => {:token => attachment.token}}
89
        assert group.save
90
      end
91
    end
92

  
93
    attachment.reload
94
    assert_equal custom_value, attachment.container
95
    assert_equal field, attachment.container.custom_field
96
    assert_equal group, attachment.container.customized
97
  end
98

  
99
  def test_should_not_validate_with_invalid_token_on_create
100
    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
101
    group = Group.new(:name => 'Group')
102

  
103
    assert_no_difference 'CustomValue.count' do
104
      assert_no_difference 'Attachment.count' do
105
        group.custom_field_values = {field.id => {:token => "123.0123456789abcdef"}}
106
        assert_equal false, group.save
107
      end
108
    end
109
  end
110

  
111
  def test_should_replace_attachment_on_update
112
    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
113
    group = Group.new(:name => 'Group')
114
    attachment = nil
115
    custom_value = new_record(CustomValue) do
116
      attachment = new_record(Attachment) do
117
        group.custom_field_values = {field.id => {:file => mock_file}}
118
        assert group.save
119
      end
120
    end
121
    group.reload
122

  
123
    assert_no_difference 'Attachment.count' do
124
      assert_no_difference 'CustomValue.count' do
125
        group.custom_field_values = {field.id => {:file => mock_file}}
126
        assert group.save
127
      end
128
    end
129

  
130
    assert !Attachment.exists?(attachment.id)
131
    assert CustomValue.exists?(custom_value.id)
132

  
133
    new_attachment = Attachment.order(:id => :desc).first
134
    custom_value.reload
135
    assert_equal custom_value, new_attachment.container
136
  end
137

  
138
  def test_should_delete_attachment_on_update
139
    field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
140
    group = Group.new(:name => 'Group')
141
    attachment = nil
142
    custom_value = new_record(CustomValue) do
143
      attachment = new_record(Attachment) do
144
        group.custom_field_values = {field.id => {:file => mock_file}}
145
        assert group.save
146
      end
147
    end
148
    group.reload
149

  
150
    assert_difference 'Attachment.count', -1 do
151
      assert_no_difference 'CustomValue.count' do
152
        group.custom_field_values = {field.id => {}}
153
        assert group.save
154
      end
155
    end
156

  
157
    assert !Attachment.exists?(attachment.id)
158
    assert CustomValue.exists?(custom_value.id)
159

  
160
    custom_value.reload
161
    assert_equal '', custom_value.value
162
  end
163
end
0 164

  

Also available in: Unified diff