##// END OF EJS Templates
Respond with errors and appropriate content type on /issues API calls with invalid query params (#8883)....
Jean-Philippe Lang -
r6189:fdd5367ebab2
parent child
Show More
@@ -1,336 +1,338
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 before_filter :find_project, :only => [:new, :create]
25 before_filter :find_project, :only => [:new, :create]
26 before_filter :authorize, :except => [:index]
26 before_filter :authorize, :except => [:index]
27 before_filter :find_optional_project, :only => [:index]
27 before_filter :find_optional_project, :only => [:index]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 accept_rss_auth :index, :show
30 accept_rss_auth :index, :show
31 accept_api_auth :index, :show, :create, :update, :destroy
31 accept_api_auth :index, :show, :create, :update, :destroy
32
32
33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34
34
35 helper :journals
35 helper :journals
36 helper :projects
36 helper :projects
37 include ProjectsHelper
37 include ProjectsHelper
38 helper :custom_fields
38 helper :custom_fields
39 include CustomFieldsHelper
39 include CustomFieldsHelper
40 helper :issue_relations
40 helper :issue_relations
41 include IssueRelationsHelper
41 include IssueRelationsHelper
42 helper :watchers
42 helper :watchers
43 include WatchersHelper
43 include WatchersHelper
44 helper :attachments
44 helper :attachments
45 include AttachmentsHelper
45 include AttachmentsHelper
46 helper :queries
46 helper :queries
47 include QueriesHelper
47 include QueriesHelper
48 helper :repositories
48 helper :repositories
49 include RepositoriesHelper
49 include RepositoriesHelper
50 helper :sort
50 helper :sort
51 include SortHelper
51 include SortHelper
52 include IssuesHelper
52 include IssuesHelper
53 helper :timelog
53 helper :timelog
54 helper :gantt
54 helper :gantt
55 include Redmine::Export::PDF
55 include Redmine::Export::PDF
56
56
57 verify :method => [:post, :delete],
57 verify :method => [:post, :delete],
58 :only => :destroy,
58 :only => :destroy,
59 :render => { :nothing => true, :status => :method_not_allowed }
59 :render => { :nothing => true, :status => :method_not_allowed }
60
60
61 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
61 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
62 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
62 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
63 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
63 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
64
64
65 def index
65 def index
66 retrieve_query
66 retrieve_query
67 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
67 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
68 sort_update(@query.sortable_columns)
68 sort_update(@query.sortable_columns)
69
69
70 if @query.valid?
70 if @query.valid?
71 case params[:format]
71 case params[:format]
72 when 'csv', 'pdf'
72 when 'csv', 'pdf'
73 @limit = Setting.issues_export_limit.to_i
73 @limit = Setting.issues_export_limit.to_i
74 when 'atom'
74 when 'atom'
75 @limit = Setting.feeds_limit.to_i
75 @limit = Setting.feeds_limit.to_i
76 when 'xml', 'json'
76 when 'xml', 'json'
77 @offset, @limit = api_offset_and_limit
77 @offset, @limit = api_offset_and_limit
78 else
78 else
79 @limit = per_page_option
79 @limit = per_page_option
80 end
80 end
81
81
82 @issue_count = @query.issue_count
82 @issue_count = @query.issue_count
83 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
83 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
84 @offset ||= @issue_pages.current.offset
84 @offset ||= @issue_pages.current.offset
85 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
85 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
86 :order => sort_clause,
86 :order => sort_clause,
87 :offset => @offset,
87 :offset => @offset,
88 :limit => @limit)
88 :limit => @limit)
89 @issue_count_by_group = @query.issue_count_by_group
89 @issue_count_by_group = @query.issue_count_by_group
90
90
91 respond_to do |format|
91 respond_to do |format|
92 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
92 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
93 format.api
93 format.api
94 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
94 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
95 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
95 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
96 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
96 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
97 end
97 end
98 else
98 else
99 # Send html if the query is not valid
99 respond_to do |format|
100 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
100 format.any(:html, :atom, :csv, :pdf) { render(:template => 'issues/index.rhtml', :layout => !request.xhr?) }
101 format.api { render_validation_errors(@query) }
102 end
101 end
103 end
102 rescue ActiveRecord::RecordNotFound
104 rescue ActiveRecord::RecordNotFound
103 render_404
105 render_404
104 end
106 end
105
107
106 def show
108 def show
107 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 @journals.each_with_index {|j,i| j.indice = i+1}
110 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110
112
111 if User.current.allowed_to?(:view_changesets, @project)
113 if User.current.allowed_to?(:view_changesets, @project)
112 @changesets = @issue.changesets.visible.all
114 @changesets = @issue.changesets.visible.all
113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 end
116 end
115
117
116 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
118 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
117 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
118 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
119 @priorities = IssuePriority.active
121 @priorities = IssuePriority.active
120 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
122 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
121 respond_to do |format|
123 respond_to do |format|
122 format.html { render :template => 'issues/show.rhtml' }
124 format.html { render :template => 'issues/show.rhtml' }
123 format.api
125 format.api
124 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
126 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
125 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
127 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
126 end
128 end
127 end
129 end
128
130
129 # Add a new issue
131 # Add a new issue
130 # The new issue will be created from an existing one if copy_from parameter is given
132 # The new issue will be created from an existing one if copy_from parameter is given
131 def new
133 def new
132 respond_to do |format|
134 respond_to do |format|
133 format.html { render :action => 'new', :layout => !request.xhr? }
135 format.html { render :action => 'new', :layout => !request.xhr? }
134 format.js { render :partial => 'attributes' }
136 format.js { render :partial => 'attributes' }
135 end
137 end
136 end
138 end
137
139
138 def create
140 def create
139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
141 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
140 if @issue.save
142 if @issue.save
141 attachments = Attachment.attach_files(@issue, params[:attachments])
143 attachments = Attachment.attach_files(@issue, params[:attachments])
142 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
143 respond_to do |format|
145 respond_to do |format|
144 format.html {
146 format.html {
145 render_attachment_warning_if_needed(@issue)
147 render_attachment_warning_if_needed(@issue)
146 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
148 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
147 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
149 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
148 { :action => 'show', :id => @issue })
150 { :action => 'show', :id => @issue })
149 }
151 }
150 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
152 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
151 end
153 end
152 return
154 return
153 else
155 else
154 respond_to do |format|
156 respond_to do |format|
155 format.html { render :action => 'new' }
157 format.html { render :action => 'new' }
156 format.api { render_validation_errors(@issue) }
158 format.api { render_validation_errors(@issue) }
157 end
159 end
158 end
160 end
159 end
161 end
160
162
161 def edit
163 def edit
162 update_issue_from_params
164 update_issue_from_params
163
165
164 @journal = @issue.current_journal
166 @journal = @issue.current_journal
165
167
166 respond_to do |format|
168 respond_to do |format|
167 format.html { }
169 format.html { }
168 format.xml { }
170 format.xml { }
169 end
171 end
170 end
172 end
171
173
172 def update
174 def update
173 update_issue_from_params
175 update_issue_from_params
174
176
175 if @issue.save_issue_with_child_records(params, @time_entry)
177 if @issue.save_issue_with_child_records(params, @time_entry)
176 render_attachment_warning_if_needed(@issue)
178 render_attachment_warning_if_needed(@issue)
177 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
179 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
178
180
179 respond_to do |format|
181 respond_to do |format|
180 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
182 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
181 format.api { head :ok }
183 format.api { head :ok }
182 end
184 end
183 else
185 else
184 render_attachment_warning_if_needed(@issue)
186 render_attachment_warning_if_needed(@issue)
185 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
187 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
186 @journal = @issue.current_journal
188 @journal = @issue.current_journal
187
189
188 respond_to do |format|
190 respond_to do |format|
189 format.html { render :action => 'edit' }
191 format.html { render :action => 'edit' }
190 format.api { render_validation_errors(@issue) }
192 format.api { render_validation_errors(@issue) }
191 end
193 end
192 end
194 end
193 end
195 end
194
196
195 # Bulk edit a set of issues
197 # Bulk edit a set of issues
196 def bulk_edit
198 def bulk_edit
197 @issues.sort!
199 @issues.sort!
198 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
200 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
199 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
201 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
200 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
202 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
201 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
203 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
202 end
204 end
203
205
204 def bulk_update
206 def bulk_update
205 @issues.sort!
207 @issues.sort!
206 attributes = parse_params_for_bulk_issue_attributes(params)
208 attributes = parse_params_for_bulk_issue_attributes(params)
207
209
208 unsaved_issue_ids = []
210 unsaved_issue_ids = []
209 @issues.each do |issue|
211 @issues.each do |issue|
210 issue.reload
212 issue.reload
211 journal = issue.init_journal(User.current, params[:notes])
213 journal = issue.init_journal(User.current, params[:notes])
212 issue.safe_attributes = attributes
214 issue.safe_attributes = attributes
213 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
215 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
214 unless issue.save
216 unless issue.save
215 # Keep unsaved issue ids to display them in flash error
217 # Keep unsaved issue ids to display them in flash error
216 unsaved_issue_ids << issue.id
218 unsaved_issue_ids << issue.id
217 end
219 end
218 end
220 end
219 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
221 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
220 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
222 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
221 end
223 end
222
224
223 def destroy
225 def destroy
224 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
226 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
225 if @hours > 0
227 if @hours > 0
226 case params[:todo]
228 case params[:todo]
227 when 'destroy'
229 when 'destroy'
228 # nothing to do
230 # nothing to do
229 when 'nullify'
231 when 'nullify'
230 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
232 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
231 when 'reassign'
233 when 'reassign'
232 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
234 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
233 if reassign_to.nil?
235 if reassign_to.nil?
234 flash.now[:error] = l(:error_issue_not_found_in_project)
236 flash.now[:error] = l(:error_issue_not_found_in_project)
235 return
237 return
236 else
238 else
237 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
239 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
238 end
240 end
239 else
241 else
240 # display the destroy form if it's a user request
242 # display the destroy form if it's a user request
241 return unless api_request?
243 return unless api_request?
242 end
244 end
243 end
245 end
244 @issues.each do |issue|
246 @issues.each do |issue|
245 begin
247 begin
246 issue.reload.destroy
248 issue.reload.destroy
247 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
249 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
248 # nothing to do, issue was already deleted (eg. by a parent)
250 # nothing to do, issue was already deleted (eg. by a parent)
249 end
251 end
250 end
252 end
251 respond_to do |format|
253 respond_to do |format|
252 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
254 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
253 format.api { head :ok }
255 format.api { head :ok }
254 end
256 end
255 end
257 end
256
258
257 private
259 private
258 def find_issue
260 def find_issue
259 # Issue.visible.find(...) can not be used to redirect user to the login form
261 # Issue.visible.find(...) can not be used to redirect user to the login form
260 # if the issue actually exists but requires authentication
262 # if the issue actually exists but requires authentication
261 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
263 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
262 unless @issue.visible?
264 unless @issue.visible?
263 deny_access
265 deny_access
264 return
266 return
265 end
267 end
266 @project = @issue.project
268 @project = @issue.project
267 rescue ActiveRecord::RecordNotFound
269 rescue ActiveRecord::RecordNotFound
268 render_404
270 render_404
269 end
271 end
270
272
271 def find_project
273 def find_project
272 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
274 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
273 @project = Project.find(project_id)
275 @project = Project.find(project_id)
274 rescue ActiveRecord::RecordNotFound
276 rescue ActiveRecord::RecordNotFound
275 render_404
277 render_404
276 end
278 end
277
279
278 # Used by #edit and #update to set some common instance variables
280 # Used by #edit and #update to set some common instance variables
279 # from the params
281 # from the params
280 # TODO: Refactor, not everything in here is needed by #edit
282 # TODO: Refactor, not everything in here is needed by #edit
281 def update_issue_from_params
283 def update_issue_from_params
282 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
284 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
283 @priorities = IssuePriority.active
285 @priorities = IssuePriority.active
284 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
286 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
285 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
287 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
286 @time_entry.attributes = params[:time_entry]
288 @time_entry.attributes = params[:time_entry]
287
289
288 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
290 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
289 @issue.init_journal(User.current, @notes)
291 @issue.init_journal(User.current, @notes)
290 @issue.safe_attributes = params[:issue]
292 @issue.safe_attributes = params[:issue]
291 end
293 end
292
294
293 # TODO: Refactor, lots of extra code in here
295 # TODO: Refactor, lots of extra code in here
294 # TODO: Changing tracker on an existing issue should not trigger this
296 # TODO: Changing tracker on an existing issue should not trigger this
295 def build_new_issue_from_params
297 def build_new_issue_from_params
296 if params[:id].blank?
298 if params[:id].blank?
297 @issue = Issue.new
299 @issue = Issue.new
298 @issue.copy_from(params[:copy_from]) if params[:copy_from]
300 @issue.copy_from(params[:copy_from]) if params[:copy_from]
299 @issue.project = @project
301 @issue.project = @project
300 else
302 else
301 @issue = @project.issues.visible.find(params[:id])
303 @issue = @project.issues.visible.find(params[:id])
302 end
304 end
303
305
304 @issue.project = @project
306 @issue.project = @project
305 @issue.author = User.current
307 @issue.author = User.current
306 # Tracker must be set before custom field values
308 # Tracker must be set before custom field values
307 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
309 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
308 if @issue.tracker.nil?
310 if @issue.tracker.nil?
309 render_error l(:error_no_tracker_in_project)
311 render_error l(:error_no_tracker_in_project)
310 return false
312 return false
311 end
313 end
312 @issue.start_date ||= Date.today
314 @issue.start_date ||= Date.today
313 if params[:issue].is_a?(Hash)
315 if params[:issue].is_a?(Hash)
314 @issue.safe_attributes = params[:issue]
316 @issue.safe_attributes = params[:issue]
315 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
317 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
316 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
318 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
317 end
319 end
318 end
320 end
319 @priorities = IssuePriority.active
321 @priorities = IssuePriority.active
320 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
322 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
321 end
323 end
322
324
323 def check_for_default_issue_status
325 def check_for_default_issue_status
324 if IssueStatus.default.nil?
326 if IssueStatus.default.nil?
325 render_error l(:error_no_default_issue_status)
327 render_error l(:error_no_default_issue_status)
326 return false
328 return false
327 end
329 end
328 end
330 end
329
331
330 def parse_params_for_bulk_issue_attributes(params)
332 def parse_params_for_bulk_issue_attributes(params)
331 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
333 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
332 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
334 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
333 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
335 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
334 attributes
336 attributes
335 end
337 end
336 end
338 end
@@ -1,523 +1,533
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :versions,
28 :versions,
29 :trackers,
29 :trackers,
30 :projects_trackers,
30 :projects_trackers,
31 :issue_categories,
31 :issue_categories,
32 :enabled_modules,
32 :enabled_modules,
33 :enumerations,
33 :enumerations,
34 :attachments,
34 :attachments,
35 :workflows,
35 :workflows,
36 :custom_fields,
36 :custom_fields,
37 :custom_values,
37 :custom_values,
38 :custom_fields_projects,
38 :custom_fields_projects,
39 :custom_fields_trackers,
39 :custom_fields_trackers,
40 :time_entries,
40 :time_entries,
41 :journals,
41 :journals,
42 :journal_details,
42 :journal_details,
43 :queries,
43 :queries,
44 :attachments
44 :attachments
45
45
46 def setup
46 def setup
47 Setting.rest_api_enabled = '1'
47 Setting.rest_api_enabled = '1'
48 end
48 end
49
49
50 context "/index.xml" do
50 context "/index.xml" do
51 # Use a private project to make sure auth is really working and not just
51 # Use a private project to make sure auth is really working and not just
52 # only showing public issues.
52 # only showing public issues.
53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
54
54
55 should "contain metadata" do
55 should "contain metadata" do
56 get '/issues.xml'
56 get '/issues.xml'
57
57
58 assert_tag :tag => 'issues',
58 assert_tag :tag => 'issues',
59 :attributes => {
59 :attributes => {
60 :type => 'array',
60 :type => 'array',
61 :total_count => assigns(:issue_count),
61 :total_count => assigns(:issue_count),
62 :limit => 25,
62 :limit => 25,
63 :offset => 0
63 :offset => 0
64 }
64 }
65 end
65 end
66
66
67 context "with offset and limit" do
67 context "with offset and limit" do
68 should "use the params" do
68 should "use the params" do
69 get '/issues.xml?offset=2&limit=3'
69 get '/issues.xml?offset=2&limit=3'
70
70
71 assert_equal 3, assigns(:limit)
71 assert_equal 3, assigns(:limit)
72 assert_equal 2, assigns(:offset)
72 assert_equal 2, assigns(:offset)
73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
74 end
74 end
75 end
75 end
76
76
77 context "with nometa param" do
77 context "with nometa param" do
78 should "not contain metadata" do
78 should "not contain metadata" do
79 get '/issues.xml?nometa=1'
79 get '/issues.xml?nometa=1'
80
80
81 assert_tag :tag => 'issues',
81 assert_tag :tag => 'issues',
82 :attributes => {
82 :attributes => {
83 :type => 'array',
83 :type => 'array',
84 :total_count => nil,
84 :total_count => nil,
85 :limit => nil,
85 :limit => nil,
86 :offset => nil
86 :offset => nil
87 }
87 }
88 end
88 end
89 end
89 end
90
90
91 context "with nometa header" do
91 context "with nometa header" do
92 should "not contain metadata" do
92 should "not contain metadata" do
93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
94
94
95 assert_tag :tag => 'issues',
95 assert_tag :tag => 'issues',
96 :attributes => {
96 :attributes => {
97 :type => 'array',
97 :type => 'array',
98 :total_count => nil,
98 :total_count => nil,
99 :limit => nil,
99 :limit => nil,
100 :offset => nil
100 :offset => nil
101 }
101 }
102 end
102 end
103 end
103 end
104
105 context "with invalid query params" do
106 should "return errors" do
107 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
108
109 assert_response :unprocessable_entity
110 assert_equal 'application/xml', @response.content_type
111 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
112 end
113 end
104 end
114 end
105
115
106 context "/index.json" do
116 context "/index.json" do
107 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
117 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
108 end
118 end
109
119
110 context "/index.xml with filter" do
120 context "/index.xml with filter" do
111 should "show only issues with the status_id" do
121 should "show only issues with the status_id" do
112 get '/issues.xml?status_id=5'
122 get '/issues.xml?status_id=5'
113 assert_tag :tag => 'issues',
123 assert_tag :tag => 'issues',
114 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
124 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
115 :only => { :tag => 'issue' } }
125 :only => { :tag => 'issue' } }
116 end
126 end
117 end
127 end
118
128
119 context "/index.json with filter" do
129 context "/index.json with filter" do
120 should "show only issues with the status_id" do
130 should "show only issues with the status_id" do
121 get '/issues.json?status_id=5'
131 get '/issues.json?status_id=5'
122
132
123 json = ActiveSupport::JSON.decode(response.body)
133 json = ActiveSupport::JSON.decode(response.body)
124 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
134 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
125 assert_equal 3, status_ids_used.length
135 assert_equal 3, status_ids_used.length
126 assert status_ids_used.all? {|id| id == 5 }
136 assert status_ids_used.all? {|id| id == 5 }
127 end
137 end
128
138
129 end
139 end
130
140
131 # Issue 6 is on a private project
141 # Issue 6 is on a private project
132 context "/issues/6.xml" do
142 context "/issues/6.xml" do
133 should_allow_api_authentication(:get, "/issues/6.xml")
143 should_allow_api_authentication(:get, "/issues/6.xml")
134 end
144 end
135
145
136 context "/issues/6.json" do
146 context "/issues/6.json" do
137 should_allow_api_authentication(:get, "/issues/6.json")
147 should_allow_api_authentication(:get, "/issues/6.json")
138 end
148 end
139
149
140 context "GET /issues/:id" do
150 context "GET /issues/:id" do
141 context "with journals" do
151 context "with journals" do
142 context ".xml" do
152 context ".xml" do
143 should "display journals" do
153 should "display journals" do
144 get '/issues/1.xml?include=journals'
154 get '/issues/1.xml?include=journals'
145
155
146 assert_tag :tag => 'issue',
156 assert_tag :tag => 'issue',
147 :child => {
157 :child => {
148 :tag => 'journals',
158 :tag => 'journals',
149 :attributes => { :type => 'array' },
159 :attributes => { :type => 'array' },
150 :child => {
160 :child => {
151 :tag => 'journal',
161 :tag => 'journal',
152 :attributes => { :id => '1'},
162 :attributes => { :id => '1'},
153 :child => {
163 :child => {
154 :tag => 'details',
164 :tag => 'details',
155 :attributes => { :type => 'array' },
165 :attributes => { :type => 'array' },
156 :child => {
166 :child => {
157 :tag => 'detail',
167 :tag => 'detail',
158 :attributes => { :name => 'status_id' },
168 :attributes => { :name => 'status_id' },
159 :child => {
169 :child => {
160 :tag => 'old_value',
170 :tag => 'old_value',
161 :content => '1',
171 :content => '1',
162 :sibling => {
172 :sibling => {
163 :tag => 'new_value',
173 :tag => 'new_value',
164 :content => '2'
174 :content => '2'
165 }
175 }
166 }
176 }
167 }
177 }
168 }
178 }
169 }
179 }
170 }
180 }
171 end
181 end
172 end
182 end
173 end
183 end
174
184
175 context "with custom fields" do
185 context "with custom fields" do
176 context ".xml" do
186 context ".xml" do
177 should "display custom fields" do
187 should "display custom fields" do
178 get '/issues/3.xml'
188 get '/issues/3.xml'
179
189
180 assert_tag :tag => 'issue',
190 assert_tag :tag => 'issue',
181 :child => {
191 :child => {
182 :tag => 'custom_fields',
192 :tag => 'custom_fields',
183 :attributes => { :type => 'array' },
193 :attributes => { :type => 'array' },
184 :child => {
194 :child => {
185 :tag => 'custom_field',
195 :tag => 'custom_field',
186 :attributes => { :id => '1'},
196 :attributes => { :id => '1'},
187 :child => {
197 :child => {
188 :tag => 'value',
198 :tag => 'value',
189 :content => 'MySQL'
199 :content => 'MySQL'
190 }
200 }
191 }
201 }
192 }
202 }
193
203
194 assert_nothing_raised do
204 assert_nothing_raised do
195 Hash.from_xml(response.body).to_xml
205 Hash.from_xml(response.body).to_xml
196 end
206 end
197 end
207 end
198 end
208 end
199 end
209 end
200
210
201 context "with attachments" do
211 context "with attachments" do
202 context ".xml" do
212 context ".xml" do
203 should "display attachments" do
213 should "display attachments" do
204 get '/issues/3.xml?include=attachments'
214 get '/issues/3.xml?include=attachments'
205
215
206 assert_tag :tag => 'issue',
216 assert_tag :tag => 'issue',
207 :child => {
217 :child => {
208 :tag => 'attachments',
218 :tag => 'attachments',
209 :children => {:count => 5},
219 :children => {:count => 5},
210 :child => {
220 :child => {
211 :tag => 'attachment',
221 :tag => 'attachment',
212 :child => {
222 :child => {
213 :tag => 'filename',
223 :tag => 'filename',
214 :content => 'source.rb',
224 :content => 'source.rb',
215 :sibling => {
225 :sibling => {
216 :tag => 'content_url',
226 :tag => 'content_url',
217 :content => 'http://www.example.com/attachments/download/4/source.rb'
227 :content => 'http://www.example.com/attachments/download/4/source.rb'
218 }
228 }
219 }
229 }
220 }
230 }
221 }
231 }
222 end
232 end
223 end
233 end
224 end
234 end
225
235
226 context "with subtasks" do
236 context "with subtasks" do
227 setup do
237 setup do
228 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
238 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
229 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
239 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
230 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
240 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
231 end
241 end
232
242
233 context ".xml" do
243 context ".xml" do
234 should "display children" do
244 should "display children" do
235 get '/issues/1.xml?include=children'
245 get '/issues/1.xml?include=children'
236
246
237 assert_tag :tag => 'issue',
247 assert_tag :tag => 'issue',
238 :child => {
248 :child => {
239 :tag => 'children',
249 :tag => 'children',
240 :children => {:count => 2},
250 :children => {:count => 2},
241 :child => {
251 :child => {
242 :tag => 'issue',
252 :tag => 'issue',
243 :attributes => {:id => @c1.id.to_s},
253 :attributes => {:id => @c1.id.to_s},
244 :child => {
254 :child => {
245 :tag => 'subject',
255 :tag => 'subject',
246 :content => 'child c1',
256 :content => 'child c1',
247 :sibling => {
257 :sibling => {
248 :tag => 'children',
258 :tag => 'children',
249 :children => {:count => 1},
259 :children => {:count => 1},
250 :child => {
260 :child => {
251 :tag => 'issue',
261 :tag => 'issue',
252 :attributes => {:id => @c3.id.to_s}
262 :attributes => {:id => @c3.id.to_s}
253 }
263 }
254 }
264 }
255 }
265 }
256 }
266 }
257 }
267 }
258 end
268 end
259
269
260 context ".json" do
270 context ".json" do
261 should "display children" do
271 should "display children" do
262 get '/issues/1.json?include=children'
272 get '/issues/1.json?include=children'
263
273
264 json = ActiveSupport::JSON.decode(response.body)
274 json = ActiveSupport::JSON.decode(response.body)
265 assert_equal([
275 assert_equal([
266 {
276 {
267 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
277 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
268 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
278 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
269 },
279 },
270 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
280 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
271 ],
281 ],
272 json['issue']['children'])
282 json['issue']['children'])
273 end
283 end
274 end
284 end
275 end
285 end
276 end
286 end
277 end
287 end
278
288
279 context "POST /issues.xml" do
289 context "POST /issues.xml" do
280 should_allow_api_authentication(:post,
290 should_allow_api_authentication(:post,
281 '/issues.xml',
291 '/issues.xml',
282 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
292 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
283 {:success_code => :created})
293 {:success_code => :created})
284
294
285 should "create an issue with the attributes" do
295 should "create an issue with the attributes" do
286 assert_difference('Issue.count') do
296 assert_difference('Issue.count') do
287 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
297 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
288 end
298 end
289
299
290 issue = Issue.first(:order => 'id DESC')
300 issue = Issue.first(:order => 'id DESC')
291 assert_equal 1, issue.project_id
301 assert_equal 1, issue.project_id
292 assert_equal 2, issue.tracker_id
302 assert_equal 2, issue.tracker_id
293 assert_equal 3, issue.status_id
303 assert_equal 3, issue.status_id
294 assert_equal 'API test', issue.subject
304 assert_equal 'API test', issue.subject
295
305
296 assert_response :created
306 assert_response :created
297 assert_equal 'application/xml', @response.content_type
307 assert_equal 'application/xml', @response.content_type
298 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
308 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
299 end
309 end
300 end
310 end
301
311
302 context "POST /issues.xml with failure" do
312 context "POST /issues.xml with failure" do
303 should "have an errors tag" do
313 should "have an errors tag" do
304 assert_no_difference('Issue.count') do
314 assert_no_difference('Issue.count') do
305 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
315 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
306 end
316 end
307
317
308 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
318 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
309 end
319 end
310 end
320 end
311
321
312 context "POST /issues.json" do
322 context "POST /issues.json" do
313 should_allow_api_authentication(:post,
323 should_allow_api_authentication(:post,
314 '/issues.json',
324 '/issues.json',
315 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
325 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
316 {:success_code => :created})
326 {:success_code => :created})
317
327
318 should "create an issue with the attributes" do
328 should "create an issue with the attributes" do
319 assert_difference('Issue.count') do
329 assert_difference('Issue.count') do
320 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
330 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
321 end
331 end
322
332
323 issue = Issue.first(:order => 'id DESC')
333 issue = Issue.first(:order => 'id DESC')
324 assert_equal 1, issue.project_id
334 assert_equal 1, issue.project_id
325 assert_equal 2, issue.tracker_id
335 assert_equal 2, issue.tracker_id
326 assert_equal 3, issue.status_id
336 assert_equal 3, issue.status_id
327 assert_equal 'API test', issue.subject
337 assert_equal 'API test', issue.subject
328 end
338 end
329
339
330 end
340 end
331
341
332 context "POST /issues.json with failure" do
342 context "POST /issues.json with failure" do
333 should "have an errors element" do
343 should "have an errors element" do
334 assert_no_difference('Issue.count') do
344 assert_no_difference('Issue.count') do
335 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
345 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
336 end
346 end
337
347
338 json = ActiveSupport::JSON.decode(response.body)
348 json = ActiveSupport::JSON.decode(response.body)
339 assert json['errors'].include?(['subject', "can't be blank"])
349 assert json['errors'].include?(['subject', "can't be blank"])
340 end
350 end
341 end
351 end
342
352
343 # Issue 6 is on a private project
353 # Issue 6 is on a private project
344 context "PUT /issues/6.xml" do
354 context "PUT /issues/6.xml" do
345 setup do
355 setup do
346 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
356 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
347 @headers = { :authorization => credentials('jsmith') }
357 @headers = { :authorization => credentials('jsmith') }
348 end
358 end
349
359
350 should_allow_api_authentication(:put,
360 should_allow_api_authentication(:put,
351 '/issues/6.xml',
361 '/issues/6.xml',
352 {:issue => {:subject => 'API update', :notes => 'A new note'}},
362 {:issue => {:subject => 'API update', :notes => 'A new note'}},
353 {:success_code => :ok})
363 {:success_code => :ok})
354
364
355 should "not create a new issue" do
365 should "not create a new issue" do
356 assert_no_difference('Issue.count') do
366 assert_no_difference('Issue.count') do
357 put '/issues/6.xml', @parameters, @headers
367 put '/issues/6.xml', @parameters, @headers
358 end
368 end
359 end
369 end
360
370
361 should "create a new journal" do
371 should "create a new journal" do
362 assert_difference('Journal.count') do
372 assert_difference('Journal.count') do
363 put '/issues/6.xml', @parameters, @headers
373 put '/issues/6.xml', @parameters, @headers
364 end
374 end
365 end
375 end
366
376
367 should "add the note to the journal" do
377 should "add the note to the journal" do
368 put '/issues/6.xml', @parameters, @headers
378 put '/issues/6.xml', @parameters, @headers
369
379
370 journal = Journal.last
380 journal = Journal.last
371 assert_equal "A new note", journal.notes
381 assert_equal "A new note", journal.notes
372 end
382 end
373
383
374 should "update the issue" do
384 should "update the issue" do
375 put '/issues/6.xml', @parameters, @headers
385 put '/issues/6.xml', @parameters, @headers
376
386
377 issue = Issue.find(6)
387 issue = Issue.find(6)
378 assert_equal "API update", issue.subject
388 assert_equal "API update", issue.subject
379 end
389 end
380
390
381 end
391 end
382
392
383 context "PUT /issues/3.xml with custom fields" do
393 context "PUT /issues/3.xml with custom fields" do
384 setup do
394 setup do
385 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
395 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
386 @headers = { :authorization => credentials('jsmith') }
396 @headers = { :authorization => credentials('jsmith') }
387 end
397 end
388
398
389 should "update custom fields" do
399 should "update custom fields" do
390 assert_no_difference('Issue.count') do
400 assert_no_difference('Issue.count') do
391 put '/issues/3.xml', @parameters, @headers
401 put '/issues/3.xml', @parameters, @headers
392 end
402 end
393
403
394 issue = Issue.find(3)
404 issue = Issue.find(3)
395 assert_equal '150', issue.custom_value_for(2).value
405 assert_equal '150', issue.custom_value_for(2).value
396 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
406 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
397 end
407 end
398 end
408 end
399
409
400 context "PUT /issues/6.xml with failed update" do
410 context "PUT /issues/6.xml with failed update" do
401 setup do
411 setup do
402 @parameters = {:issue => {:subject => ''}}
412 @parameters = {:issue => {:subject => ''}}
403 @headers = { :authorization => credentials('jsmith') }
413 @headers = { :authorization => credentials('jsmith') }
404 end
414 end
405
415
406 should "not create a new issue" do
416 should "not create a new issue" do
407 assert_no_difference('Issue.count') do
417 assert_no_difference('Issue.count') do
408 put '/issues/6.xml', @parameters, @headers
418 put '/issues/6.xml', @parameters, @headers
409 end
419 end
410 end
420 end
411
421
412 should "not create a new journal" do
422 should "not create a new journal" do
413 assert_no_difference('Journal.count') do
423 assert_no_difference('Journal.count') do
414 put '/issues/6.xml', @parameters, @headers
424 put '/issues/6.xml', @parameters, @headers
415 end
425 end
416 end
426 end
417
427
418 should "have an errors tag" do
428 should "have an errors tag" do
419 put '/issues/6.xml', @parameters, @headers
429 put '/issues/6.xml', @parameters, @headers
420
430
421 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
431 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
422 end
432 end
423 end
433 end
424
434
425 context "PUT /issues/6.json" do
435 context "PUT /issues/6.json" do
426 setup do
436 setup do
427 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
437 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
428 @headers = { :authorization => credentials('jsmith') }
438 @headers = { :authorization => credentials('jsmith') }
429 end
439 end
430
440
431 should_allow_api_authentication(:put,
441 should_allow_api_authentication(:put,
432 '/issues/6.json',
442 '/issues/6.json',
433 {:issue => {:subject => 'API update', :notes => 'A new note'}},
443 {:issue => {:subject => 'API update', :notes => 'A new note'}},
434 {:success_code => :ok})
444 {:success_code => :ok})
435
445
436 should "not create a new issue" do
446 should "not create a new issue" do
437 assert_no_difference('Issue.count') do
447 assert_no_difference('Issue.count') do
438 put '/issues/6.json', @parameters, @headers
448 put '/issues/6.json', @parameters, @headers
439 end
449 end
440 end
450 end
441
451
442 should "create a new journal" do
452 should "create a new journal" do
443 assert_difference('Journal.count') do
453 assert_difference('Journal.count') do
444 put '/issues/6.json', @parameters, @headers
454 put '/issues/6.json', @parameters, @headers
445 end
455 end
446 end
456 end
447
457
448 should "add the note to the journal" do
458 should "add the note to the journal" do
449 put '/issues/6.json', @parameters, @headers
459 put '/issues/6.json', @parameters, @headers
450
460
451 journal = Journal.last
461 journal = Journal.last
452 assert_equal "A new note", journal.notes
462 assert_equal "A new note", journal.notes
453 end
463 end
454
464
455 should "update the issue" do
465 should "update the issue" do
456 put '/issues/6.json', @parameters, @headers
466 put '/issues/6.json', @parameters, @headers
457
467
458 issue = Issue.find(6)
468 issue = Issue.find(6)
459 assert_equal "API update", issue.subject
469 assert_equal "API update", issue.subject
460 end
470 end
461
471
462 end
472 end
463
473
464 context "PUT /issues/6.json with failed update" do
474 context "PUT /issues/6.json with failed update" do
465 setup do
475 setup do
466 @parameters = {:issue => {:subject => ''}}
476 @parameters = {:issue => {:subject => ''}}
467 @headers = { :authorization => credentials('jsmith') }
477 @headers = { :authorization => credentials('jsmith') }
468 end
478 end
469
479
470 should "not create a new issue" do
480 should "not create a new issue" do
471 assert_no_difference('Issue.count') do
481 assert_no_difference('Issue.count') do
472 put '/issues/6.json', @parameters, @headers
482 put '/issues/6.json', @parameters, @headers
473 end
483 end
474 end
484 end
475
485
476 should "not create a new journal" do
486 should "not create a new journal" do
477 assert_no_difference('Journal.count') do
487 assert_no_difference('Journal.count') do
478 put '/issues/6.json', @parameters, @headers
488 put '/issues/6.json', @parameters, @headers
479 end
489 end
480 end
490 end
481
491
482 should "have an errors attribute" do
492 should "have an errors attribute" do
483 put '/issues/6.json', @parameters, @headers
493 put '/issues/6.json', @parameters, @headers
484
494
485 json = ActiveSupport::JSON.decode(response.body)
495 json = ActiveSupport::JSON.decode(response.body)
486 assert json['errors'].include?(['subject', "can't be blank"])
496 assert json['errors'].include?(['subject', "can't be blank"])
487 end
497 end
488 end
498 end
489
499
490 context "DELETE /issues/1.xml" do
500 context "DELETE /issues/1.xml" do
491 should_allow_api_authentication(:delete,
501 should_allow_api_authentication(:delete,
492 '/issues/6.xml',
502 '/issues/6.xml',
493 {},
503 {},
494 {:success_code => :ok})
504 {:success_code => :ok})
495
505
496 should "delete the issue" do
506 should "delete the issue" do
497 assert_difference('Issue.count',-1) do
507 assert_difference('Issue.count',-1) do
498 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
508 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
499 end
509 end
500
510
501 assert_nil Issue.find_by_id(6)
511 assert_nil Issue.find_by_id(6)
502 end
512 end
503 end
513 end
504
514
505 context "DELETE /issues/1.json" do
515 context "DELETE /issues/1.json" do
506 should_allow_api_authentication(:delete,
516 should_allow_api_authentication(:delete,
507 '/issues/6.json',
517 '/issues/6.json',
508 {},
518 {},
509 {:success_code => :ok})
519 {:success_code => :ok})
510
520
511 should "delete the issue" do
521 should "delete the issue" do
512 assert_difference('Issue.count',-1) do
522 assert_difference('Issue.count',-1) do
513 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
523 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
514 end
524 end
515
525
516 assert_nil Issue.find_by_id(6)
526 assert_nil Issue.find_by_id(6)
517 end
527 end
518 end
528 end
519
529
520 def credentials(user, password=nil)
530 def credentials(user, password=nil)
521 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
531 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
522 end
532 end
523 end
533 end
General Comments 0
You need to be logged in to leave comments. Login now