##// END OF EJS Templates
Adds file custom field format (#6719)....
Jean-Philippe Lang -
r15535:ef45304817e9
parent child
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,156
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,0 +1,163
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
@@ -197,6 +197,8 module ApplicationHelper
197 l(:general_text_No)
197 l(:general_text_No)
198 when 'Issue'
198 when 'Issue'
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
200 when 'Attachment'
201 html ? link_to_attachment(object, :download => true) : object.filename
200 when 'CustomValue', 'CustomFieldValue'
202 when 'CustomValue', 'CustomFieldValue'
201 if object.custom_field
203 if object.custom_field
202 f = object.custom_field.format.formatted_custom_value(self, object, html)
204 f = object.custom_field.format.formatted_custom_value(self, object, html)
@@ -329,6 +329,7 module IssuesHelper
329 def show_detail(detail, no_html=false, options={})
329 def show_detail(detail, no_html=false, options={})
330 multiple = false
330 multiple = false
331 show_diff = false
331 show_diff = false
332 no_details = false
332
333
333 case detail.property
334 case detail.property
334 when 'attr'
335 when 'attr'
@@ -364,7 +365,9 module IssuesHelper
364 custom_field = detail.custom_field
365 custom_field = detail.custom_field
365 if custom_field
366 if custom_field
366 label = custom_field.name
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 show_diff = true
371 show_diff = true
369 else
372 else
370 multiple = custom_field.multiple?
373 multiple = custom_field.multiple?
@@ -417,7 +420,9 module IssuesHelper
417 end
420 end
418 end
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 s = l(:text_journal_changed_no_detail, :label => label)
426 s = l(:text_journal_changed_no_detail, :label => label)
422 unless no_html
427 unless no_html
423 diff_link = link_to 'diff',
428 diff_link = link_to 'diff',
@@ -163,6 +163,10 class CustomField < ActiveRecord::Base
163 end
163 end
164 end
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 def cast_value(value)
170 def cast_value(value)
167 format.cast_value(self, value)
171 format.cast_value(self, value)
168 end
172 end
@@ -254,20 +258,23 class CustomField < ActiveRecord::Base
254 # or an empty array if value is a valid value for the custom field
258 # or an empty array if value is a valid value for the custom field
255 def validate_custom_value(custom_value)
259 def validate_custom_value(custom_value)
256 value = custom_value.value
260 value = custom_value.value
257 errs = []
261 errs = format.validate_custom_value(custom_value)
258 if value.is_a?(Array)
262
259 if !multiple?
263 unless errs.any?
260 errs << ::I18n.t('activerecord.errors.messages.invalid')
264 if value.is_a?(Array)
261 end
265 if !multiple?
262 if is_required? && value.detect(&:present?).nil?
266 errs << ::I18n.t('activerecord.errors.messages.invalid')
263 errs << ::I18n.t('activerecord.errors.messages.blank')
267 end
264 end
268 if is_required? && value.detect(&:present?).nil?
265 else
269 errs << ::I18n.t('activerecord.errors.messages.blank')
266 if is_required? && value.blank?
270 end
267 errs << ::I18n.t('activerecord.errors.messages.blank')
271 else
272 if is_required? && value.blank?
273 errs << ::I18n.t('activerecord.errors.messages.blank')
274 end
268 end
275 end
269 end
276 end
270 errs += format.validate_custom_value(custom_value)
277
271 errs
278 errs
272 end
279 end
273
280
@@ -281,6 +288,10 class CustomField < ActiveRecord::Base
281 validate_field_value(value).empty?
288 validate_field_value(value).empty?
282 end
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 def format_in?(*args)
295 def format_in?(*args)
285 args.include?(field_format)
296 args.include?(field_format)
286 end
297 end
@@ -48,6 +48,10 class CustomFieldValue
48 value.to_s
48 value.to_s
49 end
49 end
50
50
51 def value=(v)
52 @value = custom_field.set_custom_field_value(self, v)
53 end
54
51 def validate_value
55 def validate_value
52 custom_field.validate_custom_value(self).each do |message|
56 custom_field.validate_custom_value(self).each do |message|
53 customized.errors.add(:base, custom_field.name + ' ' + message)
57 customized.errors.add(:base, custom_field.name + ' ' + message)
@@ -20,6 +20,8 class CustomValue < ActiveRecord::Base
20 belongs_to :customized, :polymorphic => true
20 belongs_to :customized, :polymorphic => true
21 attr_protected :id
21 attr_protected :id
22
22
23 after_save :custom_field_after_save_custom_value
24
23 def initialize(attributes=nil, *args)
25 def initialize(attributes=nil, *args)
24 super
26 super
25 if new_record? && custom_field && !attributes.key?(:value)
27 if new_record? && custom_field && !attributes.key?(:value)
@@ -40,6 +42,10 class CustomValue < ActiveRecord::Base
40 custom_field.visible?
42 custom_field.visible?
41 end
43 end
42
44
45 def attachments_visible?(user)
46 visible? && customized && customized.visible?(user)
47 end
48
43 def required?
49 def required?
44 custom_field.is_required?
50 custom_field.is_required?
45 end
51 end
@@ -47,4 +53,10 class CustomValue < ActiveRecord::Base
47 def to_s
53 def to_s
48 value.to_s
54 value.to_s
49 end
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 end
62 end
@@ -1,29 +1,45
1 <span id="attachments_fields">
1 <% attachment_param ||= 'attachments' %>
2 <% if defined?(container) && container && container.saved_attachments %>
2 <% saved_attachments ||= container.saved_attachments if defined?(container) && container %>
3 <% container.saved_attachments.each_with_index do |attachment, i| %>
3 <% multiple = true unless defined?(multiple) && multiple == false %>
4 <span id="attachments_p<%= i %>">
4 <% show_add = multiple || saved_attachments.blank? %>
5 <%= text_field_tag("attachments[p#{i}][filename]", attachment.filename, :class => 'filename') +
5 <% description = (defined?(description) && description == false ? false : true) %>
6 text_field_tag("attachments[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') +
6 <% css_class = (defined?(filedrop) && filedrop == false ? '' : 'filedrop') %>
7 link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
7
8 <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.token}" %>
8 <span class="attachments_form">
9 </span>
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 <% end %>
24 <% end %>
11 <% end %>
25 </span>
12 </span>
26 <span class="add_attachment" style="<%= show_add ? nil : 'display:none;' %>">
13 <span class="add_attachment">
27 <%= file_field_tag "#{attachment_param}[dummy][file]",
14 <%= file_field_tag 'attachments[dummy][file]',
28 :id => nil,
15 :id => nil,
29 :class => "file_selector #{css_class}",
16 :class => 'file_selector',
30 :multiple => multiple,
17 :multiple => true,
31 :onchange => 'addInputFiles(this);',
18 :onchange => 'addInputFiles(this);',
32 :data => {
19 :data => {
33 :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
20 :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)),
21 :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,
22 :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
36 :upload_path => uploads_path(:format => 'js'),
23 :upload_path => uploads_path(:format => 'js'),
37 :param => attachment_param,
24 :description_placeholder => l(:label_optional_description)
38 :description => description,
25 } %>
39 :description_placeholder => l(:label_optional_description)
26 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
40 } %>
41 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
42 </span>
27 </span>
43 </span>
28
44
29 <% content_for :header_tags do %>
45 <% content_for :header_tags do %>
@@ -1,1 +1,2
1 $('#attachments_<%= j params[:attachment_id] %>').closest('.attachments_form').find('.add_attachment').show();
1 $('#attachments_<%= j params[:attachment_id] %>').remove();
2 $('#attachments_<%= j params[:attachment_id] %>').remove();
@@ -3,7 +3,7 var fileSpan = $('#attachments_<%= j params[:attachment_id] %>');
3 fileSpan.hide();
3 fileSpan.hide();
4 alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
4 alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
5 <% else %>
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 fileSpan.find('a.remove-upload')
7 fileSpan.find('a.remove-upload')
8 .attr({
8 .attr({
9 "data-remote": true,
9 "data-remote": true,
@@ -28,7 +28,9
28 when "IssueCustomField" %>
28 when "IssueCustomField" %>
29 <p><%= f.check_box :is_required %></p>
29 <p><%= f.check_box :is_required %></p>
30 <p><%= f.check_box :is_for_all, :data => {:disables => '#custom_field_project_ids input'} %></p>
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 <p><%= f.check_box :is_filter %></p>
32 <p><%= f.check_box :is_filter %></p>
33 <% end %>
32 <% if @custom_field.format.searchable_supported %>
34 <% if @custom_field.format.searchable_supported %>
33 <p><%= f.check_box :searchable %></p>
35 <p><%= f.check_box :searchable %></p>
34 <% end %>
36 <% end %>
@@ -57,7 +59,9 when "IssueCustomField" %>
57 <p><%= f.check_box :is_required %></p>
59 <p><%= f.check_box :is_required %></p>
58 <p><%= f.check_box :visible %></p>
60 <p><%= f.check_box :visible %></p>
59 <p><%= f.check_box :editable %></p>
61 <p><%= f.check_box :editable %></p>
62 <% if @custom_field.format.is_filter_supported %>
60 <p><%= f.check_box :is_filter %></p>
63 <p><%= f.check_box :is_filter %></p>
64 <% end %>
61
65
62 <% when "ProjectCustomField" %>
66 <% when "ProjectCustomField" %>
63 <p><%= f.check_box :is_required %></p>
67 <p><%= f.check_box :is_required %></p>
@@ -65,19 +69,27 when "IssueCustomField" %>
65 <% if @custom_field.format.searchable_supported %>
69 <% if @custom_field.format.searchable_supported %>
66 <p><%= f.check_box :searchable %></p>
70 <p><%= f.check_box :searchable %></p>
67 <% end %>
71 <% end %>
72 <% if @custom_field.format.is_filter_supported %>
68 <p><%= f.check_box :is_filter %></p>
73 <p><%= f.check_box :is_filter %></p>
74 <% end %>
69
75
70 <% when "VersionCustomField" %>
76 <% when "VersionCustomField" %>
71 <p><%= f.check_box :is_required %></p>
77 <p><%= f.check_box :is_required %></p>
78 <% if @custom_field.format.is_filter_supported %>
72 <p><%= f.check_box :is_filter %></p>
79 <p><%= f.check_box :is_filter %></p>
80 <% end %>
73
81
74 <% when "GroupCustomField" %>
82 <% when "GroupCustomField" %>
75 <p><%= f.check_box :is_required %></p>
83 <p><%= f.check_box :is_required %></p>
84 <% if @custom_field.format.is_filter_supported %>
76 <p><%= f.check_box :is_filter %></p>
85 <p><%= f.check_box :is_filter %></p>
86 <% end %>
77
87
78 <% when "TimeEntryCustomField" %>
88 <% when "TimeEntryCustomField" %>
79 <p><%= f.check_box :is_required %></p>
89 <p><%= f.check_box :is_required %></p>
90 <% if @custom_field.format.is_filter_supported %>
80 <p><%= f.check_box :is_filter %></p>
91 <p><%= f.check_box :is_filter %></p>
92 <% end %>
81
93
82 <% else %>
94 <% else %>
83 <p><%= f.check_box :is_required %></p>
95 <p><%= f.check_box :is_required %></p>
@@ -34,6 +34,7 module Redmine
34 options.merge(:as => :container, :dependent => :destroy, :inverse_of => :container)
34 options.merge(:as => :container, :dependent => :destroy, :inverse_of => :container)
35 send :include, Redmine::Acts::Attachable::InstanceMethods
35 send :include, Redmine::Acts::Attachable::InstanceMethods
36 before_save :attach_saved_attachments
36 before_save :attach_saved_attachments
37 after_rollback :detach_saved_attachments
37 validate :warn_about_failed_attachments
38 validate :warn_about_failed_attachments
38 end
39 end
39 end
40 end
@@ -90,7 +91,7 module Redmine
90 if file = attachment['file']
91 if file = attachment['file']
91 next unless file.size > 0
92 next unless file.size > 0
92 a = Attachment.create(:file => file, :author => author)
93 a = Attachment.create(:file => file, :author => author)
93 elsif token = attachment['token']
94 elsif token = attachment['token'].presence
94 a = Attachment.find_by_token(token)
95 a = Attachment.find_by_token(token)
95 unless a
96 unless a
96 @failed_attachment_count += 1
97 @failed_attachment_count += 1
@@ -117,6 +118,14 module Redmine
117 end
118 end
118 end
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 def warn_about_failed_attachments
129 def warn_about_failed_attachments
121 if @failed_attachment_count && @failed_attachment_count > 0
130 if @failed_attachment_count && @failed_attachment_count > 0
122 errors.add :base, ::I18n.t('warning_attachments_not_saved', count: @failed_attachment_count)
131 errors.add :base, ::I18n.t('warning_attachments_not_saved', count: @failed_attachment_count)
@@ -68,16 +68,7 module Redmine
68 custom_field_values.each do |custom_field_value|
68 custom_field_values.each do |custom_field_value|
69 key = custom_field_value.custom_field_id.to_s
69 key = custom_field_value.custom_field_id.to_s
70 if values.has_key?(key)
70 if values.has_key?(key)
71 value = values[key]
71 custom_field_value.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
81 end
72 end
82 end
73 end
83 @custom_field_values_changed = true
74 @custom_field_values_changed = true
@@ -93,11 +84,11 module Redmine
93 if values.empty?
84 if values.empty?
94 values << custom_values.build(:customized => self, :custom_field => field)
85 values << custom_values.build(:customized => self, :custom_field => field)
95 end
86 end
96 x.value = values.map(&:value)
87 x.instance_variable_set("@value", values.map(&:value))
97 else
88 else
98 cv = custom_values.detect { |v| v.custom_field == field }
89 cv = custom_values.detect { |v| v.custom_field == field }
99 cv ||= custom_values.build(:customized => self, :custom_field => field)
90 cv ||= custom_values.build(:customized => self, :custom_field => field)
100 x.value = cv.value
91 x.instance_variable_set("@value", cv.value)
101 end
92 end
102 x.value_was = x.value.dup if x.value
93 x.value_was = x.value.dup if x.value
103 x
94 x
@@ -67,6 +67,10 module Redmine
67 class_attribute :multiple_supported
67 class_attribute :multiple_supported
68 self.multiple_supported = false
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 # Set this to true if the format supports textual search on custom values
74 # Set this to true if the format supports textual search on custom values
71 class_attribute :searchable_supported
75 class_attribute :searchable_supported
72 self.searchable_supported = false
76 self.searchable_supported = false
@@ -87,6 +91,9 module Redmine
87 class_attribute :change_as_diff
91 class_attribute :change_as_diff
88 self.change_as_diff = false
92 self.change_as_diff = false
89
93
94 class_attribute :change_no_details
95 self.change_no_details = false
96
90 def self.add(name)
97 def self.add(name)
91 self.format_name = name
98 self.format_name = name
92 Redmine::FieldFormat.add(name, self)
99 Redmine::FieldFormat.add(name, self)
@@ -107,6 +114,19 module Redmine
107 "label_#{name}"
114 "label_#{name}"
108 end
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 def cast_custom_value(custom_value)
130 def cast_custom_value(custom_value)
111 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
131 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
112 end
132 end
@@ -169,6 +189,7 module Redmine
169
189
170 # Returns the validation error messages for custom_value
190 # Returns the validation error messages for custom_value
171 # Should return an empty array if custom_value is valid
191 # Should return an empty array if custom_value is valid
192 # custom_value is a CustomFieldValue.
172 def validate_custom_value(custom_value)
193 def validate_custom_value(custom_value)
173 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
194 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
174 errors = values.map do |value|
195 errors = values.map do |value|
@@ -181,6 +202,10 module Redmine
181 []
202 []
182 end
203 end
183
204
205 # CustomValue after_save callback
206 def after_save_custom_value(custom_field, custom_value)
207 end
208
184 def formatted_custom_value(view, custom_value, html=false)
209 def formatted_custom_value(view, custom_value, html=false)
185 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
210 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
186 end
211 end
@@ -830,5 +855,109 module Redmine
830 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
855 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
831 end
856 end
832 end
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 end
962 end
834 end
963 end
@@ -2,23 +2,32
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3
3
4 function addFile(inputEl, file, eagerUpload) {
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) {
9 if (attachmentsFields.children().length < maxFiles) {
7
8 var attachmentId = addFile.nextAttachmentId++;
10 var attachmentId = addFile.nextAttachmentId++;
9
10 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
11 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
12 var param = $(inputEl).data('param');
13 if (!param) {param = 'attachments'};
11
14
12 fileSpan.append(
15 fileSpan.append(
13 $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
16 $('<input>', { type: 'text', 'class': 'filename readonly', name: param +'[' + 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),
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 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
19 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
16 ).appendTo('#attachments_fields');
20 ).appendTo(attachmentsFields);
21
22 if ($(inputEl).data('description') == 0) {
23 fileSpan.find('input.description').remove();
24 }
17
25
18 if(eagerUpload) {
26 if(eagerUpload) {
19 ajaxUpload(file, attachmentId, fileSpan, inputEl);
27 ajaxUpload(file, attachmentId, fileSpan, inputEl);
20 }
28 }
21
29
30 addAttachment.toggle(attachmentsFields.children().length < maxFiles);
22 return attachmentId;
31 return attachmentId;
23 }
32 }
24 return null;
33 return null;
@@ -118,11 +127,16 function uploadBlob(blob, uploadUrl, attachmentId, options) {
118 }
127 }
119
128
120 function addInputFiles(inputEl) {
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 var clearedFileInput = $(inputEl).clone().val('');
132 var clearedFileInput = $(inputEl).clone().val('');
133 var sizeExceeded = false;
134 var param = $(inputEl).data('param');
135 if (!param) {param = 'attachments'};
122
136
123 if ($.ajaxSettings.xhr().upload && inputEl.files) {
137 if ($.ajaxSettings.xhr().upload && inputEl.files) {
124 // upload files using ajax
138 // upload files using ajax
125 uploadAndAttachFiles(inputEl.files, inputEl);
139 sizeExceeded = uploadAndAttachFiles(inputEl.files, inputEl);
126 $(inputEl).remove();
140 $(inputEl).remove();
127 } else {
141 } else {
128 // browser not supporting the file API, upload on form submission
142 // browser not supporting the file API, upload on form submission
@@ -130,11 +144,11 function addInputFiles(inputEl) {
130 var aFilename = inputEl.value.split(/\/|\\/);
144 var aFilename = inputEl.value.split(/\/|\\/);
131 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
145 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
132 if (attachmentId) {
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 function uploadAndAttachFiles(files, inputEl) {
154 function uploadAndAttachFiles(files, inputEl) {
@@ -151,6 +165,7 function uploadAndAttachFiles(files, inputEl) {
151 } else {
165 } else {
152 $.each(files, function() {addFile(inputEl, this, true);});
166 $.each(files, function() {addFile(inputEl, this, true);});
153 }
167 }
168 return sizeExceeded;
154 }
169 }
155
170
156 function handleFileDropEvent(e) {
171 function handleFileDropEvent(e) {
@@ -159,7 +174,7 function handleFileDropEvent(e) {
159 blockEventPropagation(e);
174 blockEventPropagation(e);
160
175
161 if ($.inArray('Files', e.dataTransfer.types) > -1) {
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,12 +193,12 function setupFileDrop() {
178
193
179 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
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 $(this).on({
197 $(this).on({
183 dragover: dragOverHandler,
198 dragover: dragOverHandler,
184 dragleave: dragOutHandler,
199 dragleave: dragOutHandler,
185 drop: handleFileDropEvent
200 drop: handleFileDropEvent
186 });
201 }).addClass('filedroplistner');
187 });
202 });
188 }
203 }
189 }
204 }
@@ -600,7 +600,7 span.pagination>span {white-space:nowrap;}
600 margin: 0;
600 margin: 0;
601 padding: 3px 0 3px 0;
601 padding: 3px 0 3px 0;
602 padding-left: 180px; /* width of left column containing the label elements */
602 padding-left: 180px; /* width of left column containing the label elements */
603 min-height: 1.8em;
603 line-height: 2em;
604 clear:left;
604 clear:left;
605 }
605 }
606
606
@@ -626,13 +626,16 html>body .tabular p {overflow:hidden;}
626 width: 270px;
626 width: 270px;
627 }
627 }
628
628
629 label.block {
630 display: block;
631 width: auto !important;
632 }
633
629 .tabular label.block{
634 .tabular label.block{
630 font-weight: normal;
635 font-weight: normal;
631 margin-left: 0px !important;
636 margin-left: 0px !important;
632 text-align: left;
637 text-align: left;
633 float: none;
638 float: none;
634 display: block;
635 width: auto !important;
636 }
639 }
637
640
638 .tabular label.inline{
641 .tabular label.inline{
@@ -687,13 +690,14 span.required {color: #bb0000;}
687 .check_box_group.bool_cf {border:0; background:inherit;}
690 .check_box_group.bool_cf {border:0; background:inherit;}
688 .check_box_group.bool_cf label {display: inline;}
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;}
693 .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;}
694 .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;}
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;}
693 #attachments_fields input.filename {height:1.8em;}
696 .tabular input.filename {max-width:75% !important;}
694 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
697 .attachments_fields input.filename {height:1.8em;}
695 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
698 .attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
696 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
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 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
701 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
698 a.remove-upload:hover {text-decoration:none !important;}
702 a.remove-upload:hover {text-decoration:none !important;}
699 .existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
703 .existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
@@ -1160,7 +1164,7 a.close-icon:hover {background-image:url('../images/close_hl.png');}
1160 padding-top: 0;
1164 padding-top: 0;
1161 padding-bottom: 0;
1165 padding-bottom: 0;
1162 font-size: 8px;
1166 font-size: 8px;
1163 vertical-align: text-bottom;
1167 vertical-align: middle;
1164 }
1168 }
1165 .icon-only::after {
1169 .icon-only::after {
1166 content: "&nbsp;";
1170 content: "&nbsp;";
General Comments 0
You need to be logged in to leave comments. Login now