##// END OF EJS Templates
Allow key authentication when deleting issues (with tests) #6447...
Eric Davis -
r4253:c55e060bab62
parent child
Show More
@@ -1,332 +1,332
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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_key_auth :index, :show, :create, :update
30 accept_key_auth :index, :show, :create, :update, :destroy
31
31
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33
33
34 helper :journals
34 helper :journals
35 helper :projects
35 helper :projects
36 include ProjectsHelper
36 include ProjectsHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :issue_relations
39 helper :issue_relations
40 include IssueRelationsHelper
40 include IssueRelationsHelper
41 helper :watchers
41 helper :watchers
42 include WatchersHelper
42 include WatchersHelper
43 helper :attachments
43 helper :attachments
44 include AttachmentsHelper
44 include AttachmentsHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :sort
47 helper :sort
48 include SortHelper
48 include SortHelper
49 include IssuesHelper
49 include IssuesHelper
50 helper :timelog
50 helper :timelog
51 helper :gantt
51 helper :gantt
52 include Redmine::Export::PDF
52 include Redmine::Export::PDF
53
53
54 verify :method => [:post, :delete],
54 verify :method => [:post, :delete],
55 :only => :destroy,
55 :only => :destroy,
56 :render => { :nothing => true, :status => :method_not_allowed }
56 :render => { :nothing => true, :status => :method_not_allowed }
57
57
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
61
61
62 def index
62 def index
63 retrieve_query
63 retrieve_query
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
65 sort_update(@query.sortable_columns)
65 sort_update(@query.sortable_columns)
66
66
67 if @query.valid?
67 if @query.valid?
68 limit = case params[:format]
68 limit = case params[:format]
69 when 'csv', 'pdf'
69 when 'csv', 'pdf'
70 Setting.issues_export_limit.to_i
70 Setting.issues_export_limit.to_i
71 when 'atom'
71 when 'atom'
72 Setting.feeds_limit.to_i
72 Setting.feeds_limit.to_i
73 else
73 else
74 per_page_option
74 per_page_option
75 end
75 end
76
76
77 @issue_count = @query.issue_count
77 @issue_count = @query.issue_count
78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 :order => sort_clause,
80 :order => sort_clause,
81 :offset => @issue_pages.current.offset,
81 :offset => @issue_pages.current.offset,
82 :limit => limit)
82 :limit => limit)
83 @issue_count_by_group = @query.issue_count_by_group
83 @issue_count_by_group = @query.issue_count_by_group
84
84
85 respond_to do |format|
85 respond_to do |format|
86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
87 format.xml { render :layout => false }
87 format.xml { render :layout => false }
88 format.json { render :text => @issues.to_json, :layout => false }
88 format.json { render :text => @issues.to_json, :layout => false }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
92 end
92 end
93 else
93 else
94 # Send html if the query is not valid
94 # Send html if the query is not valid
95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
96 end
96 end
97 rescue ActiveRecord::RecordNotFound
97 rescue ActiveRecord::RecordNotFound
98 render_404
98 render_404
99 end
99 end
100
100
101 def show
101 def show
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 @journals.each_with_index {|j,i| j.indice = i+1}
103 @journals.each_with_index {|j,i| j.indice = i+1}
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 @changesets = @issue.changesets.visible.all
105 @changesets = @issue.changesets.visible.all
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
109 @priorities = IssuePriority.all
109 @priorities = IssuePriority.all
110 @time_entry = TimeEntry.new
110 @time_entry = TimeEntry.new
111 respond_to do |format|
111 respond_to do |format|
112 format.html { render :template => 'issues/show.rhtml' }
112 format.html { render :template => 'issues/show.rhtml' }
113 format.xml { render :layout => false }
113 format.xml { render :layout => false }
114 format.json { render :text => @issue.to_json, :layout => false }
114 format.json { render :text => @issue.to_json, :layout => false }
115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 end
117 end
118 end
118 end
119
119
120 # Add a new issue
120 # Add a new issue
121 # The new issue will be created from an existing one if copy_from parameter is given
121 # The new issue will be created from an existing one if copy_from parameter is given
122 def new
122 def new
123 respond_to do |format|
123 respond_to do |format|
124 format.html { render :action => 'new', :layout => !request.xhr? }
124 format.html { render :action => 'new', :layout => !request.xhr? }
125 format.js { render :partial => 'attributes' }
125 format.js { render :partial => 'attributes' }
126 end
126 end
127 end
127 end
128
128
129 def create
129 def create
130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
131 if @issue.save
131 if @issue.save
132 attachments = Attachment.attach_files(@issue, params[:attachments])
132 attachments = Attachment.attach_files(@issue, params[:attachments])
133 render_attachment_warning_if_needed(@issue)
133 render_attachment_warning_if_needed(@issue)
134 flash[:notice] = l(:notice_successful_create)
134 flash[:notice] = l(:notice_successful_create)
135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
136 respond_to do |format|
136 respond_to do |format|
137 format.html {
137 format.html {
138 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?} } :
138 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?} } :
139 { :action => 'show', :id => @issue })
139 { :action => 'show', :id => @issue })
140 }
140 }
141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
143 end
143 end
144 return
144 return
145 else
145 else
146 respond_to do |format|
146 respond_to do |format|
147 format.html { render :action => 'new' }
147 format.html { render :action => 'new' }
148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
150 end
150 end
151 end
151 end
152 end
152 end
153
153
154 # Attributes that can be updated on workflow transition (without :edit permission)
154 # Attributes that can be updated on workflow transition (without :edit permission)
155 # TODO: make it configurable (at least per role)
155 # TODO: make it configurable (at least per role)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157
157
158 def edit
158 def edit
159 update_issue_from_params
159 update_issue_from_params
160
160
161 @journal = @issue.current_journal
161 @journal = @issue.current_journal
162
162
163 respond_to do |format|
163 respond_to do |format|
164 format.html { }
164 format.html { }
165 format.xml { }
165 format.xml { }
166 end
166 end
167 end
167 end
168
168
169 def update
169 def update
170 update_issue_from_params
170 update_issue_from_params
171
171
172 if @issue.save_issue_with_child_records(params, @time_entry)
172 if @issue.save_issue_with_child_records(params, @time_entry)
173 render_attachment_warning_if_needed(@issue)
173 render_attachment_warning_if_needed(@issue)
174 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
174 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
175
175
176 respond_to do |format|
176 respond_to do |format|
177 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
177 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
178 format.xml { head :ok }
178 format.xml { head :ok }
179 format.json { head :ok }
179 format.json { head :ok }
180 end
180 end
181 else
181 else
182 render_attachment_warning_if_needed(@issue)
182 render_attachment_warning_if_needed(@issue)
183 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
183 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
184 @journal = @issue.current_journal
184 @journal = @issue.current_journal
185
185
186 respond_to do |format|
186 respond_to do |format|
187 format.html { render :action => 'edit' }
187 format.html { render :action => 'edit' }
188 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
188 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
189 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
189 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
190 end
190 end
191 end
191 end
192 end
192 end
193
193
194 # Bulk edit a set of issues
194 # Bulk edit a set of issues
195 def bulk_edit
195 def bulk_edit
196 @issues.sort!
196 @issues.sort!
197 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
197 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
198 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
198 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
199 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
199 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
200 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
200 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
201 end
201 end
202
202
203 def bulk_update
203 def bulk_update
204 @issues.sort!
204 @issues.sort!
205 attributes = parse_params_for_bulk_issue_attributes(params)
205 attributes = parse_params_for_bulk_issue_attributes(params)
206
206
207 unsaved_issue_ids = []
207 unsaved_issue_ids = []
208 @issues.each do |issue|
208 @issues.each do |issue|
209 issue.reload
209 issue.reload
210 journal = issue.init_journal(User.current, params[:notes])
210 journal = issue.init_journal(User.current, params[:notes])
211 issue.safe_attributes = attributes
211 issue.safe_attributes = attributes
212 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
212 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
213 unless issue.save
213 unless issue.save
214 # Keep unsaved issue ids to display them in flash error
214 # Keep unsaved issue ids to display them in flash error
215 unsaved_issue_ids << issue.id
215 unsaved_issue_ids << issue.id
216 end
216 end
217 end
217 end
218 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
218 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
219 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
219 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
220 end
220 end
221
221
222 def destroy
222 def destroy
223 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
223 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
224 if @hours > 0
224 if @hours > 0
225 case params[:todo]
225 case params[:todo]
226 when 'destroy'
226 when 'destroy'
227 # nothing to do
227 # nothing to do
228 when 'nullify'
228 when 'nullify'
229 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
229 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
230 when 'reassign'
230 when 'reassign'
231 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
231 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
232 if reassign_to.nil?
232 if reassign_to.nil?
233 flash.now[:error] = l(:error_issue_not_found_in_project)
233 flash.now[:error] = l(:error_issue_not_found_in_project)
234 return
234 return
235 else
235 else
236 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
236 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
237 end
237 end
238 else
238 else
239 unless params[:format] == 'xml' || params[:format] == 'json'
239 unless params[:format] == 'xml' || params[:format] == 'json'
240 # display the destroy form if it's a user request
240 # display the destroy form if it's a user request
241 return
241 return
242 end
242 end
243 end
243 end
244 end
244 end
245 @issues.each(&:destroy)
245 @issues.each(&:destroy)
246 respond_to do |format|
246 respond_to do |format|
247 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
247 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
248 format.xml { head :ok }
248 format.xml { head :ok }
249 format.json { head :ok }
249 format.json { head :ok }
250 end
250 end
251 end
251 end
252
252
253 private
253 private
254 def find_issue
254 def find_issue
255 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
255 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
256 @project = @issue.project
256 @project = @issue.project
257 rescue ActiveRecord::RecordNotFound
257 rescue ActiveRecord::RecordNotFound
258 render_404
258 render_404
259 end
259 end
260
260
261 def find_project
261 def find_project
262 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
262 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
263 @project = Project.find(project_id)
263 @project = Project.find(project_id)
264 rescue ActiveRecord::RecordNotFound
264 rescue ActiveRecord::RecordNotFound
265 render_404
265 render_404
266 end
266 end
267
267
268 # Used by #edit and #update to set some common instance variables
268 # Used by #edit and #update to set some common instance variables
269 # from the params
269 # from the params
270 # TODO: Refactor, not everything in here is needed by #edit
270 # TODO: Refactor, not everything in here is needed by #edit
271 def update_issue_from_params
271 def update_issue_from_params
272 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
272 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
273 @priorities = IssuePriority.all
273 @priorities = IssuePriority.all
274 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
274 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
275 @time_entry = TimeEntry.new
275 @time_entry = TimeEntry.new
276
276
277 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
277 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
278 @issue.init_journal(User.current, @notes)
278 @issue.init_journal(User.current, @notes)
279 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
279 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
280 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
280 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
281 attrs = params[:issue].dup
281 attrs = params[:issue].dup
282 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
282 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
283 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
283 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
284 @issue.safe_attributes = attrs
284 @issue.safe_attributes = attrs
285 end
285 end
286
286
287 end
287 end
288
288
289 # TODO: Refactor, lots of extra code in here
289 # TODO: Refactor, lots of extra code in here
290 # TODO: Changing tracker on an existing issue should not trigger this
290 # TODO: Changing tracker on an existing issue should not trigger this
291 def build_new_issue_from_params
291 def build_new_issue_from_params
292 if params[:id].blank?
292 if params[:id].blank?
293 @issue = Issue.new
293 @issue = Issue.new
294 @issue.copy_from(params[:copy_from]) if params[:copy_from]
294 @issue.copy_from(params[:copy_from]) if params[:copy_from]
295 @issue.project = @project
295 @issue.project = @project
296 else
296 else
297 @issue = @project.issues.visible.find(params[:id])
297 @issue = @project.issues.visible.find(params[:id])
298 end
298 end
299
299
300 @issue.project = @project
300 @issue.project = @project
301 # Tracker must be set before custom field values
301 # Tracker must be set before custom field values
302 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
302 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
303 if @issue.tracker.nil?
303 if @issue.tracker.nil?
304 render_error l(:error_no_tracker_in_project)
304 render_error l(:error_no_tracker_in_project)
305 return false
305 return false
306 end
306 end
307 if params[:issue].is_a?(Hash)
307 if params[:issue].is_a?(Hash)
308 @issue.safe_attributes = params[:issue]
308 @issue.safe_attributes = params[:issue]
309 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
309 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
310 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
310 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
311 end
311 end
312 end
312 end
313 @issue.author = User.current
313 @issue.author = User.current
314 @issue.start_date ||= Date.today
314 @issue.start_date ||= Date.today
315 @priorities = IssuePriority.all
315 @priorities = IssuePriority.all
316 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
316 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
317 end
317 end
318
318
319 def check_for_default_issue_status
319 def check_for_default_issue_status
320 if IssueStatus.default.nil?
320 if IssueStatus.default.nil?
321 render_error l(:error_no_default_issue_status)
321 render_error l(:error_no_default_issue_status)
322 return false
322 return false
323 end
323 end
324 end
324 end
325
325
326 def parse_params_for_bulk_issue_attributes(params)
326 def parse_params_for_bulk_issue_attributes(params)
327 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
327 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
328 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
328 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
329 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
329 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
330 attributes
330 attributes
331 end
331 end
332 end
332 end
@@ -1,336 +1,336
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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.dirname(__FILE__)}/../../test_helper"
18 require "#{File.dirname(__FILE__)}/../../test_helper"
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
44
45 def setup
45 def setup
46 Setting.rest_api_enabled = '1'
46 Setting.rest_api_enabled = '1'
47 end
47 end
48
48
49 # Use a private project to make sure auth is really working and not just
49 # Use a private project to make sure auth is really working and not just
50 # only showing public issues.
50 # only showing public issues.
51 context "/index.xml" do
51 context "/index.xml" do
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53 end
53 end
54
54
55 context "/index.json" do
55 context "/index.json" do
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
57 end
57 end
58
58
59 context "/index.xml with filter" do
59 context "/index.xml with filter" do
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
61
61
62 should "show only issues with the status_id" do
62 should "show only issues with the status_id" do
63 get '/issues.xml?status_id=5'
63 get '/issues.xml?status_id=5'
64 assert_tag :tag => 'issues',
64 assert_tag :tag => 'issues',
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
66 :only => { :tag => 'issue' } }
66 :only => { :tag => 'issue' } }
67 end
67 end
68 end
68 end
69
69
70 context "/index.json with filter" do
70 context "/index.json with filter" do
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
72
72
73 should "show only issues with the status_id" do
73 should "show only issues with the status_id" do
74 get '/issues.json?status_id=5'
74 get '/issues.json?status_id=5'
75
75
76 json = ActiveSupport::JSON.decode(response.body)
76 json = ActiveSupport::JSON.decode(response.body)
77 status_ids_used = json.collect {|j| j['status_id'] }
77 status_ids_used = json.collect {|j| j['status_id'] }
78 assert_equal 3, status_ids_used.length
78 assert_equal 3, status_ids_used.length
79 assert status_ids_used.all? {|id| id == 5 }
79 assert status_ids_used.all? {|id| id == 5 }
80 end
80 end
81
81
82 end
82 end
83
83
84 # Issue 6 is on a private project
84 # Issue 6 is on a private project
85 context "/issues/6.xml" do
85 context "/issues/6.xml" do
86 should_allow_api_authentication(:get, "/issues/6.xml")
86 should_allow_api_authentication(:get, "/issues/6.xml")
87 end
87 end
88
88
89 context "/issues/6.json" do
89 context "/issues/6.json" do
90 should_allow_api_authentication(:get, "/issues/6.json")
90 should_allow_api_authentication(:get, "/issues/6.json")
91 end
91 end
92
92
93 context "POST /issues.xml" do
93 context "POST /issues.xml" do
94 should_allow_api_authentication(:post,
94 should_allow_api_authentication(:post,
95 '/issues.xml',
95 '/issues.xml',
96 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
96 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
97 {:success_code => :created})
97 {:success_code => :created})
98
98
99 should "create an issue with the attributes" do
99 should "create an issue with the attributes" do
100 assert_difference('Issue.count') do
100 assert_difference('Issue.count') do
101 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
101 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
102 end
102 end
103
103
104 issue = Issue.first(:order => 'id DESC')
104 issue = Issue.first(:order => 'id DESC')
105 assert_equal 1, issue.project_id
105 assert_equal 1, issue.project_id
106 assert_equal 2, issue.tracker_id
106 assert_equal 2, issue.tracker_id
107 assert_equal 3, issue.status_id
107 assert_equal 3, issue.status_id
108 assert_equal 'API test', issue.subject
108 assert_equal 'API test', issue.subject
109 end
109 end
110 end
110 end
111
111
112 context "POST /issues.xml with failure" do
112 context "POST /issues.xml with failure" do
113 should_allow_api_authentication(:post,
113 should_allow_api_authentication(:post,
114 '/issues.xml',
114 '/issues.xml',
115 {:issue => {:project_id => 1}},
115 {:issue => {:project_id => 1}},
116 {:success_code => :unprocessable_entity})
116 {:success_code => :unprocessable_entity})
117
117
118 should "have an errors tag" do
118 should "have an errors tag" do
119 assert_no_difference('Issue.count') do
119 assert_no_difference('Issue.count') do
120 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
120 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
121 end
121 end
122
122
123 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
123 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
124 end
124 end
125 end
125 end
126
126
127 context "POST /issues.json" do
127 context "POST /issues.json" do
128 should_allow_api_authentication(:post,
128 should_allow_api_authentication(:post,
129 '/issues.json',
129 '/issues.json',
130 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
130 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
131 {:success_code => :created})
131 {:success_code => :created})
132
132
133 should "create an issue with the attributes" do
133 should "create an issue with the attributes" do
134 assert_difference('Issue.count') do
134 assert_difference('Issue.count') do
135 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
135 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
136 end
136 end
137
137
138 issue = Issue.first(:order => 'id DESC')
138 issue = Issue.first(:order => 'id DESC')
139 assert_equal 1, issue.project_id
139 assert_equal 1, issue.project_id
140 assert_equal 2, issue.tracker_id
140 assert_equal 2, issue.tracker_id
141 assert_equal 3, issue.status_id
141 assert_equal 3, issue.status_id
142 assert_equal 'API test', issue.subject
142 assert_equal 'API test', issue.subject
143 end
143 end
144
144
145 end
145 end
146
146
147 context "POST /issues.json with failure" do
147 context "POST /issues.json with failure" do
148 should_allow_api_authentication(:post,
148 should_allow_api_authentication(:post,
149 '/issues.json',
149 '/issues.json',
150 {:issue => {:project_id => 1}},
150 {:issue => {:project_id => 1}},
151 {:success_code => :unprocessable_entity})
151 {:success_code => :unprocessable_entity})
152
152
153 should "have an errors element" do
153 should "have an errors element" do
154 assert_no_difference('Issue.count') do
154 assert_no_difference('Issue.count') do
155 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
155 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
156 end
156 end
157
157
158 json = ActiveSupport::JSON.decode(response.body)
158 json = ActiveSupport::JSON.decode(response.body)
159 assert_equal "can't be blank", json.first['subject']
159 assert_equal "can't be blank", json.first['subject']
160 end
160 end
161 end
161 end
162
162
163 # Issue 6 is on a private project
163 # Issue 6 is on a private project
164 context "PUT /issues/6.xml" do
164 context "PUT /issues/6.xml" do
165 setup do
165 setup do
166 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
166 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
167 @headers = { :authorization => credentials('jsmith') }
167 @headers = { :authorization => credentials('jsmith') }
168 end
168 end
169
169
170 should_allow_api_authentication(:put,
170 should_allow_api_authentication(:put,
171 '/issues/6.xml',
171 '/issues/6.xml',
172 {:issue => {:subject => 'API update', :notes => 'A new note'}},
172 {:issue => {:subject => 'API update', :notes => 'A new note'}},
173 {:success_code => :ok})
173 {:success_code => :ok})
174
174
175 should "not create a new issue" do
175 should "not create a new issue" do
176 assert_no_difference('Issue.count') do
176 assert_no_difference('Issue.count') do
177 put '/issues/6.xml', @parameters, @headers
177 put '/issues/6.xml', @parameters, @headers
178 end
178 end
179 end
179 end
180
180
181 should "create a new journal" do
181 should "create a new journal" do
182 assert_difference('Journal.count') do
182 assert_difference('Journal.count') do
183 put '/issues/6.xml', @parameters, @headers
183 put '/issues/6.xml', @parameters, @headers
184 end
184 end
185 end
185 end
186
186
187 should "add the note to the journal" do
187 should "add the note to the journal" do
188 put '/issues/6.xml', @parameters, @headers
188 put '/issues/6.xml', @parameters, @headers
189
189
190 journal = Journal.last
190 journal = Journal.last
191 assert_equal "A new note", journal.notes
191 assert_equal "A new note", journal.notes
192 end
192 end
193
193
194 should "update the issue" do
194 should "update the issue" do
195 put '/issues/6.xml', @parameters, @headers
195 put '/issues/6.xml', @parameters, @headers
196
196
197 issue = Issue.find(6)
197 issue = Issue.find(6)
198 assert_equal "API update", issue.subject
198 assert_equal "API update", issue.subject
199 end
199 end
200
200
201 end
201 end
202
202
203 context "PUT /issues/6.xml with failed update" do
203 context "PUT /issues/6.xml with failed update" do
204 setup do
204 setup do
205 @parameters = {:issue => {:subject => ''}}
205 @parameters = {:issue => {:subject => ''}}
206 @headers = { :authorization => credentials('jsmith') }
206 @headers = { :authorization => credentials('jsmith') }
207 end
207 end
208
208
209 should_allow_api_authentication(:put,
209 should_allow_api_authentication(:put,
210 '/issues/6.xml',
210 '/issues/6.xml',
211 {:issue => {:subject => ''}}, # Missing subject should fail
211 {:issue => {:subject => ''}}, # Missing subject should fail
212 {:success_code => :unprocessable_entity})
212 {:success_code => :unprocessable_entity})
213
213
214 should "not create a new issue" do
214 should "not create a new issue" do
215 assert_no_difference('Issue.count') do
215 assert_no_difference('Issue.count') do
216 put '/issues/6.xml', @parameters, @headers
216 put '/issues/6.xml', @parameters, @headers
217 end
217 end
218 end
218 end
219
219
220 should "not create a new journal" do
220 should "not create a new journal" do
221 assert_no_difference('Journal.count') do
221 assert_no_difference('Journal.count') do
222 put '/issues/6.xml', @parameters, @headers
222 put '/issues/6.xml', @parameters, @headers
223 end
223 end
224 end
224 end
225
225
226 should "have an errors tag" do
226 should "have an errors tag" do
227 put '/issues/6.xml', @parameters, @headers
227 put '/issues/6.xml', @parameters, @headers
228
228
229 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
229 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
230 end
230 end
231 end
231 end
232
232
233 context "PUT /issues/6.json" do
233 context "PUT /issues/6.json" do
234 setup do
234 setup do
235 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
235 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
236 @headers = { :authorization => credentials('jsmith') }
236 @headers = { :authorization => credentials('jsmith') }
237 end
237 end
238
238
239 should_allow_api_authentication(:put,
239 should_allow_api_authentication(:put,
240 '/issues/6.json',
240 '/issues/6.json',
241 {:issue => {:subject => 'API update', :notes => 'A new note'}},
241 {:issue => {:subject => 'API update', :notes => 'A new note'}},
242 {:success_code => :ok})
242 {:success_code => :ok})
243
243
244 should "not create a new issue" do
244 should "not create a new issue" do
245 assert_no_difference('Issue.count') do
245 assert_no_difference('Issue.count') do
246 put '/issues/6.json', @parameters, @headers
246 put '/issues/6.json', @parameters, @headers
247 end
247 end
248 end
248 end
249
249
250 should "create a new journal" do
250 should "create a new journal" do
251 assert_difference('Journal.count') do
251 assert_difference('Journal.count') do
252 put '/issues/6.json', @parameters, @headers
252 put '/issues/6.json', @parameters, @headers
253 end
253 end
254 end
254 end
255
255
256 should "add the note to the journal" do
256 should "add the note to the journal" do
257 put '/issues/6.json', @parameters, @headers
257 put '/issues/6.json', @parameters, @headers
258
258
259 journal = Journal.last
259 journal = Journal.last
260 assert_equal "A new note", journal.notes
260 assert_equal "A new note", journal.notes
261 end
261 end
262
262
263 should "update the issue" do
263 should "update the issue" do
264 put '/issues/6.json', @parameters, @headers
264 put '/issues/6.json', @parameters, @headers
265
265
266 issue = Issue.find(6)
266 issue = Issue.find(6)
267 assert_equal "API update", issue.subject
267 assert_equal "API update", issue.subject
268 end
268 end
269
269
270 end
270 end
271
271
272 context "PUT /issues/6.json with failed update" do
272 context "PUT /issues/6.json with failed update" do
273 setup do
273 setup do
274 @parameters = {:issue => {:subject => ''}}
274 @parameters = {:issue => {:subject => ''}}
275 @headers = { :authorization => credentials('jsmith') }
275 @headers = { :authorization => credentials('jsmith') }
276 end
276 end
277
277
278 should_allow_api_authentication(:put,
278 should_allow_api_authentication(:put,
279 '/issues/6.json',
279 '/issues/6.json',
280 {:issue => {:subject => ''}}, # Missing subject should fail
280 {:issue => {:subject => ''}}, # Missing subject should fail
281 {:success_code => :unprocessable_entity})
281 {:success_code => :unprocessable_entity})
282
282
283 should "not create a new issue" do
283 should "not create a new issue" do
284 assert_no_difference('Issue.count') do
284 assert_no_difference('Issue.count') do
285 put '/issues/6.json', @parameters, @headers
285 put '/issues/6.json', @parameters, @headers
286 end
286 end
287 end
287 end
288
288
289 should "not create a new journal" do
289 should "not create a new journal" do
290 assert_no_difference('Journal.count') do
290 assert_no_difference('Journal.count') do
291 put '/issues/6.json', @parameters, @headers
291 put '/issues/6.json', @parameters, @headers
292 end
292 end
293 end
293 end
294
294
295 should "have an errors attribute" do
295 should "have an errors attribute" do
296 put '/issues/6.json', @parameters, @headers
296 put '/issues/6.json', @parameters, @headers
297
297
298 json = ActiveSupport::JSON.decode(response.body)
298 json = ActiveSupport::JSON.decode(response.body)
299 assert_equal "can't be blank", json.first['subject']
299 assert_equal "can't be blank", json.first['subject']
300 end
300 end
301 end
301 end
302
302
303 context "DELETE /issues/1.xml" do
303 context "DELETE /issues/1.xml" do
304 setup do
304 should_allow_api_authentication(:delete,
305 @issue_count = Issue.count
305 '/issues/6.xml',
306 delete '/issues/1.xml', {}, :authorization => credentials('jsmith')
306 {},
307 end
307 {:success_code => :ok})
308
309 should_respond_with :ok
310 should_respond_with_content_type 'application/xml'
311
308
312 should "delete the issue" do
309 should "delete the issue" do
313 assert_equal Issue.count, @issue_count -1
310 assert_difference('Issue.count',-1) do
314 assert_nil Issue.find_by_id(1)
311 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
315 end
316 end
312 end
317
313
318 context "DELETE /issues/1.json" do
314 assert_nil Issue.find_by_id(6)
319 setup do
315 end
320 @issue_count = Issue.count
321 delete '/issues/1.json', {}, :authorization => credentials('jsmith')
322 end
316 end
323
317
324 should_respond_with :ok
318 context "DELETE /issues/1.json" do
325 should_respond_with_content_type 'application/json'
319 should_allow_api_authentication(:delete,
320 '/issues/6.json',
321 {},
322 {:success_code => :ok})
326
323
327 should "delete the issue" do
324 should "delete the issue" do
328 assert_equal Issue.count, @issue_count -1
325 assert_difference('Issue.count',-1) do
329 assert_nil Issue.find_by_id(1)
326 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
327 end
328
329 assert_nil Issue.find_by_id(6)
330 end
330 end
331 end
331 end
332
332
333 def credentials(user, password=nil)
333 def credentials(user, password=nil)
334 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
334 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
335 end
335 end
336 end
336 end
General Comments 0
You need to be logged in to leave comments. Login now