|
1 | 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 | 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) |
@@ -329,6 +329,7 module IssuesHelper | |||
|
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,7 +365,9 module IssuesHelper | |||
|
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_ |
|
|
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,7 +420,9 module IssuesHelper | |||
|
417 | 420 | end |
|
418 | 421 | end |
|
419 | 422 | |
|
420 |
if |
|
|
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', |
@@ -163,6 +163,10 class CustomField < ActiveRecord::Base | |||
|
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,7 +258,9 class CustomField < ActiveRecord::Base | |||
|
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 = [] | |
|
261 | errs = format.validate_custom_value(custom_value) | |
|
262 | ||
|
263 | unless errs.any? | |
|
258 | 264 | if value.is_a?(Array) |
|
259 | 265 | if !multiple? |
|
260 | 266 | errs << ::I18n.t('activerecord.errors.messages.invalid') |
@@ -267,7 +273,8 class CustomField < ActiveRecord::Base | |||
|
267 | 273 | errs << ::I18n.t('activerecord.errors.messages.blank') |
|
268 | 274 | end |
|
269 | 275 | end |
|
270 | errs += format.validate_custom_value(custom_value) | |
|
276 | end | |
|
277 | ||
|
271 | 278 | errs |
|
272 | 279 | end |
|
273 | 280 | |
@@ -281,6 +288,10 class CustomField < ActiveRecord::Base | |||
|
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 |
@@ -48,6 +48,10 class CustomFieldValue | |||
|
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) |
@@ -20,6 +20,8 class CustomValue < ActiveRecord::Base | |||
|
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,6 +42,10 class CustomValue < ActiveRecord::Base | |||
|
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,4 +53,10 class CustomValue < ActiveRecord::Base | |||
|
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 |
@@ -1,30 +1,46 | |||
|
1 | <span id="attachments_fields"> | |
|
2 | <% if defined?(container) && container && container.saved_attachments %> | |
|
3 | <% container.saved_attachments.each_with_index do |attachment, i| %> | |
|
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| %> | |
|
4 | 12 | <span id="attachments_p<%= i %>"> |
|
5 |
<%= text_field_tag("attachment |
|
|
6 | text_field_tag("attachments[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') + | |
|
7 | link_to(' '.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}" %> | |
|
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(' '.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 %> | |
|
9 | 22 | </span> |
|
10 | 23 | <% end %> |
|
11 | 24 | <% end %> |
|
12 | 25 | </span> |
|
13 | <span class="add_attachment"> | |
|
14 |
<%= file_field_tag |
|
|
26 | <span class="add_attachment" style="<%= show_add ? nil : 'display:none;' %>"> | |
|
27 | <%= file_field_tag "#{attachment_param}[dummy][file]", | |
|
15 | 28 | :id => nil, |
|
16 |
:class => |
|
|
17 |
:multiple => |
|
|
29 | :class => "file_selector #{css_class}", | |
|
30 | :multiple => multiple, | |
|
18 | 31 | :onchange => 'addInputFiles(this);', |
|
19 | 32 | :data => { |
|
20 | 33 | :max_file_size => Setting.attachment_max_size.to_i.kilobytes, |
|
21 | 34 | :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)), |
|
22 | 35 | :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i, |
|
23 | 36 | :upload_path => uploads_path(:format => 'js'), |
|
37 | :param => attachment_param, | |
|
38 | :description => description, | |
|
24 | 39 | :description_placeholder => l(:label_optional_description) |
|
25 | 40 | } %> |
|
26 | 41 | (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) |
|
27 | 42 | </span> |
|
43 | </span> | |
|
28 | 44 | |
|
29 | 45 | <% content_for :header_tags do %> |
|
30 | 46 | <%= javascript_include_tag 'attachments' %> |
@@ -1,1 +1,2 | |||
|
1 | $('#attachments_<%= j params[:attachment_id] %>').closest('.attachments_form').find('.add_attachment').show(); | |
|
1 | 2 | $('#attachments_<%= j params[:attachment_id] %>').remove(); |
@@ -3,7 +3,7 var fileSpan = $('#attachments_<%= j params[:attachment_id] %>'); | |||
|
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, |
@@ -28,7 +28,9 | |||
|
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,7 +59,9 when "IssueCustomField" %> | |||
|
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,19 +69,27 when "IssueCustomField" %> | |||
|
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> |
@@ -34,6 +34,7 module Redmine | |||
|
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,7 +91,7 module Redmine | |||
|
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,6 +118,14 module Redmine | |||
|
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) |
@@ -68,16 +68,7 module Redmine | |||
|
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,11 +84,11 module Redmine | |||
|
93 | 84 | if values.empty? |
|
94 | 85 | values << custom_values.build(:customized => self, :custom_field => field) |
|
95 | 86 | end |
|
96 |
x. |
|
|
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. |
|
|
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 |
@@ -67,6 +67,10 module Redmine | |||
|
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,6 +91,9 module Redmine | |||
|
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,6 +114,19 module Redmine | |||
|
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,6 +189,7 module Redmine | |||
|
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,6 +202,10 module Redmine | |||
|
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,5 +855,109 module Redmine | |||
|
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 |
@@ -2,23 +2,32 | |||
|
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 ( |
|
|
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: |
|
|
14 |
$('<input>', { type: 'text', 'class': 'description', name: |
|
|
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> </a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload) |
|
16 |
).appendTo( |
|
|
20 | ).appendTo(attachmentsFields); | |
|
21 | ||
|
22 | if ($(inputEl).data('description') == 0) { | |
|
23 | fileSpan.find('input.description').remove(); | |
|
24 | } | |
|
17 | 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,11 +127,16 function uploadBlob(blob, uploadUrl, attachmentId, options) { | |||
|
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,11 +144,11 function addInputFiles(inputEl) { | |||
|
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: |
|
|
147 | $(inputEl).attr({ name: param + '[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId); | |
|
134 | 148 | } |
|
135 | 149 | } |
|
136 | 150 | |
|
137 |
clearedFileInput. |
|
|
151 | clearedFileInput.prependTo(addAttachment); | |
|
138 | 152 | } |
|
139 | 153 | |
|
140 | 154 | function uploadAndAttachFiles(files, inputEl) { |
@@ -151,6 +165,7 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,7 +174,7 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 |
|
|
177 | uploadAndAttachFiles(e.dataTransfer.files, $('input:file.filedrop').first()); | |
|
163 | 178 | } |
|
164 | 179 | } |
|
165 | 180 | |
@@ -178,12 +193,12 function setupFileDrop() { | |||
|
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 | } |
@@ -600,7 +600,7 span.pagination>span {white-space:nowrap;} | |||
|
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 |
|
|
|
603 | line-height: 2em; | |
|
604 | 604 | clear:left; |
|
605 | 605 | } |
|
606 | 606 | |
@@ -626,13 +626,16 html>body .tabular p {overflow:hidden;} | |||
|
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,13 +690,14 span.required {color: #bb0000;} | |||
|
687 | 690 | .check_box_group.bool_cf {border:0; background:inherit;} |
|
688 | 691 | .check_box_group.bool_cf label {display: inline;} |
|
689 | 692 | |
|
690 |
|
|
|
691 |
|
|
|
692 |
|
|
|
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 |
|
|
|
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,7 +1164,7 a.close-icon:hover {background-image:url('../images/close_hl.png');} | |||
|
1160 | 1164 | padding-top: 0; |
|
1161 | 1165 | padding-bottom: 0; |
|
1162 | 1166 | font-size: 8px; |
|
1163 |
vertical-align: |
|
|
1167 | vertical-align: middle; | |
|
1164 | 1168 | } |
|
1165 | 1169 | .icon-only::after { |
|
1166 | 1170 | content: " "; |
General Comments 0
You need to be logged in to leave comments.
Login now