##// END OF EJS Templates
Adds support for adding attachments to issues through the REST API (#8171)....
Jean-Philippe Lang -
r8808:77626ef6fbf2
parent child
Show More
@@ -0,0 +1,3
1 api.upload do
2 api.token @attachment.token
3 end
@@ -16,11 +16,12
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AttachmentsController < ApplicationController
19 before_filter :find_project
20 before_filter :file_readable, :read_authorize, :except => :destroy
19 before_filter :find_project, :except => :upload
20 before_filter :file_readable, :read_authorize, :only => [:show, :download]
21 21 before_filter :delete_authorize, :only => :destroy
22 before_filter :authorize_global, :only => :upload
22 23
23 accept_api_auth :show, :download
24 accept_api_auth :show, :download, :upload
24 25
25 26 def show
26 27 respond_to do |format|
@@ -58,6 +59,29 class AttachmentsController < ApplicationController
58 59
59 60 end
60 61
62 def upload
63 # Make sure that API users get used to set this content type
64 # as it won't trigger Rails' automatic parsing of the request body for parameters
65 unless request.content_type == 'application/octet-stream'
66 render :nothing => true, :status => 406
67 return
68 end
69
70 @attachment = Attachment.new(:file => request.body)
71 @attachment.author = User.current
72 @attachment.filename = "test" #ActiveSupport::SecureRandom.hex(16)
73
74 if @attachment.save
75 respond_to do |format|
76 format.api { render :action => 'upload', :status => :created }
77 end
78 else
79 respond_to do |format|
80 format.api { render_validation_errors(@attachment) }
81 end
82 end
83 end
84
61 85 verify :method => :delete, :only => :destroy
62 86 def destroy
63 87 # Make sure association callbacks are called
@@ -149,7 +149,7 class IssuesController < ApplicationController
149 149
150 150 def create
151 151 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
152 @issue.save_attachments(params[:attachments])
152 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
153 153 if @issue.save
154 154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
155 155 respond_to do |format|
@@ -181,7 +181,7 class IssuesController < ApplicationController
181 181
182 182 def update
183 183 return unless update_issue_from_params
184 @issue.save_attachments(params[:attachments])
184 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
185 185 saved = false
186 186 begin
187 187 saved = @issue.save_issue_with_child_records(params, @time_entry)
@@ -76,21 +76,32 class Attachment < ActiveRecord::Base
76 76 unless incoming_file.nil?
77 77 @temp_file = incoming_file
78 78 if @temp_file.size > 0
79 self.filename = sanitize_filename(@temp_file.original_filename)
80 self.disk_filename = Attachment.disk_filename(filename)
81 self.content_type = @temp_file.content_type.to_s.chomp
82 if content_type.blank?
79 if @temp_file.respond_to?(:original_filename)
80 self.filename = @temp_file.original_filename
81 end
82 if @temp_file.respond_to?(:content_type)
83 self.content_type = @temp_file.content_type.to_s.chomp
84 end
85 if content_type.blank? && filename.present?
83 86 self.content_type = Redmine::MimeType.of(filename)
84 87 end
85 88 self.filesize = @temp_file.size
86 89 end
87 90 end
88 91 end
89
92
90 93 def file
91 94 nil
92 95 end
93 96
97 def filename=(arg)
98 write_attribute :filename, sanitize_filename(arg.to_s)
99 if new_record? && disk_filename.blank?
100 self.disk_filename = Attachment.disk_filename(filename)
101 end
102 filename
103 end
104
94 105 # Copies the temporary file to its final location
95 106 # and computes its MD5 hash
96 107 def files_to_final_location
@@ -409,6 +409,8 ActionController::Routing::Routes.draw do |map|
409 409 :conditions => {:method => :get}
410 410 end
411 411
412 map.connect 'uploads.:format', :controller => 'attachments', :action => 'upload', :conditions => {:method => :post}
413
412 414 map.connect 'robots.txt', :controller => 'welcome',
413 415 :action => 'robots', :conditions => {:method => :get}
414 416
@@ -67,13 +67,13 Redmine::AccessControl.map do |map|
67 67 :journals => [:index, :diff],
68 68 :queries => :index,
69 69 :reports => [:issue_report, :issue_report_details]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
72 72 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
73 73 map.permission :manage_subtasks, {}
74 74 map.permission :set_issues_private, {}
75 75 map.permission :set_own_issues_private, {}, :require => :loggedin
76 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
76 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
77 77 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
78 78 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
79 79 map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
@@ -82,4 +82,39 class ApiTest::AttachmentsTest < ActionController::IntegrationTest
82 82 end
83 83 end
84 84 end
85
86 context "POST /uploads" do
87 should "return the token" do
88 set_tmp_attachments_directory
89 assert_difference 'Attachment.count' do
90 post '/uploads.xml', 'File content', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith'))
91 assert_response :created
92 assert_equal 'application/xml', response.content_type
93
94 xml = Hash.from_xml(response.body)
95 assert_kind_of Hash, xml['upload']
96 token = xml['upload']['token']
97 assert_not_nil token
98
99 attachment = Attachment.first(:order => 'id DESC')
100 assert_equal token, attachment.token
101 assert_nil attachment.container
102 assert_equal 2, attachment.author_id
103 assert_equal 'File content'.size, attachment.filesize
104 assert attachment.content_type.blank?
105 assert attachment.filename.present?
106 assert_match /\d+_[0-9a-z]+/, attachment.diskfile
107 assert File.exist?(attachment.diskfile)
108 assert_equal 'File content', File.read(attachment.diskfile)
109 end
110 end
111
112 should "not accept other content types" do
113 set_tmp_attachments_directory
114 assert_no_difference 'Attachment.count' do
115 post '/uploads.xml', 'PNG DATA', {'Content-Type' => 'image/png'}.merge(credentials('jsmith'))
116 assert_response 406
117 end
118 end
119 end
85 120 end
@@ -707,4 +707,72 class ApiTest::IssuesTest < ActionController::IntegrationTest
707 707 assert_nil Issue.find_by_id(6)
708 708 end
709 709 end
710
711 def test_create_issue_with_uploaded_file
712 set_tmp_attachments_directory
713
714 # upload the file
715 assert_difference 'Attachment.count' do
716 post '/uploads.xml', 'test_create_with_upload', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith'))
717 assert_response :created
718 end
719 xml = Hash.from_xml(response.body)
720 token = xml['upload']['token']
721 attachment = Attachment.first(:order => 'id DESC')
722
723 # create the issue with the upload's token
724 assert_difference 'Issue.count' do
725 post '/issues.xml',
726 {:issue => {:project_id => 1, :subject => 'Uploaded file', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
727 credentials('jsmith')
728 assert_response :created
729 end
730 issue = Issue.first(:order => 'id DESC')
731 assert_equal 1, issue.attachments.count
732 assert_equal attachment, issue.attachments.first
733
734 attachment.reload
735 assert_equal 'test.txt', attachment.filename
736 assert_equal 'text/plain', attachment.content_type
737 assert_equal 'test_create_with_upload'.size, attachment.filesize
738 assert_equal 2, attachment.author_id
739
740 # get the issue with its attachments
741 get "/issues/#{issue.id}.xml", :include => 'attachments'
742 assert_response :success
743 xml = Hash.from_xml(response.body)
744 attachments = xml['issue']['attachments']
745 assert_kind_of Array, attachments
746 assert_equal 1, attachments.size
747 url = attachments.first['content_url']
748 assert_not_nil url
749
750 # download the attachment
751 get url
752 assert_response :success
753 end
754
755 def test_update_issue_with_uploaded_file
756 set_tmp_attachments_directory
757
758 # upload the file
759 assert_difference 'Attachment.count' do
760 post '/uploads.xml', 'test_upload_with_upload', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith'))
761 assert_response :created
762 end
763 xml = Hash.from_xml(response.body)
764 token = xml['upload']['token']
765 attachment = Attachment.first(:order => 'id DESC')
766
767 # update the issue with the upload's token
768 assert_difference 'Journal.count' do
769 put '/issues/1.xml',
770 {:issue => {:notes => 'Attachment added', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
771 credentials('jsmith')
772 assert_response :ok
773 end
774
775 issue = Issue.find(1)
776 assert_include attachment, issue.attachments
777 end
710 778 end
@@ -49,5 +49,13 class RoutingAttachmentsTest < ActionController::IntegrationTest
49 49 { :method => 'delete', :path => "/attachments/1" },
50 50 { :controller => 'attachments', :action => 'destroy', :id => '1' }
51 51 )
52 assert_routing(
53 { :method => 'post', :path => '/uploads.xml' },
54 { :controller => 'attachments', :action => 'upload', :format => 'xml' }
55 )
56 assert_routing(
57 { :method => 'post', :path => '/uploads.json' },
58 { :controller => 'attachments', :action => 'upload', :format => 'json' }
59 )
52 60 end
53 61 end
@@ -61,18 +61,23 module Redmine
61 61 end
62 62
63 63 def save_attachments(attachments, author=User.current)
64 if attachments && attachments.is_a?(Hash)
65 attachments.each_value do |attachment|
64 if attachments.is_a?(Hash)
65 attachments = attachments.values
66 end
67 if attachments.is_a?(Array)
68 attachments.each do |attachment|
66 69 a = nil
67 70 if file = attachment['file']
68 next unless file && file.size > 0
69 a = Attachment.create(:file => file,
70 :description => attachment['description'].to_s.strip,
71 :author => author)
71 next unless file.size > 0
72 a = Attachment.create(:file => file, :author => author)
72 73 elsif token = attachment['token']
73 74 a = Attachment.find_by_token(token)
75 next unless a
76 a.filename = attachment['filename'] unless attachment['filename'].blank?
77 a.content_type = attachment['content_type']
74 78 end
75 79 next unless a
80 a.description = attachment['description'].to_s.strip
76 81 if a.new_record?
77 82 unsaved_attachments << a
78 83 else
General Comments 0
You need to be logged in to leave comments. Login now