##// END OF EJS Templates
Refactor: Move the updating of an Issue to the #update method....
Eric Davis -
r3372:c68d853bf4cf
parent child
Show More
@@ -1,569 +1,587
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => :new
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
23 23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 27 accept_key_auth :index, :show, :changes
28 28
29 29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30 30
31 31 helper :journals
32 32 helper :projects
33 33 include ProjectsHelper
34 34 helper :custom_fields
35 35 include CustomFieldsHelper
36 36 helper :issue_relations
37 37 include IssueRelationsHelper
38 38 helper :watchers
39 39 include WatchersHelper
40 40 helper :attachments
41 41 include AttachmentsHelper
42 42 helper :queries
43 43 include QueriesHelper
44 44 helper :sort
45 45 include SortHelper
46 46 include IssuesHelper
47 47 helper :timelog
48 48 include Redmine::Export::PDF
49 49
50 50 verify :method => [:post, :delete],
51 51 :only => :destroy,
52 52 :render => { :nothing => true, :status => :method_not_allowed }
53 53
54 54 def index
55 55 retrieve_query
56 56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 57 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
58 58
59 59 if @query.valid?
60 60 limit = case params[:format]
61 61 when 'csv', 'pdf'
62 62 Setting.issues_export_limit.to_i
63 63 when 'atom'
64 64 Setting.feeds_limit.to_i
65 65 else
66 66 per_page_option
67 67 end
68 68
69 69 @issue_count = @query.issue_count
70 70 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
71 71 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
72 72 :order => sort_clause,
73 73 :offset => @issue_pages.current.offset,
74 74 :limit => limit)
75 75 @issue_count_by_group = @query.issue_count_by_group
76 76
77 77 respond_to do |format|
78 78 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
79 79 format.xml { render :layout => false }
80 80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
81 81 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
82 82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
83 83 end
84 84 else
85 85 # Send html if the query is not valid
86 86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
87 87 end
88 88 rescue ActiveRecord::RecordNotFound
89 89 render_404
90 90 end
91 91
92 92 def changes
93 93 retrieve_query
94 94 sort_init 'id', 'desc'
95 95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
96 96
97 97 if @query.valid?
98 98 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
99 99 :limit => 25)
100 100 end
101 101 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
102 102 render :layout => false, :content_type => 'application/atom+xml'
103 103 rescue ActiveRecord::RecordNotFound
104 104 render_404
105 105 end
106 106
107 107 def show
108 108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 109 @journals.each_with_index {|j,i| j.indice = i+1}
110 110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 111 @changesets = @issue.changesets.visible.all
112 112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
113 113 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
114 114 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
115 115 @priorities = IssuePriority.all
116 116 @time_entry = TimeEntry.new
117 117 respond_to do |format|
118 118 format.html { render :template => 'issues/show.rhtml' }
119 119 format.xml { render :layout => false }
120 120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
121 121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 122 end
123 123 end
124 124
125 125 # Add a new issue
126 126 # The new issue will be created from an existing one if copy_from parameter is given
127 127 def new
128 128 @issue = Issue.new
129 129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
130 130 @issue.project = @project
131 131 # Tracker must be set before custom field values
132 132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
133 133 if @issue.tracker.nil?
134 134 render_error l(:error_no_tracker_in_project)
135 135 return
136 136 end
137 137 if params[:issue].is_a?(Hash)
138 138 @issue.safe_attributes = params[:issue]
139 139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
140 140 end
141 141 @issue.author = User.current
142 142
143 143 default_status = IssueStatus.default
144 144 unless default_status
145 145 render_error l(:error_no_default_issue_status)
146 146 return
147 147 end
148 148 @issue.status = default_status
149 149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
150 150
151 151 if request.get? || request.xhr?
152 152 @issue.start_date ||= Date.today
153 153 else
154 154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
155 155 # Check that the user is allowed to apply the requested status
156 156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
157 157 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
158 158 if @issue.save
159 159 attach_files(@issue, params[:attachments])
160 160 flash[:notice] = l(:notice_successful_create)
161 161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
162 162 respond_to do |format|
163 163 format.html {
164 164 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
165 165 { :action => 'show', :id => @issue })
166 166 }
167 167 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
168 168 end
169 169 return
170 170 else
171 171 respond_to do |format|
172 172 format.html { }
173 173 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
174 174 end
175 175 end
176 176 end
177 177 @priorities = IssuePriority.all
178 178 render :layout => !request.xhr?
179 179 end
180 180
181 181 # Attributes that can be updated on workflow transition (without :edit permission)
182 182 # TODO: make it configurable (at least per role)
183 183 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
184 184
185 185 def edit
186 186 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
187 187 @priorities = IssuePriority.all
188 188 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
189 189 @time_entry = TimeEntry.new
190 190
191 191 @notes = params[:notes]
192 192 journal = @issue.init_journal(User.current, @notes)
193 193 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
194 194 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
195 195 attrs = params[:issue].dup
196 196 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
197 197 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
198 198 @issue.safe_attributes = attrs
199 199 end
200 200
201 respond_to do |format|
202 format.html { }
203 format.xml { }
204 end
205 end
206
207 #--
208 # Start converting to the Rails REST controllers
209 #++
210 def update
211 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
212 @priorities = IssuePriority.all
213 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
214 @time_entry = TimeEntry.new
215
216 @notes = params[:notes]
217 journal = @issue.init_journal(User.current, @notes)
218 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
219 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
220 attrs = params[:issue].dup
221 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
222 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
223 @issue.safe_attributes = attrs
224 end
225
201 226 if request.get?
202 227 # nop
203 228 else
204 229 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
205 230 @time_entry.attributes = params[:time_entry]
206 231 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
207 232 attachments = attach_files(@issue, params[:attachments])
208 233 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
209 234 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
210 235 if @issue.save
211 236 # Log spend time
212 237 if User.current.allowed_to?(:log_time, @project)
213 238 @time_entry.save
214 239 end
215 240 if !journal.new_record?
216 241 # Only send notification if something was actually changed
217 242 flash[:notice] = l(:notice_successful_update)
218 243 end
219 244 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
220 245 respond_to do |format|
221 246 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
222 247 format.xml { head :ok }
223 248 end
224 249 return
225 250 end
226 251 end
227 252 # failure
228 253 respond_to do |format|
229 254 format.html { render :action => 'edit' }
230 255 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
231 256 end
232 257 end
233 258 rescue ActiveRecord::StaleObjectError
234 259 # Optimistic locking exception
235 260 flash.now[:error] = l(:notice_locking_conflict)
236 261 # Remove the previously added attachments if issue was not updated
237 262 attachments.each(&:destroy)
238 263 end
239 264
240 #--
241 # Start converting to the Rails REST controllers
242 #++
243 def update
244 edit
245 end
246
247 265 def reply
248 266 journal = Journal.find(params[:journal_id]) if params[:journal_id]
249 267 if journal
250 268 user = journal.user
251 269 text = journal.notes
252 270 else
253 271 user = @issue.author
254 272 text = @issue.description
255 273 end
256 274 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
257 275 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
258 276 render(:update) { |page|
259 277 page.<< "$('notes').value = \"#{content}\";"
260 278 page.show 'update'
261 279 page << "Form.Element.focus('notes');"
262 280 page << "Element.scrollTo('update');"
263 281 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
264 282 }
265 283 end
266 284
267 285 # Bulk edit a set of issues
268 286 def bulk_edit
269 287 if request.post?
270 288 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
271 289 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
272 290 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
273 291
274 292 unsaved_issue_ids = []
275 293 @issues.each do |issue|
276 294 journal = issue.init_journal(User.current, params[:notes])
277 295 issue.safe_attributes = attributes
278 296 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
279 297 unless issue.save
280 298 # Keep unsaved issue ids to display them in flash error
281 299 unsaved_issue_ids << issue.id
282 300 end
283 301 end
284 302 if unsaved_issue_ids.empty?
285 303 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
286 304 else
287 305 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
288 306 :total => @issues.size,
289 307 :ids => '#' + unsaved_issue_ids.join(', #'))
290 308 end
291 309 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
292 310 return
293 311 end
294 312 @available_statuses = Workflow.available_statuses(@project)
295 313 @custom_fields = @project.all_issue_custom_fields
296 314 end
297 315
298 316 def move
299 317 @copy = params[:copy_options] && params[:copy_options][:copy]
300 318 @allowed_projects = []
301 319 # find projects to which the user is allowed to move the issue
302 320 if User.current.admin?
303 321 # admin is allowed to move issues to any active (visible) project
304 322 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
305 323 else
306 324 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
307 325 end
308 326 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
309 327 @target_project ||= @project
310 328 @trackers = @target_project.trackers
311 329 @available_statuses = Workflow.available_statuses(@project)
312 330 if request.post?
313 331 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
314 332 unsaved_issue_ids = []
315 333 moved_issues = []
316 334 @issues.each do |issue|
317 335 changed_attributes = {}
318 336 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
319 337 unless params[valid_attribute].blank?
320 338 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
321 339 end
322 340 end
323 341 issue.init_journal(User.current)
324 342 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
325 343 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
326 344 moved_issues << r
327 345 else
328 346 unsaved_issue_ids << issue.id
329 347 end
330 348 end
331 349 if unsaved_issue_ids.empty?
332 350 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
333 351 else
334 352 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
335 353 :total => @issues.size,
336 354 :ids => '#' + unsaved_issue_ids.join(', #'))
337 355 end
338 356 if params[:follow]
339 357 if @issues.size == 1 && moved_issues.size == 1
340 358 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
341 359 else
342 360 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
343 361 end
344 362 else
345 363 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
346 364 end
347 365 return
348 366 end
349 367 render :layout => false if request.xhr?
350 368 end
351 369
352 370 def destroy
353 371 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
354 372 if @hours > 0
355 373 case params[:todo]
356 374 when 'destroy'
357 375 # nothing to do
358 376 when 'nullify'
359 377 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
360 378 when 'reassign'
361 379 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
362 380 if reassign_to.nil?
363 381 flash.now[:error] = l(:error_issue_not_found_in_project)
364 382 return
365 383 else
366 384 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
367 385 end
368 386 else
369 387 unless params[:format] == 'xml'
370 388 # display the destroy form if it's a user request
371 389 return
372 390 end
373 391 end
374 392 end
375 393 @issues.each(&:destroy)
376 394 respond_to do |format|
377 395 format.html { redirect_to :action => 'index', :project_id => @project }
378 396 format.xml { head :ok }
379 397 end
380 398 end
381 399
382 400 def gantt
383 401 @gantt = Redmine::Helpers::Gantt.new(params)
384 402 retrieve_query
385 403 @query.group_by = nil
386 404 if @query.valid?
387 405 events = []
388 406 # Issues that have start and due dates
389 407 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
390 408 :order => "start_date, due_date",
391 409 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
392 410 )
393 411 # Issues that don't have a due date but that are assigned to a version with a date
394 412 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
395 413 :order => "start_date, effective_date",
396 414 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
397 415 )
398 416 # Versions
399 417 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
400 418
401 419 @gantt.events = events
402 420 end
403 421
404 422 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
405 423
406 424 respond_to do |format|
407 425 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
408 426 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
409 427 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
410 428 end
411 429 end
412 430
413 431 def calendar
414 432 if params[:year] and params[:year].to_i > 1900
415 433 @year = params[:year].to_i
416 434 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
417 435 @month = params[:month].to_i
418 436 end
419 437 end
420 438 @year ||= Date.today.year
421 439 @month ||= Date.today.month
422 440
423 441 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
424 442 retrieve_query
425 443 @query.group_by = nil
426 444 if @query.valid?
427 445 events = []
428 446 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
429 447 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
430 448 )
431 449 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
432 450
433 451 @calendar.events = events
434 452 end
435 453
436 454 render :layout => false if request.xhr?
437 455 end
438 456
439 457 def context_menu
440 458 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
441 459 if (@issues.size == 1)
442 460 @issue = @issues.first
443 461 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
444 462 end
445 463 projects = @issues.collect(&:project).compact.uniq
446 464 @project = projects.first if projects.size == 1
447 465
448 466 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
449 467 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
450 468 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
451 469 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
452 470 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
453 471 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
454 472 }
455 473 if @project
456 474 @assignables = @project.assignable_users
457 475 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
458 476 @trackers = @project.trackers
459 477 end
460 478
461 479 @priorities = IssuePriority.all.reverse
462 480 @statuses = IssueStatus.find(:all, :order => 'position')
463 481 @back = params[:back_url] || request.env['HTTP_REFERER']
464 482
465 483 render :layout => false
466 484 end
467 485
468 486 def update_form
469 487 if params[:id].blank?
470 488 @issue = Issue.new
471 489 @issue.project = @project
472 490 else
473 491 @issue = @project.issues.visible.find(params[:id])
474 492 end
475 493 @issue.attributes = params[:issue]
476 494 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
477 495 @priorities = IssuePriority.all
478 496
479 497 render :partial => 'attributes'
480 498 end
481 499
482 500 def preview
483 501 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
484 502 @attachements = @issue.attachments if @issue
485 503 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
486 504 render :partial => 'common/preview'
487 505 end
488 506
489 507 private
490 508 def find_issue
491 509 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
492 510 @project = @issue.project
493 511 rescue ActiveRecord::RecordNotFound
494 512 render_404
495 513 end
496 514
497 515 # Filter for bulk operations
498 516 def find_issues
499 517 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
500 518 raise ActiveRecord::RecordNotFound if @issues.empty?
501 519 projects = @issues.collect(&:project).compact.uniq
502 520 if projects.size == 1
503 521 @project = projects.first
504 522 else
505 523 # TODO: let users bulk edit/move/destroy issues from different projects
506 524 render_error 'Can not bulk edit/move/destroy issues from different projects'
507 525 return false
508 526 end
509 527 rescue ActiveRecord::RecordNotFound
510 528 render_404
511 529 end
512 530
513 531 def find_project
514 532 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
515 533 @project = Project.find(project_id)
516 534 rescue ActiveRecord::RecordNotFound
517 535 render_404
518 536 end
519 537
520 538 def find_optional_project
521 539 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
522 540 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
523 541 allowed ? true : deny_access
524 542 rescue ActiveRecord::RecordNotFound
525 543 render_404
526 544 end
527 545
528 546 # Retrieve query from session or build a new query
529 547 def retrieve_query
530 548 if !params[:query_id].blank?
531 549 cond = "project_id IS NULL"
532 550 cond << " OR project_id = #{@project.id}" if @project
533 551 @query = Query.find(params[:query_id], :conditions => cond)
534 552 @query.project = @project
535 553 session[:query] = {:id => @query.id, :project_id => @query.project_id}
536 554 sort_clear
537 555 else
538 556 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
539 557 # Give it a name, required to be valid
540 558 @query = Query.new(:name => "_")
541 559 @query.project = @project
542 560 if params[:fields] and params[:fields].is_a? Array
543 561 params[:fields].each do |field|
544 562 @query.add_filter(field, params[:operators][field], params[:values][field])
545 563 end
546 564 else
547 565 @query.available_filters.keys.each do |field|
548 566 @query.add_short_filter(field, params[field]) if params[field]
549 567 end
550 568 end
551 569 @query.group_by = params[:group_by]
552 570 @query.column_names = params[:query] && params[:query][:column_names]
553 571 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
554 572 else
555 573 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
556 574 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
557 575 @query.project = @project
558 576 end
559 577 end
560 578 end
561 579
562 580 # Rescues an invalid query statement. Just in case...
563 581 def query_statement_invalid(exception)
564 582 logger.error "Query::StatementInvalid: #{exception.message}" if logger
565 583 session.delete(:query)
566 584 sort_clear
567 585 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
568 586 end
569 587 end
@@ -1,287 +1,288
1 1 ActionController::Routing::Routes.draw do |map|
2 2 # Add your own custom routes here.
3 3 # The priority is based upon order of creation: first created -> highest priority.
4 4
5 5 # Here's a sample route:
6 6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 7 # Keep in mind you can assign values other than :controller and :action
8 8
9 9 map.home '', :controller => 'welcome'
10 10
11 11 map.signin 'login', :controller => 'account', :action => 'login'
12 12 map.signout 'logout', :controller => 'account', :action => 'logout'
13 13
14 14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
15 15 map.connect 'help/:ctrl/:page', :controller => 'help'
16 16
17 17 map.connect 'time_entries/:id/edit', :action => 'edit', :controller => 'timelog'
18 18 map.connect 'projects/:project_id/time_entries/new', :action => 'edit', :controller => 'timelog'
19 19 map.connect 'projects/:project_id/issues/:issue_id/time_entries/new', :action => 'edit', :controller => 'timelog'
20 20
21 21 map.with_options :controller => 'timelog' do |timelog|
22 22 timelog.connect 'projects/:project_id/time_entries', :action => 'details'
23 23
24 24 timelog.with_options :action => 'details', :conditions => {:method => :get} do |time_details|
25 25 time_details.connect 'time_entries'
26 26 time_details.connect 'time_entries.:format'
27 27 time_details.connect 'issues/:issue_id/time_entries'
28 28 time_details.connect 'issues/:issue_id/time_entries.:format'
29 29 time_details.connect 'projects/:project_id/time_entries.:format'
30 30 time_details.connect 'projects/:project_id/issues/:issue_id/time_entries'
31 31 time_details.connect 'projects/:project_id/issues/:issue_id/time_entries.:format'
32 32 end
33 33 timelog.connect 'projects/:project_id/time_entries/report', :action => 'report'
34 34 timelog.with_options :action => 'report',:conditions => {:method => :get} do |time_report|
35 35 time_report.connect 'time_entries/report'
36 36 time_report.connect 'time_entries/report.:format'
37 37 time_report.connect 'projects/:project_id/time_entries/report.:format'
38 38 end
39 39
40 40 timelog.with_options :action => 'edit', :conditions => {:method => :get} do |time_edit|
41 41 time_edit.connect 'issues/:issue_id/time_entries/new'
42 42 end
43 43
44 44 timelog.connect 'time_entries/:id/destroy', :action => 'destroy', :conditions => {:method => :post}
45 45 end
46 46
47 47 map.connect 'projects/:id/wiki', :controller => 'wikis', :action => 'edit', :conditions => {:method => :post}
48 48 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :get}
49 49 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :post}
50 50 map.with_options :controller => 'wiki' do |wiki_routes|
51 51 wiki_routes.with_options :conditions => {:method => :get} do |wiki_views|
52 52 wiki_views.connect 'projects/:id/wiki/:page', :action => 'special', :page => /page_index|date_index|export/i
53 53 wiki_views.connect 'projects/:id/wiki/:page', :action => 'index', :page => nil
54 54 wiki_views.connect 'projects/:id/wiki/:page/edit', :action => 'edit'
55 55 wiki_views.connect 'projects/:id/wiki/:page/rename', :action => 'rename'
56 56 wiki_views.connect 'projects/:id/wiki/:page/history', :action => 'history'
57 57 wiki_views.connect 'projects/:id/wiki/:page/diff/:version/vs/:version_from', :action => 'diff'
58 58 wiki_views.connect 'projects/:id/wiki/:page/annotate/:version', :action => 'annotate'
59 59 end
60 60
61 61 wiki_routes.connect 'projects/:id/wiki/:page/:action',
62 62 :action => /edit|rename|destroy|preview|protect/,
63 63 :conditions => {:method => :post}
64 64 end
65 65
66 66 map.with_options :controller => 'messages' do |messages_routes|
67 67 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
68 68 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
69 69 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
70 70 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
71 71 end
72 72 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
73 73 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
74 74 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
75 75 messages_actions.connect 'boards/:board_id/topics/:id/:action', :action => /edit|destroy/
76 76 end
77 77 end
78 78
79 79 map.with_options :controller => 'boards' do |board_routes|
80 80 board_routes.with_options :conditions => {:method => :get} do |board_views|
81 81 board_views.connect 'projects/:project_id/boards', :action => 'index'
82 82 board_views.connect 'projects/:project_id/boards/new', :action => 'new'
83 83 board_views.connect 'projects/:project_id/boards/:id', :action => 'show'
84 84 board_views.connect 'projects/:project_id/boards/:id.:format', :action => 'show'
85 85 board_views.connect 'projects/:project_id/boards/:id/edit', :action => 'edit'
86 86 end
87 87 board_routes.with_options :conditions => {:method => :post} do |board_actions|
88 88 board_actions.connect 'projects/:project_id/boards', :action => 'new'
89 89 board_actions.connect 'projects/:project_id/boards/:id/:action', :action => /edit|destroy/
90 90 end
91 91 end
92 92
93 93 map.with_options :controller => 'documents' do |document_routes|
94 94 document_routes.with_options :conditions => {:method => :get} do |document_views|
95 95 document_views.connect 'projects/:project_id/documents', :action => 'index'
96 96 document_views.connect 'projects/:project_id/documents/new', :action => 'new'
97 97 document_views.connect 'documents/:id', :action => 'show'
98 98 document_views.connect 'documents/:id/edit', :action => 'edit'
99 99 end
100 100 document_routes.with_options :conditions => {:method => :post} do |document_actions|
101 101 document_actions.connect 'projects/:project_id/documents', :action => 'new'
102 102 document_actions.connect 'documents/:id/:action', :action => /destroy|edit/
103 103 end
104 104 end
105 105
106 106 map.with_options :controller => 'issues' do |issues_routes|
107 107 issues_routes.with_options :conditions => {:method => :get} do |issues_views|
108 108 issues_views.connect 'issues', :action => 'index'
109 109 issues_views.connect 'issues.:format', :action => 'index'
110 110 issues_views.connect 'projects/:project_id/issues', :action => 'index'
111 111 issues_views.connect 'projects/:project_id/issues.:format', :action => 'index'
112 112 issues_views.connect 'projects/:project_id/issues/new', :action => 'new'
113 113 issues_views.connect 'projects/:project_id/issues/gantt', :action => 'gantt'
114 114 issues_views.connect 'projects/:project_id/issues/calendar', :action => 'calendar'
115 115 issues_views.connect 'projects/:project_id/issues/:copy_from/copy', :action => 'new'
116 116 issues_views.connect 'issues/:id', :action => 'show', :id => /\d+/
117 117 issues_views.connect 'issues/:id.:format', :action => 'show', :id => /\d+/
118 118 issues_views.connect 'issues/:id/edit', :action => 'edit', :id => /\d+/
119 119 issues_views.connect 'issues/:id/move', :action => 'move', :id => /\d+/
120 120 end
121 121 issues_routes.with_options :conditions => {:method => :post} do |issues_actions|
122 122 issues_actions.connect 'issues', :action => 'index'
123 123 issues_actions.connect 'projects/:project_id/issues', :action => 'new'
124 124 issues_actions.connect 'issues/:id/quoted', :action => 'reply', :id => /\d+/
125 125 issues_actions.connect 'issues/:id/:action', :action => /edit|move|destroy/, :id => /\d+/
126 126 issues_actions.connect 'issues.:format', :action => 'new', :format => /xml/
127 127 end
128 128 issues_routes.with_options :conditions => {:method => :put} do |issues_actions|
129 issues_actions.connect 'issues/:id.:format', :action => 'edit', :id => /\d+/, :format => /xml/
129 issues_actions.connect 'issues/:id/edit', :action => 'update', :id => /\d+/
130 issues_actions.connect 'issues/:id.:format', :action => 'update', :id => /\d+/, :format => /xml/
130 131 end
131 132 issues_routes.with_options :conditions => {:method => :delete} do |issues_actions|
132 133 issues_actions.connect 'issues/:id.:format', :action => 'destroy', :id => /\d+/, :format => /xml/
133 134 end
134 135 issues_routes.connect 'issues/:action'
135 136 end
136 137
137 138 map.with_options :controller => 'issue_relations', :conditions => {:method => :post} do |relations|
138 139 relations.connect 'issues/:issue_id/relations/:id', :action => 'new'
139 140 relations.connect 'issues/:issue_id/relations/:id/destroy', :action => 'destroy'
140 141 end
141 142
142 143 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
143 144 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
144 145 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
145 146 end
146 147
147 148 map.with_options :controller => 'news' do |news_routes|
148 149 news_routes.with_options :conditions => {:method => :get} do |news_views|
149 150 news_views.connect 'news', :action => 'index'
150 151 news_views.connect 'projects/:project_id/news', :action => 'index'
151 152 news_views.connect 'projects/:project_id/news.:format', :action => 'index'
152 153 news_views.connect 'news.:format', :action => 'index'
153 154 news_views.connect 'projects/:project_id/news/new', :action => 'new'
154 155 news_views.connect 'news/:id', :action => 'show'
155 156 news_views.connect 'news/:id/edit', :action => 'edit'
156 157 end
157 158 news_routes.with_options do |news_actions|
158 159 news_actions.connect 'projects/:project_id/news', :action => 'new'
159 160 news_actions.connect 'news/:id/edit', :action => 'edit'
160 161 news_actions.connect 'news/:id/destroy', :action => 'destroy'
161 162 end
162 163 end
163 164
164 165 map.connect 'projects/:id/members/new', :controller => 'members', :action => 'new'
165 166
166 167 map.with_options :controller => 'users' do |users|
167 168 users.with_options :conditions => {:method => :get} do |user_views|
168 169 user_views.connect 'users', :action => 'index'
169 170 user_views.connect 'users/:id', :action => 'show', :id => /\d+/
170 171 user_views.connect 'users/new', :action => 'add'
171 172 user_views.connect 'users/:id/edit/:tab', :action => 'edit', :tab => nil
172 173 end
173 174 users.with_options :conditions => {:method => :post} do |user_actions|
174 175 user_actions.connect 'users', :action => 'add'
175 176 user_actions.connect 'users/new', :action => 'add'
176 177 user_actions.connect 'users/:id/edit', :action => 'edit'
177 178 user_actions.connect 'users/:id/memberships', :action => 'edit_membership'
178 179 user_actions.connect 'users/:id/memberships/:membership_id', :action => 'edit_membership'
179 180 user_actions.connect 'users/:id/memberships/:membership_id/destroy', :action => 'destroy_membership'
180 181 end
181 182 end
182 183
183 184 map.with_options :controller => 'projects' do |projects|
184 185 projects.with_options :conditions => {:method => :get} do |project_views|
185 186 project_views.connect 'projects', :action => 'index'
186 187 project_views.connect 'projects.:format', :action => 'index'
187 188 project_views.connect 'projects/new', :action => 'add'
188 189 project_views.connect 'projects/:id', :action => 'show'
189 190 project_views.connect 'projects/:id.:format', :action => 'show'
190 191 project_views.connect 'projects/:id/:action', :action => /roadmap|destroy|settings/
191 192 project_views.connect 'projects/:id/files', :action => 'list_files'
192 193 project_views.connect 'projects/:id/files/new', :action => 'add_file'
193 194 project_views.connect 'projects/:id/versions/new', :action => 'add_version'
194 195 project_views.connect 'projects/:id/categories/new', :action => 'add_issue_category'
195 196 project_views.connect 'projects/:id/settings/:tab', :action => 'settings'
196 197 end
197 198
198 199 projects.with_options :action => 'activity', :conditions => {:method => :get} do |activity|
199 200 activity.connect 'projects/:id/activity'
200 201 activity.connect 'projects/:id/activity.:format'
201 202 activity.connect 'activity', :id => nil
202 203 activity.connect 'activity.:format', :id => nil
203 204 end
204 205
205 206 projects.with_options :conditions => {:method => :post} do |project_actions|
206 207 project_actions.connect 'projects/new', :action => 'add'
207 208 project_actions.connect 'projects', :action => 'add'
208 209 project_actions.connect 'projects.:format', :action => 'add', :format => /xml/
209 210 project_actions.connect 'projects/:id/:action', :action => /edit|destroy|archive|unarchive/
210 211 project_actions.connect 'projects/:id/files/new', :action => 'add_file'
211 212 project_actions.connect 'projects/:id/versions/new', :action => 'add_version'
212 213 project_actions.connect 'projects/:id/categories/new', :action => 'add_issue_category'
213 214 project_actions.connect 'projects/:id/activities/save', :action => 'save_activities'
214 215 end
215 216
216 217 projects.with_options :conditions => {:method => :put} do |project_actions|
217 218 project_actions.conditions 'projects/:id.:format', :action => 'edit', :format => /xml/
218 219 end
219 220
220 221 projects.with_options :conditions => {:method => :delete} do |project_actions|
221 222 project_actions.conditions 'projects/:id.:format', :action => 'destroy', :format => /xml/
222 223 project_actions.conditions 'projects/:id/reset_activities', :action => 'reset_activities'
223 224 end
224 225 end
225 226
226 227 map.with_options :controller => 'versions' do |versions|
227 228 versions.with_options :conditions => {:method => :post} do |version_actions|
228 229 version_actions.connect 'projects/:project_id/versions/close_completed', :action => 'close_completed'
229 230 end
230 231 end
231 232
232 233 map.with_options :controller => 'repositories' do |repositories|
233 234 repositories.with_options :conditions => {:method => :get} do |repository_views|
234 235 repository_views.connect 'projects/:id/repository', :action => 'show'
235 236 repository_views.connect 'projects/:id/repository/edit', :action => 'edit'
236 237 repository_views.connect 'projects/:id/repository/statistics', :action => 'stats'
237 238 repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions'
238 239 repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions'
239 240 repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision'
240 241 repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff'
241 242 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff'
242 243 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
243 244 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
244 245 repository_views.connect 'projects/:id/repository/raw/*path', :action => 'entry', :format => 'raw'
245 246 # TODO: why the following route is required?
246 247 repository_views.connect 'projects/:id/repository/entry/*path', :action => 'entry'
247 248 repository_views.connect 'projects/:id/repository/:action/*path'
248 249 end
249 250
250 251 repositories.connect 'projects/:id/repository/:action', :conditions => {:method => :post}
251 252 end
252 253
253 254 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
254 255 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
255 256 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
256 257
257 258 map.resources :groups
258 259
259 260 #left old routes at the bottom for backwards compat
260 261 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
261 262 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
262 263 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
263 264 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
264 265 map.connect 'wiki/:id/:page/:action', :page => nil, :controller => 'wiki'
265 266 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
266 267 map.connect 'projects/:project_id/news/:action', :controller => 'news'
267 268 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
268 269 map.with_options :controller => 'repositories' do |omap|
269 270 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
270 271 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
271 272 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
272 273 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
273 274 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
274 275 omap.connect 'repositories/revision/:id/:rev', :action => 'revision'
275 276 end
276 277
277 278 map.with_options :controller => 'sys' do |sys|
278 279 sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get}
279 280 sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post}
280 281 end
281 282
282 283 # Install the default route as the lowest priority.
283 284 map.connect ':controller/:action/:id'
284 285 map.connect 'robots.txt', :controller => 'welcome', :action => 'robots'
285 286 # Used for OpenID
286 287 map.root :controller => 'account', :action => 'login'
287 288 end
@@ -1,167 +1,167
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "#{File.dirname(__FILE__)}/../test_helper"
19 19
20 20 class IssuesApiTest < ActionController::IntegrationTest
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :versions,
29 29 :trackers,
30 30 :projects_trackers,
31 31 :issue_categories,
32 32 :enabled_modules,
33 33 :enumerations,
34 34 :attachments,
35 35 :workflows,
36 36 :custom_fields,
37 37 :custom_values,
38 38 :custom_fields_projects,
39 39 :custom_fields_trackers,
40 40 :time_entries,
41 41 :journals,
42 42 :journal_details,
43 43 :queries
44 44
45 45 def setup
46 46 Setting.rest_api_enabled = '1'
47 47 end
48 48
49 49 def test_index_routing
50 50 assert_routing(
51 51 {:method => :get, :path => '/issues.xml'},
52 52 :controller => 'issues', :action => 'index', :format => 'xml'
53 53 )
54 54 end
55 55
56 56 def test_index
57 57 get '/issues.xml'
58 58 assert_response :success
59 59 assert_equal 'application/xml', @response.content_type
60 60 end
61 61
62 62 def test_index_with_filter
63 63 get '/issues.xml?status_id=5'
64 64 assert_response :success
65 65 assert_equal 'application/xml', @response.content_type
66 66 assert_tag :tag => 'issues',
67 67 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
68 68 :only => { :tag => 'issue' } }
69 69 end
70 70
71 71 def test_show_routing
72 72 assert_routing(
73 73 {:method => :get, :path => '/issues/1.xml'},
74 74 :controller => 'issues', :action => 'show', :id => '1', :format => 'xml'
75 75 )
76 76 end
77 77
78 78 def test_show
79 79 get '/issues/1.xml'
80 80 assert_response :success
81 81 assert_equal 'application/xml', @response.content_type
82 82 end
83 83
84 84 def test_create_routing
85 85 assert_routing(
86 86 {:method => :post, :path => '/issues.xml'},
87 87 :controller => 'issues', :action => 'new', :format => 'xml'
88 88 )
89 89 end
90 90
91 91 def test_create
92 92 attributes = {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}
93 93 assert_difference 'Issue.count' do
94 94 post '/issues.xml', {:issue => attributes}, :authorization => credentials('jsmith')
95 95 end
96 96 assert_response :created
97 97 assert_equal 'application/xml', @response.content_type
98 98 issue = Issue.first(:order => 'id DESC')
99 99 attributes.each do |attribute, value|
100 100 assert_equal value, issue.send(attribute)
101 101 end
102 102 end
103 103
104 104 def test_create_failure
105 105 attributes = {:project_id => 1}
106 106 assert_no_difference 'Issue.count' do
107 107 post '/issues.xml', {:issue => attributes}, :authorization => credentials('jsmith')
108 108 end
109 109 assert_response :unprocessable_entity
110 110 assert_equal 'application/xml', @response.content_type
111 111 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
112 112 end
113 113
114 114 def test_update_routing
115 115 assert_routing(
116 116 {:method => :put, :path => '/issues/1.xml'},
117 :controller => 'issues', :action => 'edit', :id => '1', :format => 'xml'
117 :controller => 'issues', :action => 'update', :id => '1', :format => 'xml'
118 118 )
119 119 end
120 120
121 121 def test_update
122 122 attributes = {:subject => 'API update'}
123 123 assert_no_difference 'Issue.count' do
124 124 assert_difference 'Journal.count' do
125 125 put '/issues/1.xml', {:issue => attributes}, :authorization => credentials('jsmith')
126 126 end
127 127 end
128 128 assert_response :ok
129 129 assert_equal 'application/xml', @response.content_type
130 130 issue = Issue.find(1)
131 131 attributes.each do |attribute, value|
132 132 assert_equal value, issue.send(attribute)
133 133 end
134 134 end
135 135
136 136 def test_update_failure
137 137 attributes = {:subject => ''}
138 138 assert_no_difference 'Issue.count' do
139 139 assert_no_difference 'Journal.count' do
140 140 put '/issues/1.xml', {:issue => attributes}, :authorization => credentials('jsmith')
141 141 end
142 142 end
143 143 assert_response :unprocessable_entity
144 144 assert_equal 'application/xml', @response.content_type
145 145 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
146 146 end
147 147
148 148 def test_destroy_routing
149 149 assert_routing(
150 150 {:method => :delete, :path => '/issues/1.xml'},
151 151 :controller => 'issues', :action => 'destroy', :id => '1', :format => 'xml'
152 152 )
153 153 end
154 154
155 155 def test_destroy
156 156 assert_difference 'Issue.count', -1 do
157 157 delete '/issues/1.xml', {}, :authorization => credentials('jsmith')
158 158 end
159 159 assert_response :ok
160 160 assert_equal 'application/xml', @response.content_type
161 161 assert_nil Issue.find_by_id(1)
162 162 end
163 163
164 164 def credentials(user, password=nil)
165 165 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
166 166 end
167 167 end
@@ -1,129 +1,129
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "#{File.dirname(__FILE__)}/../test_helper"
19 19
20 20 class IssuesTest < ActionController::IntegrationTest
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :trackers,
26 26 :projects_trackers,
27 27 :enabled_modules,
28 28 :issue_statuses,
29 29 :issues,
30 30 :enumerations,
31 31 :custom_fields,
32 32 :custom_values,
33 33 :custom_fields_trackers
34 34
35 35 # create an issue
36 36 def test_add_issue
37 37 log_user('jsmith', 'jsmith')
38 38 get 'projects/1/issues/new', :tracker_id => '1'
39 39 assert_response :success
40 40 assert_template 'issues/new'
41 41
42 42 post 'projects/1/issues', :tracker_id => "1",
43 43 :issue => { :start_date => "2006-12-26",
44 44 :priority_id => "4",
45 45 :subject => "new test issue",
46 46 :category_id => "",
47 47 :description => "new issue",
48 48 :done_ratio => "0",
49 49 :due_date => "",
50 50 :assigned_to_id => "" },
51 51 :custom_fields => {'2' => 'Value for field 2'}
52 52 # find created issue
53 53 issue = Issue.find_by_subject("new test issue")
54 54 assert_kind_of Issue, issue
55 55
56 56 # check redirection
57 57 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
58 58 follow_redirect!
59 59 assert_equal issue, assigns(:issue)
60 60
61 61 # check issue attributes
62 62 assert_equal 'jsmith', issue.author.login
63 63 assert_equal 1, issue.project.id
64 64 assert_equal 1, issue.status.id
65 65 end
66 66
67 67 # add then remove 2 attachments to an issue
68 68 def test_issue_attachements
69 69 log_user('jsmith', 'jsmith')
70 70 set_tmp_attachments_directory
71 71
72 post 'issues/1/edit',
72 put 'issues/1/edit',
73 73 :notes => 'Some notes',
74 74 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
75 75 assert_redirected_to "issues/1"
76 76
77 77 # make sure attachment was saved
78 78 attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
79 79 assert_kind_of Attachment, attachment
80 80 assert_equal Issue.find(1), attachment.container
81 81 assert_equal 'This is an attachment', attachment.description
82 82 # verify the size of the attachment stored in db
83 83 #assert_equal file_data_1.length, attachment.filesize
84 84 # verify that the attachment was written to disk
85 85 assert File.exist?(attachment.diskfile)
86 86
87 87 # remove the attachments
88 88 Issue.find(1).attachments.each(&:destroy)
89 89 assert_equal 0, Issue.find(1).attachments.length
90 90 end
91 91
92 92 def test_other_formats_links_on_get_index
93 93 get '/projects/ecookbook/issues'
94 94
95 95 %w(Atom PDF CSV).each do |format|
96 96 assert_tag :a, :content => format,
97 97 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
98 98 :rel => 'nofollow' }
99 99 end
100 100 end
101 101
102 102 def test_other_formats_links_on_post_index_without_project_id_in_url
103 103 post '/issues', :project_id => 'ecookbook'
104 104
105 105 %w(Atom PDF CSV).each do |format|
106 106 assert_tag :a, :content => format,
107 107 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
108 108 :rel => 'nofollow' }
109 109 end
110 110 end
111 111
112 112 def test_pagination_links_on_get_index
113 113 Setting.per_page_options = '2'
114 114 get '/projects/ecookbook/issues'
115 115
116 116 assert_tag :a, :content => '2',
117 117 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
118 118
119 119 end
120 120
121 121 def test_pagination_links_on_post_index_without_project_id_in_url
122 122 Setting.per_page_options = '2'
123 123 post '/issues', :project_id => 'ecookbook'
124 124
125 125 assert_tag :a, :content => '2',
126 126 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
127 127
128 128 end
129 129 end
General Comments 0
You need to be logged in to leave comments. Login now