Revision 15917
Added by Jean-Philippe Lang over 8 years ago
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(' '.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(' '.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> </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: " "; |
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
Adds file custom field format (#6719).