@@ -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> </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 |
e |
|
93 | if saved | |
93 | else |
|
94 | render :action => 'upload', :status => :created | |
94 | respond_to do |format| |
|
95 | else | |
95 |
|
|
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 |
@attach |
|
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] || |
|
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(' '.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 => @attach |
|
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 => @attach |
|
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 => @attach |
|
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: |
|
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_ |
|
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 |
|
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 |
|
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