##// 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 # 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 AttachmentsController < ApplicationController
18 class AttachmentsController < ApplicationController
19 before_filter :find_project
19 before_filter :find_project, :except => :upload
20 before_filter :file_readable, :read_authorize, :except => :destroy
20 before_filter :file_readable, :read_authorize, :only => [:show, :download]
21 before_filter :delete_authorize, :only => :destroy
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 def show
26 def show
26 respond_to do |format|
27 respond_to do |format|
@@ -58,6 +59,29 class AttachmentsController < ApplicationController
58
59
59 end
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 verify :method => :delete, :only => :destroy
85 verify :method => :delete, :only => :destroy
62 def destroy
86 def destroy
63 # Make sure association callbacks are called
87 # Make sure association callbacks are called
@@ -149,7 +149,7 class IssuesController < ApplicationController
149
149
150 def create
150 def create
151 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
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 if @issue.save
153 if @issue.save
154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
155 respond_to do |format|
155 respond_to do |format|
@@ -181,7 +181,7 class IssuesController < ApplicationController
181
181
182 def update
182 def update
183 return unless update_issue_from_params
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 saved = false
185 saved = false
186 begin
186 begin
187 saved = @issue.save_issue_with_child_records(params, @time_entry)
187 saved = @issue.save_issue_with_child_records(params, @time_entry)
@@ -76,21 +76,32 class Attachment < ActiveRecord::Base
76 unless incoming_file.nil?
76 unless incoming_file.nil?
77 @temp_file = incoming_file
77 @temp_file = incoming_file
78 if @temp_file.size > 0
78 if @temp_file.size > 0
79 self.filename = sanitize_filename(@temp_file.original_filename)
79 if @temp_file.respond_to?(:original_filename)
80 self.disk_filename = Attachment.disk_filename(filename)
80 self.filename = @temp_file.original_filename
81 self.content_type = @temp_file.content_type.to_s.chomp
81 end
82 if content_type.blank?
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 self.content_type = Redmine::MimeType.of(filename)
86 self.content_type = Redmine::MimeType.of(filename)
84 end
87 end
85 self.filesize = @temp_file.size
88 self.filesize = @temp_file.size
86 end
89 end
87 end
90 end
88 end
91 end
89
92
90 def file
93 def file
91 nil
94 nil
92 end
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 # Copies the temporary file to its final location
105 # Copies the temporary file to its final location
95 # and computes its MD5 hash
106 # and computes its MD5 hash
96 def files_to_final_location
107 def files_to_final_location
@@ -409,6 +409,8 ActionController::Routing::Routes.draw do |map|
409 :conditions => {:method => :get}
409 :conditions => {:method => :get}
410 end
410 end
411
411
412 map.connect 'uploads.:format', :controller => 'attachments', :action => 'upload', :conditions => {:method => :post}
413
412 map.connect 'robots.txt', :controller => 'welcome',
414 map.connect 'robots.txt', :controller => 'welcome',
413 :action => 'robots', :conditions => {:method => :get}
415 :action => 'robots', :conditions => {:method => :get}
414
416
@@ -67,13 +67,13 Redmine::AccessControl.map do |map|
67 :journals => [:index, :diff],
67 :journals => [:index, :diff],
68 :queries => :index,
68 :queries => :index,
69 :reports => [:issue_report, :issue_report_details]}
69 :reports => [:issue_report, :issue_report_details]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
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]}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
72 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
72 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
73 map.permission :manage_subtasks, {}
73 map.permission :manage_subtasks, {}
74 map.permission :set_issues_private, {}
74 map.permission :set_issues_private, {}
75 map.permission :set_own_issues_private, {}, :require => :loggedin
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 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
77 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
78 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
78 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
79 map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
79 map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
@@ -82,4 +82,39 class ApiTest::AttachmentsTest < ActionController::IntegrationTest
82 end
82 end
83 end
83 end
84 end
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 end
120 end
@@ -707,4 +707,72 class ApiTest::IssuesTest < ActionController::IntegrationTest
707 assert_nil Issue.find_by_id(6)
707 assert_nil Issue.find_by_id(6)
708 end
708 end
709 end
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 end
778 end
@@ -49,5 +49,13 class RoutingAttachmentsTest < ActionController::IntegrationTest
49 { :method => 'delete', :path => "/attachments/1" },
49 { :method => 'delete', :path => "/attachments/1" },
50 { :controller => 'attachments', :action => 'destroy', :id => '1' }
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 end
60 end
53 end
61 end
@@ -61,18 +61,23 module Redmine
61 end
61 end
62
62
63 def save_attachments(attachments, author=User.current)
63 def save_attachments(attachments, author=User.current)
64 if attachments && attachments.is_a?(Hash)
64 if attachments.is_a?(Hash)
65 attachments.each_value do |attachment|
65 attachments = attachments.values
66 end
67 if attachments.is_a?(Array)
68 attachments.each do |attachment|
66 a = nil
69 a = nil
67 if file = attachment['file']
70 if file = attachment['file']
68 next unless file && file.size > 0
71 next unless file.size > 0
69 a = Attachment.create(:file => file,
72 a = Attachment.create(:file => file, :author => author)
70 :description => attachment['description'].to_s.strip,
71 :author => author)
72 elsif token = attachment['token']
73 elsif token = attachment['token']
73 a = Attachment.find_by_token(token)
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 end
78 end
75 next unless a
79 next unless a
80 a.description = attachment['description'].to_s.strip
76 if a.new_record?
81 if a.new_record?
77 unsaved_attachments << a
82 unsaved_attachments << a
78 else
83 else
General Comments 0
You need to be logged in to leave comments. Login now