@@ -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 |
@@ -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> </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 | 300 | render_404 |
|
301 | 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 | 313 | # make sure that the user is a member of the project (or admin) if project is private |
|
304 | 314 | # used as a before_filter for actions that do not require any particular permission on the project |
|
305 | 315 | def check_project_privacy |
@@ -85,15 +85,17 class AttachmentsController < ApplicationController | |||
|
85 | 85 | @attachment = Attachment.new(:file => request.raw_post) |
|
86 | 86 | @attachment.author = User.current |
|
87 | 87 | @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16) |
|
88 | saved = @attachment.save | |
|
88 | 89 | |
|
89 | if @attachment.save | |
|
90 | 90 |
|
|
91 | format.api { render :action => 'upload', :status => :created } | |
|
92 | end | |
|
91 | format.js | |
|
92 | format.api { | |
|
93 | if saved | |
|
94 | render :action => 'upload', :status => :created | |
|
93 | 95 | else |
|
94 | respond_to do |format| | |
|
95 | format.api { render_validation_errors(@attachment) } | |
|
96 | render_validation_errors(@attachment) | |
|
96 | 97 | end |
|
98 | } | |
|
97 | 99 | end |
|
98 | 100 | end |
|
99 | 101 | |
@@ -101,9 +103,17 class AttachmentsController < ApplicationController | |||
|
101 | 103 | if @attachment.container.respond_to?(:init_journal) |
|
102 | 104 | @attachment.container.init_journal(User.current) |
|
103 | 105 | end |
|
106 | if @attachment.container | |
|
104 | 107 | # Make sure association callbacks are called |
|
105 | 108 | @attachment.container.attachments.delete(@attachment) |
|
106 | redirect_to_referer_or project_path(@project) | |
|
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 | 117 | end |
|
108 | 118 | |
|
109 | 119 | private |
@@ -19,6 +19,7 class MessagesController < ApplicationController | |||
|
19 | 19 | menu_item :boards |
|
20 | 20 | default_search_scope :messages |
|
21 | 21 | before_filter :find_board, :only => [:new, :preview] |
|
22 | before_filter :find_attachments, :only => [:preview] | |
|
22 | 23 | before_filter :find_message, :except => [:new, :preview] |
|
23 | 24 | before_filter :authorize, :except => [:preview, :edit, :destroy] |
|
24 | 25 | |
@@ -117,7 +118,6 class MessagesController < ApplicationController | |||
|
117 | 118 | |
|
118 | 119 | def preview |
|
119 | 120 | message = @board.messages.find_by_id(params[:id]) |
|
120 | @attachements = message.attachments if message | |
|
121 | 121 | @text = (params[:message] || params[:reply])[:content] |
|
122 | 122 | @previewed = message |
|
123 | 123 | render :partial => 'common/preview' |
@@ -16,12 +16,11 | |||
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class PreviewsController < ApplicationController |
|
19 | before_filter :find_project | |
|
19 | before_filter :find_project, :find_attachments | |
|
20 | 20 | |
|
21 | 21 | def issue |
|
22 | 22 | @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? |
|
23 | 23 | if @issue |
|
24 | @attachements = @issue.attachments | |
|
25 | 24 | @description = params[:issue] && params[:issue][:description] |
|
26 | 25 | if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n") |
|
27 | 26 | @description = nil |
@@ -37,7 +36,6 class PreviewsController < ApplicationController | |||
|
37 | 36 | def news |
|
38 | 37 | if params[:id].present? && news = News.visible.find_by_id(params[:id]) |
|
39 | 38 | @previewed = news |
|
40 | @attachments = news.attachments | |
|
41 | 39 | end |
|
42 | 40 | @text = (params[:news] ? params[:news][:description] : nil) |
|
43 | 41 | render :partial => 'common/preview' |
@@ -37,6 +37,7 class WikiController < ApplicationController | |||
|
37 | 37 | before_filter :find_existing_or_new_page, :only => [:show, :edit, :update] |
|
38 | 38 | before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version] |
|
39 | 39 | accept_api_auth :index, :show, :update, :destroy |
|
40 | before_filter :find_attachments, :only => [:preview] | |
|
40 | 41 | |
|
41 | 42 | helper :attachments |
|
42 | 43 | include AttachmentsHelper |
@@ -293,7 +294,7 class WikiController < ApplicationController | |||
|
293 | 294 | # page is nil when previewing a new page |
|
294 | 295 | return render_403 unless page.nil? || editable?(page) |
|
295 | 296 | if page |
|
296 |
@attach |
|
|
297 | @attachments += page.attachments | |
|
297 | 298 | @previewed = page.content |
|
298 | 299 | end |
|
299 | 300 | @text = params[:content][:text] |
@@ -597,8 +597,9 module ApplicationHelper | |||
|
597 | 597 | |
|
598 | 598 | def parse_inline_attachments(text, project, obj, attr, only_path, options) |
|
599 | 599 | # when using an image link, try to use an attachment, if possible |
|
600 | if options[:attachments] || (obj && obj.respond_to?(:attachments)) | |
|
601 |
attachments = options[:attachments] || |
|
|
600 | if options[:attachments].present? || (obj && obj.respond_to?(:attachments)) | |
|
601 | attachments = options[:attachments] || [] | |
|
602 | attachments += obj.attachments if obj | |
|
602 | 603 | text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
|
603 | 604 | filename, ext, alt, alttext = $1.downcase, $2, $3, $4 |
|
604 | 605 | # search for the picture in attachments |
@@ -154,11 +154,19 class Attachment < ActiveRecord::Base | |||
|
154 | 154 | end |
|
155 | 155 | |
|
156 | 156 | def visible?(user=User.current) |
|
157 | if container_id | |
|
157 | 158 | container && container.attachments_visible?(user) |
|
159 | else | |
|
160 | author == user | |
|
161 | end | |
|
158 | 162 | end |
|
159 | 163 | |
|
160 | 164 | def deletable?(user=User.current) |
|
165 | if container_id | |
|
161 | 166 | container && container.attachments_deletable?(user) |
|
167 | else | |
|
168 | author == user | |
|
169 | end | |
|
162 | 170 | end |
|
163 | 171 | |
|
164 | 172 | def image? |
@@ -1,18 +1,28 | |||
|
1 | <span id="attachments_fields"> | |
|
1 | 2 | <% if defined?(container) && container && container.saved_attachments %> |
|
2 | 3 | <% container.saved_attachments.each_with_index do |attachment, i| %> |
|
3 | <span class="icon icon-attachment" style="display:block; line-height:1.5em;"> | |
|
4 | <%= h(attachment.filename) %> (<%= number_to_human_size(attachment.filesize) %>) | |
|
5 | <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.id}.#{attachment.digest}" %> | |
|
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}" %> | |
|
6 | 9 | </span> |
|
7 | 10 | <% end %> |
|
8 | 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 | 12 |
|
|
13 | <span class="add_attachment"> | |
|
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) %>) | |
|
16 | 26 | </span> |
|
17 | <span class="add_attachment"><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;', :class => 'add_attachment' %> | |
|
18 | (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)</span> | |
|
27 | ||
|
28 | <%= javascript_include_tag 'attachments' %> |
@@ -1,3 +1,3 | |||
|
1 | 1 | <fieldset class="preview"><legend><%= l(:label_preview) %></legend> |
|
2 |
<%= textilizable @text, :attachments => @attach |
|
|
2 | <%= textilizable @text, :attachments => @attachments, :object => @previewed %> | |
|
3 | 3 | </fieldset> |
@@ -1,11 +1,11 | |||
|
1 | 1 | <% if @notes %> |
|
2 | 2 | <fieldset class="preview"><legend><%= l(:field_notes) %></legend> |
|
3 |
<%= textilizable @notes, :attachments => @attach |
|
|
3 | <%= textilizable @notes, :attachments => @attachments, :object => @issue %> | |
|
4 | 4 | </fieldset> |
|
5 | 5 | <% end %> |
|
6 | 6 | |
|
7 | 7 | <% if @description %> |
|
8 | 8 | <fieldset class="preview"><legend><%= l(:field_description) %></legend> |
|
9 |
<%= textilizable @description, :attachments => @attach |
|
|
9 | <%= textilizable @description, :attachments => @attachments, :object => @issue %> | |
|
10 | 10 | </fieldset> |
|
11 | 11 | <% end %> |
@@ -188,6 +188,9 default: | |||
|
188 | 188 | # |
|
189 | 189 | rmagick_font_path: |
|
190 | 190 | |
|
191 | # Maximum number of simultaneous AJAX uploads | |
|
192 | #max_concurrent_ajax_uploads: 2 | |
|
193 | ||
|
191 | 194 | # specific configuration options for production environment |
|
192 | 195 | # that overrides the default ones |
|
193 | 196 | production: |
@@ -20,7 +20,8 module Redmine | |||
|
20 | 20 | |
|
21 | 21 | # Configuration default values |
|
22 | 22 | @defaults = { |
|
23 | 'email_delivery' => nil | |
|
23 | 'email_delivery' => nil, | |
|
24 | 'max_concurrent_ajax_uploads' => 2 | |
|
24 | 25 | } |
|
25 | 26 | |
|
26 | 27 | @config = nil |
@@ -290,40 +290,6 function submit_query_form(id) { | |||
|
290 | 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 | 293 | function showTab(name) { |
|
328 | 294 | $('div#content .tab-content').hide(); |
|
329 | 295 | $('div.tabs a').removeClass('selected'); |
@@ -579,8 +545,8 function warnLeavingUnsaved(message) { | |||
|
579 | 545 | }; |
|
580 | 546 | |
|
581 | 547 | $(document).ready(function(){ |
|
582 | $('#ajax-indicator').bind('ajaxSend', function(){ | |
|
583 | if ($('.ajax-loading').length == 0) { | |
|
548 | $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings){ | |
|
549 | if ($('.ajax-loading').length == 0 && settings.contentType != 'application/octet-stream') { | |
|
584 | 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 | 581 | $(document).ready(hideOnLoad); |
|
611 | 582 | $(document).ready(addFormObserversForDoubleSubmit); |
@@ -527,9 +527,16 fieldset#notified_events .parent { padding-left: 20px; } | |||
|
527 | 527 | span.required {color: #bb0000;} |
|
528 | 528 | .summary {font-style: italic;} |
|
529 | 529 | |
|
530 |
#attachments_fields input.description {margin-left: |
|
|
530 | #attachments_fields input.description {margin-left:4px; width:340px;} | |
|
531 | 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 | 541 | div.attachments { margin-top: 12px; } |
|
535 | 542 | div.attachments p { margin:4px 0 2px 0; } |
@@ -223,12 +223,21 class AttachmentsControllerTest < ActionController::TestCase | |||
|
223 | 223 | set_tmp_attachments_directory |
|
224 | 224 | end |
|
225 | 225 | |
|
226 |
def test_show_file_without_container_should_be_ |
|
|
226 | def test_show_file_without_container_should_be_allowed_to_author | |
|
227 | 227 | set_tmp_attachments_directory |
|
228 | 228 | attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) |
|
229 | 229 | |
|
230 | 230 | @request.session[:user_id] = 2 |
|
231 | 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 | 241 | assert_response 403 |
|
233 | 242 | end |
|
234 | 243 |
@@ -1000,7 +1000,7 class IssuesControllerTest < ActionController::TestCase | |||
|
1000 | 1000 | get :show, :id => 1 |
|
1001 | 1001 | |
|
1002 | 1002 | assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do |
|
1003 |
assert_select 'input[type=file][name=?]', 'attachments |
|
|
1003 | assert_select 'input[type=file][name=?]', 'attachments_files' | |
|
1004 | 1004 | end |
|
1005 | 1005 | end |
|
1006 | 1006 | |
@@ -1569,8 +1569,7 class IssuesControllerTest < ActionController::TestCase | |||
|
1569 | 1569 | get :new, :project_id => 1, :tracker_id => 1 |
|
1570 | 1570 | |
|
1571 | 1571 | assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do |
|
1572 |
assert_select 'input[name=?][type=file]', 'attachments |
|
|
1573 | assert_select 'input[name=?][maxlength=255]', 'attachments[1][description]' | |
|
1572 | assert_select 'input[name=?][type=file]', 'attachments_files' | |
|
1574 | 1573 | end |
|
1575 | 1574 | end |
|
1576 | 1575 | |
@@ -2165,7 +2164,7 class IssuesControllerTest < ActionController::TestCase | |||
|
2165 | 2164 | assert_nil attachment.container |
|
2166 | 2165 | |
|
2167 | 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 | 2168 | end |
|
2170 | 2169 | |
|
2171 | 2170 | def test_post_create_with_failure_should_keep_saved_attachments |
@@ -2184,7 +2183,7 class IssuesControllerTest < ActionController::TestCase | |||
|
2184 | 2183 | end |
|
2185 | 2184 | |
|
2186 | 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 | 2187 | end |
|
2189 | 2188 | |
|
2190 | 2189 | def test_post_create_should_attach_saved_attachments |
@@ -2967,7 +2966,7 class IssuesControllerTest < ActionController::TestCase | |||
|
2967 | 2966 | assert_nil attachment.container |
|
2968 | 2967 | |
|
2969 | 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 | 2970 | end |
|
2972 | 2971 | |
|
2973 | 2972 | def test_put_update_with_failure_should_keep_saved_attachments |
@@ -2986,7 +2985,7 class IssuesControllerTest < ActionController::TestCase | |||
|
2986 | 2985 | end |
|
2987 | 2986 | |
|
2988 | 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 | 2989 | end |
|
2991 | 2990 | |
|
2992 | 2991 | def test_put_update_should_attach_saved_attachments |
@@ -104,7 +104,7 class IssuesControllerTransactionTest < ActionController::TestCase | |||
|
104 | 104 | assert_template 'edit' |
|
105 | 105 | attachment = Attachment.first(:order => 'id DESC') |
|
106 | 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 | 108 | end |
|
109 | 109 | |
|
110 | 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