##// END OF EJS Templates
Merged ajax_upload branch (#3957)....
Jean-Philippe Lang -
r10748:ef25210aca92
parent child
Show More
@@ -0,0 +1,1
1 $('#attachments_<%= j params[:attachment_id] %>').remove();
@@ -0,0 +1,9
1 var fileSpan = $('#attachments_<%= j params[:attachment_id] %>');
2 $('<input>', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan);
3 fileSpan.find('a.remove-upload')
4 .attr({
5 "data-remote": true,
6 "data-method": 'delete',
7 href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>'
8 })
9 .off('click');
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,189
1 /* Redmine - project management software
2 Copyright (C) 2006-2012 Jean-Philippe Lang */
3
4 function addFile(inputEl, file, eagerUpload) {
5
6 if ($('#attachments_fields').children().length < 10) {
7
8 var attachmentId = addFile.nextAttachmentId++;
9
10 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
11
12 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),
15 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
16 ).appendTo('#attachments_fields');
17
18 if(eagerUpload) {
19 ajaxUpload(file, attachmentId, fileSpan, inputEl);
20 }
21
22 return attachmentId;
23 }
24 return null;
25 }
26
27 addFile.nextAttachmentId = 1;
28
29 function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
30
31 function onLoadstart(e) {
32 fileSpan.removeClass('ajax-waiting');
33 fileSpan.addClass('ajax-loading');
34 $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
35 }
36
37 function onProgress(e) {
38 if(e.lengthComputable) {
39 this.progressbar( 'value', e.loaded * 100 / e.total );
40 }
41 }
42
43 function actualUpload(file, attachmentId, fileSpan, inputEl) {
44
45 ajaxUpload.uploading++;
46
47 uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
48 loadstartEventHandler: onLoadstart.bind(progressSpan),
49 progressEventHandler: onProgress.bind(progressSpan)
50 })
51 .done(function(result) {
52 progressSpan.progressbar( 'value', 100 ).remove();
53 fileSpan.find('input.description, a').css('display', 'inline-block');
54 })
55 .fail(function(result) {
56 progressSpan.text(result.statusText);
57 }).always(function() {
58 ajaxUpload.uploading--;
59 fileSpan.removeClass('ajax-loading');
60 var form = fileSpan.parents('form');
61 if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
62 $('input:submit', form).removeAttr('disabled');
63 }
64 form.dequeue('upload');
65 });
66 }
67
68 var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
69 progressSpan.progressbar();
70 fileSpan.addClass('ajax-waiting');
71
72 var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
73
74 if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
75 actualUpload(file, attachmentId, fileSpan, inputEl);
76 else
77 $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
78 }
79
80 ajaxUpload.uploading = 0;
81
82 function removeFile() {
83 $(this).parent('span').remove();
84 return false;
85 }
86
87 function uploadBlob(blob, uploadUrl, attachmentId, options) {
88
89 var actualOptions = $.extend({
90 loadstartEventHandler: $.noop,
91 progressEventHandler: $.noop
92 }, options);
93
94 uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
95 if (blob instanceof window.File) {
96 uploadUrl += '&filename=' + encodeURIComponent(blob.name);
97 }
98
99 return $.ajax(uploadUrl, {
100 type: 'POST',
101 contentType: 'application/octet-stream',
102 beforeSend: function(jqXhr) {
103 jqXhr.setRequestHeader('Accept', 'application/js');
104 },
105 xhr: function() {
106 var xhr = $.ajaxSettings.xhr();
107 xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
108 xhr.upload.onprogress = actualOptions.progressEventHandler;
109 return xhr;
110 },
111 data: blob,
112 cache: false,
113 processData: false
114 });
115 }
116
117 function addInputFiles(inputEl) {
118 var clearedFileInput = $(inputEl).clone().val('');
119
120 if (inputEl.files) {
121 // upload files using ajax
122 uploadAndAttachFiles(inputEl.files, inputEl);
123 $(inputEl).remove();
124 } else {
125 // browser not supporting the file API, upload on form submission
126 var attachmentId;
127 var aFilename = inputEl.value.split(/\/|\\/);
128 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
129 if (attachmentId) {
130 $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
131 }
132 }
133
134 clearedFileInput.insertAfter('#attachments_fields');
135 }
136
137 function uploadAndAttachFiles(files, inputEl) {
138
139 var maxFileSize = $(inputEl).data('max-file-size');
140 var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
141
142 var sizeExceeded = false;
143 $.each(files, function() {
144 if (this.size && maxFileSize && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
145 });
146 if (sizeExceeded) {
147 window.alert(maxFileSizeExceeded);
148 } else {
149 $.each(files, function() {addFile(inputEl, this, true);});
150 }
151 }
152
153 function handleFileDropEvent(e) {
154
155 $(this).removeClass('fileover');
156 blockEventPropagation(e);
157
158 if ($.inArray('Files', e.dataTransfer.types) > -1) {
159
160 uploadAndAttachFiles(e.dataTransfer.files, $('input:file[name=attachments_files]'));
161 }
162 }
163
164 function dragOverHandler(e) {
165 $(this).addClass('fileover');
166 blockEventPropagation(e);
167 }
168
169 function dragOutHandler(e) {
170 $(this).removeClass('fileover');
171 blockEventPropagation(e);
172 }
173
174 function setupFileDrop() {
175 if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
176
177 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
178
179 $('form div.box').has('input:file').each(function() {
180 $(this).on({
181 dragover: dragOverHandler,
182 dragleave: dragOutHandler,
183 drop: handleFileDropEvent
184 });
185 });
186 }
187 }
188
189 $(document).ready(setupFileDrop);
@@ -0,0 +1,132
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 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 AttachmentsTest < ActionController::IntegrationTest
21 fixtures :projects, :enabled_modules,
22 :users, :roles, :members, :member_roles,
23 :trackers, :projects_trackers,
24 :issue_statuses, :enumerations
25
26 def test_upload_as_js_and_attach_to_an_issue
27 log_user('jsmith', 'jsmith')
28
29 token = ajax_upload('myupload.txt', 'File content')
30
31 assert_difference 'Issue.count' do
32 post '/projects/ecookbook/issues', {
33 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
34 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
35 }
36 assert_response 302
37 end
38
39 issue = Issue.order('id DESC').first
40 assert_equal 'Issue with upload', issue.subject
41 assert_equal 1, issue.attachments.count
42
43 attachment = issue.attachments.first
44 assert_equal 'myupload.txt', attachment.filename
45 assert_equal 'My uploaded file', attachment.description
46 assert_equal 'File content'.length, attachment.filesize
47 end
48
49 def test_upload_as_js_and_preview_as_inline_attachment
50 log_user('jsmith', 'jsmith')
51
52 token = ajax_upload('myupload.jpg', 'JPEG content')
53
54 post '/issues/preview/new/ecookbook', {
55 :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'},
56 :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}}
57 }
58 assert_response :success
59
60 attachment_path = response.body.match(%r{<img src="(/attachments/download/\d+)"})[1]
61 assert_not_nil token, "No attachment path found in response:\n#{response.body}"
62
63 get attachment_path
64 assert_response :success
65 assert_equal 'JPEG content', response.body
66 end
67
68 def test_upload_and_resubmit_after_validation_failure
69 log_user('jsmith', 'jsmith')
70
71 token = ajax_upload('myupload.txt', 'File content')
72
73 assert_no_difference 'Issue.count' do
74 post '/projects/ecookbook/issues', {
75 :issue => {:tracker_id => 1, :subject => ''},
76 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
77 }
78 assert_response :success
79 end
80 assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token
81 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt'
82 assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file'
83
84 assert_difference 'Issue.count' do
85 post '/projects/ecookbook/issues', {
86 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
87 :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
88 }
89 assert_response 302
90 end
91
92 issue = Issue.order('id DESC').first
93 assert_equal 'Issue with upload', issue.subject
94 assert_equal 1, issue.attachments.count
95
96 attachment = issue.attachments.first
97 assert_equal 'myupload.txt', attachment.filename
98 assert_equal 'My uploaded file', attachment.description
99 assert_equal 'File content'.length, attachment.filesize
100 end
101
102 def test_upload_as_js_and_destroy
103 log_user('jsmith', 'jsmith')
104
105 token = ajax_upload('myupload.txt', 'File content')
106
107 attachment = Attachment.order('id DESC').first
108 attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1"
109 assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}"
110
111 assert_difference 'Attachment.count', -1 do
112 delete attachment_path
113 assert_response :success
114 end
115
116 assert_include "$('#attachments_1').remove();", response.body
117 end
118
119 private
120
121 def ajax_upload(filename, content, attachment_id=1)
122 assert_difference 'Attachment.count' do
123 post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'}
124 assert_response :success
125 assert_equal 'text/javascript', response.content_type
126 end
127
128 token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1]
129 assert_not_nil token, "No upload token found in response:\n#{response.body}"
130 token
131 end
132 end
@@ -300,6 +300,16 class ApplicationController < ActionController::Base
300 render_404
300 render_404
301 end
301 end
302
302
303 def find_attachments
304 if (attachments = params[:attachments]).present?
305 att = attachments.values.collect do |attachment|
306 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
307 end
308 att.compact!
309 end
310 @attachments = att || []
311 end
312
303 # make sure that the user is a member of the project (or admin) if project is private
313 # make sure that the user is a member of the project (or admin) if project is private
304 # used as a before_filter for actions that do not require any particular permission on the project
314 # used as a before_filter for actions that do not require any particular permission on the project
305 def check_project_privacy
315 def check_project_privacy
@@ -85,15 +85,17 class AttachmentsController < ApplicationController
85 @attachment = Attachment.new(:file => request.raw_post)
85 @attachment = Attachment.new(:file => request.raw_post)
86 @attachment.author = User.current
86 @attachment.author = User.current
87 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
87 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
88 saved = @attachment.save
88
89
89 if @attachment.save
90 respond_to do |format|
90 respond_to do |format|
91 format.js
91 format.api { render :action => 'upload', :status => :created }
92 format.api {
92 end
93 if saved
93 else
94 render :action => 'upload', :status => :created
94 respond_to do |format|
95 else
95 format.api { render_validation_errors(@attachment) }
96 render_validation_errors(@attachment)
96 end
97 end
98 }
97 end
99 end
98 end
100 end
99
101
@@ -101,9 +103,17 class AttachmentsController < ApplicationController
101 if @attachment.container.respond_to?(:init_journal)
103 if @attachment.container.respond_to?(:init_journal)
102 @attachment.container.init_journal(User.current)
104 @attachment.container.init_journal(User.current)
103 end
105 end
104 # Make sure association callbacks are called
106 if @attachment.container
105 @attachment.container.attachments.delete(@attachment)
107 # Make sure association callbacks are called
106 redirect_to_referer_or project_path(@project)
108 @attachment.container.attachments.delete(@attachment)
109 else
110 @attachment.destroy
111 end
112
113 respond_to do |format|
114 format.html { redirect_to_referer_or project_path(@project) }
115 format.js
116 end
107 end
117 end
108
118
109 private
119 private
@@ -19,6 +19,7 class MessagesController < ApplicationController
19 menu_item :boards
19 menu_item :boards
20 default_search_scope :messages
20 default_search_scope :messages
21 before_filter :find_board, :only => [:new, :preview]
21 before_filter :find_board, :only => [:new, :preview]
22 before_filter :find_attachments, :only => [:preview]
22 before_filter :find_message, :except => [:new, :preview]
23 before_filter :find_message, :except => [:new, :preview]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
24 before_filter :authorize, :except => [:preview, :edit, :destroy]
24
25
@@ -117,7 +118,6 class MessagesController < ApplicationController
117
118
118 def preview
119 def preview
119 message = @board.messages.find_by_id(params[:id])
120 message = @board.messages.find_by_id(params[:id])
120 @attachements = message.attachments if message
121 @text = (params[:message] || params[:reply])[:content]
121 @text = (params[:message] || params[:reply])[:content]
122 @previewed = message
122 @previewed = message
123 render :partial => 'common/preview'
123 render :partial => 'common/preview'
@@ -16,12 +16,11
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class PreviewsController < ApplicationController
18 class PreviewsController < ApplicationController
19 before_filter :find_project
19 before_filter :find_project, :find_attachments
20
20
21 def issue
21 def issue
22 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
22 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
23 if @issue
23 if @issue
24 @attachements = @issue.attachments
25 @description = params[:issue] && params[:issue][:description]
24 @description = params[:issue] && params[:issue][:description]
26 if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
25 if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
27 @description = nil
26 @description = nil
@@ -37,7 +36,6 class PreviewsController < ApplicationController
37 def news
36 def news
38 if params[:id].present? && news = News.visible.find_by_id(params[:id])
37 if params[:id].present? && news = News.visible.find_by_id(params[:id])
39 @previewed = news
38 @previewed = news
40 @attachments = news.attachments
41 end
39 end
42 @text = (params[:news] ? params[:news][:description] : nil)
40 @text = (params[:news] ? params[:news][:description] : nil)
43 render :partial => 'common/preview'
41 render :partial => 'common/preview'
@@ -37,6 +37,7 class WikiController < ApplicationController
37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
39 accept_api_auth :index, :show, :update, :destroy
39 accept_api_auth :index, :show, :update, :destroy
40 before_filter :find_attachments, :only => [:preview]
40
41
41 helper :attachments
42 helper :attachments
42 include AttachmentsHelper
43 include AttachmentsHelper
@@ -293,7 +294,7 class WikiController < ApplicationController
293 # page is nil when previewing a new page
294 # page is nil when previewing a new page
294 return render_403 unless page.nil? || editable?(page)
295 return render_403 unless page.nil? || editable?(page)
295 if page
296 if page
296 @attachements = page.attachments
297 @attachments += page.attachments
297 @previewed = page.content
298 @previewed = page.content
298 end
299 end
299 @text = params[:content][:text]
300 @text = params[:content][:text]
@@ -597,8 +597,9 module ApplicationHelper
597
597
598 def parse_inline_attachments(text, project, obj, attr, only_path, options)
598 def parse_inline_attachments(text, project, obj, attr, only_path, options)
599 # when using an image link, try to use an attachment, if possible
599 # when using an image link, try to use an attachment, if possible
600 if options[:attachments] || (obj && obj.respond_to?(:attachments))
600 if options[:attachments].present? || (obj && obj.respond_to?(:attachments))
601 attachments = options[:attachments] || obj.attachments
601 attachments = options[:attachments] || []
602 attachments += obj.attachments if obj
602 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
603 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
603 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
604 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
604 # search for the picture in attachments
605 # search for the picture in attachments
@@ -154,11 +154,19 class Attachment < ActiveRecord::Base
154 end
154 end
155
155
156 def visible?(user=User.current)
156 def visible?(user=User.current)
157 container && container.attachments_visible?(user)
157 if container_id
158 container && container.attachments_visible?(user)
159 else
160 author == user
161 end
158 end
162 end
159
163
160 def deletable?(user=User.current)
164 def deletable?(user=User.current)
161 container && container.attachments_deletable?(user)
165 if container_id
166 container && container.attachments_deletable?(user)
167 else
168 author == user
169 end
162 end
170 end
163
171
164 def image?
172 def image?
@@ -1,18 +1,28
1 <span id="attachments_fields">
1 <% if defined?(container) && container && container.saved_attachments %>
2 <% if defined?(container) && container && container.saved_attachments %>
2 <% container.saved_attachments.each_with_index do |attachment, i| %>
3 <% container.saved_attachments.each_with_index do |attachment, i| %>
3 <span class="icon icon-attachment" style="display:block; line-height:1.5em;">
4 <span id="attachments_p<%= i %>">
4 <%= h(attachment.filename) %> (<%= number_to_human_size(attachment.filesize) %>)
5 <%= text_field_tag("attachments[p#{i}][filename]", attachment.filename, :class => 'filename') +
5 <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.id}.#{attachment.digest}" %>
6 text_field_tag("attachments[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') +
7 link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
8 <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.token}" %>
6 </span>
9 </span>
7 <% end %>
10 <% end %>
8 <% end %>
11 <% end %>
9 <span id="attachments_fields">
10 <span>
11 <%= file_field_tag 'attachments[1][file]', :id => nil, :class => 'file',
12 :onchange => "checkFileSize(this, #{Setting.attachment_max_size.to_i.kilobytes}, '#{escape_javascript(l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)))}');" -%>
13 <%= text_field_tag 'attachments[1][description]', '', :id => nil, :class => 'description', :maxlength => 255, :placeholder => l(:label_optional_description) %>
14 <%= link_to_function(image_tag('delete.png'), 'removeFileField(this)', :title => (l(:button_delete))) %>
15 </span>
16 </span>
12 </span>
17 <span class="add_attachment"><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;', :class => 'add_attachment' %>
13 <span class="add_attachment">
18 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)</span>
14 <%= file_field_tag 'attachments_files',
15 :id => nil,
16 :multiple => true,
17 :onchange => 'addInputFiles(this);',
18 :data => {
19 :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
20 :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
21 :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
22 :upload_path => uploads_path(:format => 'js'),
23 :description_placeholder => l(:label_optional_description)
24 } %>
25 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
26 </span>
27
28 <%= javascript_include_tag 'attachments' %>
@@ -1,3 +1,3
1 <fieldset class="preview"><legend><%= l(:label_preview) %></legend>
1 <fieldset class="preview"><legend><%= l(:label_preview) %></legend>
2 <%= textilizable @text, :attachments => @attachements, :object => @previewed %>
2 <%= textilizable @text, :attachments => @attachments, :object => @previewed %>
3 </fieldset>
3 </fieldset>
@@ -1,11 +1,11
1 <% if @notes %>
1 <% if @notes %>
2 <fieldset class="preview"><legend><%= l(:field_notes) %></legend>
2 <fieldset class="preview"><legend><%= l(:field_notes) %></legend>
3 <%= textilizable @notes, :attachments => @attachements, :object => @issue %>
3 <%= textilizable @notes, :attachments => @attachments, :object => @issue %>
4 </fieldset>
4 </fieldset>
5 <% end %>
5 <% end %>
6
6
7 <% if @description %>
7 <% if @description %>
8 <fieldset class="preview"><legend><%= l(:field_description) %></legend>
8 <fieldset class="preview"><legend><%= l(:field_description) %></legend>
9 <%= textilizable @description, :attachments => @attachements, :object => @issue %>
9 <%= textilizable @description, :attachments => @attachments, :object => @issue %>
10 </fieldset>
10 </fieldset>
11 <% end %>
11 <% end %>
@@ -188,6 +188,9 default:
188 #
188 #
189 rmagick_font_path:
189 rmagick_font_path:
190
190
191 # Maximum number of simultaneous AJAX uploads
192 #max_concurrent_ajax_uploads: 2
193
191 # specific configuration options for production environment
194 # specific configuration options for production environment
192 # that overrides the default ones
195 # that overrides the default ones
193 production:
196 production:
@@ -20,7 +20,8 module Redmine
20
20
21 # Configuration default values
21 # Configuration default values
22 @defaults = {
22 @defaults = {
23 'email_delivery' => nil
23 'email_delivery' => nil,
24 'max_concurrent_ajax_uploads' => 2
24 }
25 }
25
26
26 @config = nil
27 @config = nil
@@ -290,40 +290,6 function submit_query_form(id) {
290 $('#'+id).submit();
290 $('#'+id).submit();
291 }
291 }
292
292
293 var fileFieldCount = 1;
294 function addFileField() {
295 var fields = $('#attachments_fields');
296 if (fields.children().length >= 10) return false;
297 fileFieldCount++;
298 var s = fields.children('span').first().clone();
299 s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val('');
300 s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val('');
301 fields.append(s);
302 }
303
304 function removeFileField(el) {
305 var fields = $('#attachments_fields');
306 var s = $(el).parents('span').first();
307 if (fields.children().length > 1) {
308 s.remove();
309 } else {
310 s.children('input.file').val('');
311 s.children('input.description').val('');
312 }
313 }
314
315 function checkFileSize(el, maxSize, message) {
316 var files = el.files;
317 if (files) {
318 for (var i=0; i<files.length; i++) {
319 if (files[i].size > maxSize) {
320 alert(message);
321 el.value = "";
322 }
323 }
324 }
325 }
326
327 function showTab(name) {
293 function showTab(name) {
328 $('div#content .tab-content').hide();
294 $('div#content .tab-content').hide();
329 $('div.tabs a').removeClass('selected');
295 $('div.tabs a').removeClass('selected');
@@ -579,8 +545,8 function warnLeavingUnsaved(message) {
579 };
545 };
580
546
581 $(document).ready(function(){
547 $(document).ready(function(){
582 $('#ajax-indicator').bind('ajaxSend', function(){
548 $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings){
583 if ($('.ajax-loading').length == 0) {
549 if ($('.ajax-loading').length == 0 && settings.contentType != 'application/octet-stream') {
584 $('#ajax-indicator').show();
550 $('#ajax-indicator').show();
585 }
551 }
586 });
552 });
@@ -607,5 +573,10 function addFormObserversForDoubleSubmit() {
607 });
573 });
608 }
574 }
609
575
576 function blockEventPropagation(event) {
577 event.stopPropagation();
578 event.preventDefault();
579 }
580
610 $(document).ready(hideOnLoad);
581 $(document).ready(hideOnLoad);
611 $(document).ready(addFormObserversForDoubleSubmit);
582 $(document).ready(addFormObserversForDoubleSubmit);
@@ -527,9 +527,16 fieldset#notified_events .parent { padding-left: 20px; }
527 span.required {color: #bb0000;}
527 span.required {color: #bb0000;}
528 .summary {font-style: italic;}
528 .summary {font-style: italic;}
529
529
530 #attachments_fields input.description {margin-left: 8px; width:340px;}
530 #attachments_fields input.description {margin-left:4px; width:340px;}
531 #attachments_fields span {display:block; white-space:nowrap;}
531 #attachments_fields span {display:block; white-space:nowrap;}
532 #attachments_fields img {vertical-align: middle;}
532 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
533 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
534 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
535 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
536 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
537 a.remove-upload:hover {text-decoration:none !important;}
538
539 div.fileover { background-color: lavender; }
533
540
534 div.attachments { margin-top: 12px; }
541 div.attachments { margin-top: 12px; }
535 div.attachments p { margin:4px 0 2px 0; }
542 div.attachments p { margin:4px 0 2px 0; }
@@ -223,12 +223,21 class AttachmentsControllerTest < ActionController::TestCase
223 set_tmp_attachments_directory
223 set_tmp_attachments_directory
224 end
224 end
225
225
226 def test_show_file_without_container_should_be_denied
226 def test_show_file_without_container_should_be_allowed_to_author
227 set_tmp_attachments_directory
227 set_tmp_attachments_directory
228 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
228 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
229
229
230 @request.session[:user_id] = 2
230 @request.session[:user_id] = 2
231 get :show, :id => attachment.id
231 get :show, :id => attachment.id
232 assert_response 200
233 end
234
235 def test_show_file_without_container_should_be_allowed_to_author
236 set_tmp_attachments_directory
237 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
238
239 @request.session[:user_id] = 3
240 get :show, :id => attachment.id
232 assert_response 403
241 assert_response 403
233 end
242 end
234
243
@@ -1000,7 +1000,7 class IssuesControllerTest < ActionController::TestCase
1000 get :show, :id => 1
1000 get :show, :id => 1
1001
1001
1002 assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do
1002 assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do
1003 assert_select 'input[type=file][name=?]', 'attachments[1][file]'
1003 assert_select 'input[type=file][name=?]', 'attachments_files'
1004 end
1004 end
1005 end
1005 end
1006
1006
@@ -1569,8 +1569,7 class IssuesControllerTest < ActionController::TestCase
1569 get :new, :project_id => 1, :tracker_id => 1
1569 get :new, :project_id => 1, :tracker_id => 1
1570
1570
1571 assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do
1571 assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do
1572 assert_select 'input[name=?][type=file]', 'attachments[1][file]'
1572 assert_select 'input[name=?][type=file]', 'attachments_files'
1573 assert_select 'input[name=?][maxlength=255]', 'attachments[1][description]'
1574 end
1573 end
1575 end
1574 end
1576
1575
@@ -2165,7 +2164,7 class IssuesControllerTest < ActionController::TestCase
2165 assert_nil attachment.container
2164 assert_nil attachment.container
2166
2165
2167 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2166 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2168 assert_tag 'span', :content => /testfile.txt/
2167 assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'}
2169 end
2168 end
2170
2169
2171 def test_post_create_with_failure_should_keep_saved_attachments
2170 def test_post_create_with_failure_should_keep_saved_attachments
@@ -2184,7 +2183,7 class IssuesControllerTest < ActionController::TestCase
2184 end
2183 end
2185
2184
2186 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2185 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2187 assert_tag 'span', :content => /testfile.txt/
2186 assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'}
2188 end
2187 end
2189
2188
2190 def test_post_create_should_attach_saved_attachments
2189 def test_post_create_should_attach_saved_attachments
@@ -2967,7 +2966,7 class IssuesControllerTest < ActionController::TestCase
2967 assert_nil attachment.container
2966 assert_nil attachment.container
2968
2967
2969 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2968 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2970 assert_tag 'span', :content => /testfile.txt/
2969 assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'}
2971 end
2970 end
2972
2971
2973 def test_put_update_with_failure_should_keep_saved_attachments
2972 def test_put_update_with_failure_should_keep_saved_attachments
@@ -2986,7 +2985,7 class IssuesControllerTest < ActionController::TestCase
2986 end
2985 end
2987
2986
2988 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2987 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2989 assert_tag 'span', :content => /testfile.txt/
2988 assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'}
2990 end
2989 end
2991
2990
2992 def test_put_update_should_attach_saved_attachments
2991 def test_put_update_should_attach_saved_attachments
@@ -104,7 +104,7 class IssuesControllerTransactionTest < ActionController::TestCase
104 assert_template 'edit'
104 assert_template 'edit'
105 attachment = Attachment.first(:order => 'id DESC')
105 attachment = Attachment.first(:order => 'id DESC')
106 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
106 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
107 assert_tag 'span', :content => /testfile.txt/
107 assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'}
108 end
108 end
109
109
110 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
110 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
General Comments 0
You need to be logged in to leave comments. Login now