##// END OF EJS Templates
Fixed: issue status bulk edit broken by r2726 (#3347)....
Jean-Philippe Lang -
r2645:f7d7186c133d
parent child
Show More
@@ -1,506 +1,506
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
21 21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 26 accept_key_auth :index, :changes
27 27
28 28 helper :journals
29 29 helper :projects
30 30 include ProjectsHelper
31 31 helper :custom_fields
32 32 include CustomFieldsHelper
33 33 helper :issue_relations
34 34 include IssueRelationsHelper
35 35 helper :watchers
36 36 include WatchersHelper
37 37 helper :attachments
38 38 include AttachmentsHelper
39 39 helper :queries
40 40 helper :sort
41 41 include SortHelper
42 42 include IssuesHelper
43 43 helper :timelog
44 44 include Redmine::Export::PDF
45 45
46 46 def index
47 47 retrieve_query
48 48 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
49 49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50 50
51 51 if @query.valid?
52 52 limit = per_page_option
53 53 respond_to do |format|
54 54 format.html { }
55 55 format.atom { }
56 56 format.csv { limit = Setting.issues_export_limit.to_i }
57 57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 58 end
59 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 61 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
62 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 63 :conditions => @query.statement,
64 64 :limit => limit,
65 65 :offset => @issue_pages.current.offset
66 66 respond_to do |format|
67 67 format.html {
68 68 if @query.grouped?
69 69 # Retrieve the issue count by group
70 70 @issue_count_by_group = begin
71 71 Issue.count(:group => @query.group_by, :include => [:status, :project], :conditions => @query.statement)
72 72 # Rails will raise an (unexpected) error if there's only a nil group value
73 73 rescue ActiveRecord::RecordNotFound
74 74 {nil => @issue_count}
75 75 end
76 76 end
77 77 render :template => 'issues/index.rhtml', :layout => !request.xhr?
78 78 }
79 79 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 80 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
81 81 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 82 end
83 83 else
84 84 # Send html if the query is not valid
85 85 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 86 end
87 87 rescue ActiveRecord::RecordNotFound
88 88 render_404
89 89 end
90 90
91 91 def changes
92 92 retrieve_query
93 93 sort_init 'id', 'desc'
94 94 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95 95
96 96 if @query.valid?
97 97 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
98 98 :conditions => @query.statement,
99 99 :limit => 25,
100 100 :order => "#{Journal.table_name}.created_on DESC"
101 101 end
102 102 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
103 103 render :layout => false, :content_type => 'application/atom+xml'
104 104 rescue ActiveRecord::RecordNotFound
105 105 render_404
106 106 end
107 107
108 108 def show
109 109 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
110 110 @journals.each_with_index {|j,i| j.indice = i+1}
111 111 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 112 @changesets = @issue.changesets
113 113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 114 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
115 115 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
116 116 @priorities = Enumeration.priorities
117 117 @time_entry = TimeEntry.new
118 118 respond_to do |format|
119 119 format.html { render :template => 'issues/show.rhtml' }
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 'No tracker is associated to this project. Please check the Project settings.'
135 135 return
136 136 end
137 137 if params[:issue].is_a?(Hash)
138 138 @issue.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 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
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 if @issue.save
158 158 attach_files(@issue, params[:attachments])
159 159 flash[:notice] = l(:notice_successful_create)
160 160 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 161 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
162 162 { :action => 'show', :id => @issue })
163 163 return
164 164 end
165 165 end
166 166 @priorities = Enumeration.priorities
167 167 render :layout => !request.xhr?
168 168 end
169 169
170 170 # Attributes that can be updated on workflow transition (without :edit permission)
171 171 # TODO: make it configurable (at least per role)
172 172 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
173 173
174 174 def edit
175 175 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
176 176 @priorities = Enumeration.priorities
177 177 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
178 178 @time_entry = TimeEntry.new
179 179
180 180 @notes = params[:notes]
181 181 journal = @issue.init_journal(User.current, @notes)
182 182 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
183 183 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
184 184 attrs = params[:issue].dup
185 185 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
186 186 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
187 187 @issue.attributes = attrs
188 188 end
189 189
190 190 if request.post?
191 191 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
192 192 @time_entry.attributes = params[:time_entry]
193 193 attachments = attach_files(@issue, params[:attachments])
194 194 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
195 195
196 196 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
197 197
198 198 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
199 199 # Log spend time
200 200 if User.current.allowed_to?(:log_time, @project)
201 201 @time_entry.save
202 202 end
203 203 if !journal.new_record?
204 204 # Only send notification if something was actually changed
205 205 flash[:notice] = l(:notice_successful_update)
206 206 end
207 207 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
208 208 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
209 209 end
210 210 end
211 211 rescue ActiveRecord::StaleObjectError
212 212 # Optimistic locking exception
213 213 flash.now[:error] = l(:notice_locking_conflict)
214 214 end
215 215
216 216 def reply
217 217 journal = Journal.find(params[:journal_id]) if params[:journal_id]
218 218 if journal
219 219 user = journal.user
220 220 text = journal.notes
221 221 else
222 222 user = @issue.author
223 223 text = @issue.description
224 224 end
225 225 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
226 226 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
227 227 render(:update) { |page|
228 228 page.<< "$('notes').value = \"#{content}\";"
229 229 page.show 'update'
230 230 page << "Form.Element.focus('notes');"
231 231 page << "Element.scrollTo('update');"
232 232 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
233 233 }
234 234 end
235 235
236 236 # Bulk edit a set of issues
237 237 def bulk_edit
238 238 if request.post?
239 239 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
240 240 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
241 241 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
242 242 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
243 243 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
244 244 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
245 245
246 246 unsaved_issue_ids = []
247 247 @issues.each do |issue|
248 248 journal = issue.init_journal(User.current, params[:notes])
249 249 issue.priority = priority if priority
250 250 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
251 251 issue.category = category if category || params[:category_id] == 'none'
252 252 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
253 253 issue.start_date = params[:start_date] unless params[:start_date].blank?
254 254 issue.due_date = params[:due_date] unless params[:due_date].blank?
255 255 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
256 256 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
257 257 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
258 258 # Don't save any change to the issue if the user is not authorized to apply the requested status
259 unless (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
259 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
260 260 # Keep unsaved issue ids to display them in flash error
261 261 unsaved_issue_ids << issue.id
262 262 end
263 263 end
264 264 if unsaved_issue_ids.empty?
265 265 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
266 266 else
267 267 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
268 268 :total => @issues.size,
269 269 :ids => '#' + unsaved_issue_ids.join(', #'))
270 270 end
271 271 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
272 272 return
273 273 end
274 274 # Find potential statuses the user could be allowed to switch issues to
275 275 @available_statuses = Workflow.find(:all, :include => :new_status,
276 276 :conditions => {:role_id => User.current.roles_for_project(@project).collect(&:id)}).collect(&:new_status).compact.uniq.sort
277 277 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
278 278 end
279 279
280 280 def move
281 281 @allowed_projects = []
282 282 # find projects to which the user is allowed to move the issue
283 283 if User.current.admin?
284 284 # admin is allowed to move issues to any active (visible) project
285 285 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
286 286 else
287 287 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
288 288 end
289 289 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
290 290 @target_project ||= @project
291 291 @trackers = @target_project.trackers
292 292 if request.post?
293 293 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
294 294 unsaved_issue_ids = []
295 295 @issues.each do |issue|
296 296 issue.init_journal(User.current)
297 297 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
298 298 end
299 299 if unsaved_issue_ids.empty?
300 300 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
301 301 else
302 302 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
303 303 :total => @issues.size,
304 304 :ids => '#' + unsaved_issue_ids.join(', #'))
305 305 end
306 306 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
307 307 return
308 308 end
309 309 render :layout => false if request.xhr?
310 310 end
311 311
312 312 def destroy
313 313 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
314 314 if @hours > 0
315 315 case params[:todo]
316 316 when 'destroy'
317 317 # nothing to do
318 318 when 'nullify'
319 319 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
320 320 when 'reassign'
321 321 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
322 322 if reassign_to.nil?
323 323 flash.now[:error] = l(:error_issue_not_found_in_project)
324 324 return
325 325 else
326 326 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
327 327 end
328 328 else
329 329 # display the destroy form
330 330 return
331 331 end
332 332 end
333 333 @issues.each(&:destroy)
334 334 redirect_to :action => 'index', :project_id => @project
335 335 end
336 336
337 337 def gantt
338 338 @gantt = Redmine::Helpers::Gantt.new(params)
339 339 retrieve_query
340 340 if @query.valid?
341 341 events = []
342 342 # Issues that have start and due dates
343 343 events += Issue.find(:all,
344 344 :order => "start_date, due_date",
345 345 :include => [:tracker, :status, :assigned_to, :priority, :project],
346 346 :conditions => ["(#{@query.statement}) AND (((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]
347 347 )
348 348 # Issues that don't have a due date but that are assigned to a version with a date
349 349 events += Issue.find(:all,
350 350 :order => "start_date, effective_date",
351 351 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
352 352 :conditions => ["(#{@query.statement}) AND (((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]
353 353 )
354 354 # Versions
355 355 events += Version.find(:all, :include => :project,
356 356 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
357 357
358 358 @gantt.events = events
359 359 end
360 360
361 361 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
362 362
363 363 respond_to do |format|
364 364 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
365 365 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
366 366 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
367 367 end
368 368 end
369 369
370 370 def calendar
371 371 if params[:year] and params[:year].to_i > 1900
372 372 @year = params[:year].to_i
373 373 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
374 374 @month = params[:month].to_i
375 375 end
376 376 end
377 377 @year ||= Date.today.year
378 378 @month ||= Date.today.month
379 379
380 380 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
381 381 retrieve_query
382 382 if @query.valid?
383 383 events = []
384 384 events += Issue.find(:all,
385 385 :include => [:tracker, :status, :assigned_to, :priority, :project],
386 386 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
387 387 )
388 388 events += Version.find(:all, :include => :project,
389 389 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
390 390
391 391 @calendar.events = events
392 392 end
393 393
394 394 render :layout => false if request.xhr?
395 395 end
396 396
397 397 def context_menu
398 398 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
399 399 if (@issues.size == 1)
400 400 @issue = @issues.first
401 401 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
402 402 end
403 403 projects = @issues.collect(&:project).compact.uniq
404 404 @project = projects.first if projects.size == 1
405 405
406 406 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
407 407 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
408 408 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
409 409 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
410 410 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
411 411 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
412 412 }
413 413 if @project
414 414 @assignables = @project.assignable_users
415 415 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
416 416 end
417 417
418 418 @priorities = Enumeration.priorities.reverse
419 419 @statuses = IssueStatus.find(:all, :order => 'position')
420 420 @back = request.env['HTTP_REFERER']
421 421
422 422 render :layout => false
423 423 end
424 424
425 425 def update_form
426 426 @issue = Issue.new(params[:issue])
427 427 render :action => :new, :layout => false
428 428 end
429 429
430 430 def preview
431 431 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
432 432 @attachements = @issue.attachments if @issue
433 433 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
434 434 render :partial => 'common/preview'
435 435 end
436 436
437 437 private
438 438 def find_issue
439 439 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
440 440 @project = @issue.project
441 441 rescue ActiveRecord::RecordNotFound
442 442 render_404
443 443 end
444 444
445 445 # Filter for bulk operations
446 446 def find_issues
447 447 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
448 448 raise ActiveRecord::RecordNotFound if @issues.empty?
449 449 projects = @issues.collect(&:project).compact.uniq
450 450 if projects.size == 1
451 451 @project = projects.first
452 452 else
453 453 # TODO: let users bulk edit/move/destroy issues from different projects
454 454 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
455 455 end
456 456 rescue ActiveRecord::RecordNotFound
457 457 render_404
458 458 end
459 459
460 460 def find_project
461 461 @project = Project.find(params[:project_id])
462 462 rescue ActiveRecord::RecordNotFound
463 463 render_404
464 464 end
465 465
466 466 def find_optional_project
467 467 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
468 468 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
469 469 allowed ? true : deny_access
470 470 rescue ActiveRecord::RecordNotFound
471 471 render_404
472 472 end
473 473
474 474 # Retrieve query from session or build a new query
475 475 def retrieve_query
476 476 if !params[:query_id].blank?
477 477 cond = "project_id IS NULL"
478 478 cond << " OR project_id = #{@project.id}" if @project
479 479 @query = Query.find(params[:query_id], :conditions => cond)
480 480 @query.project = @project
481 481 session[:query] = {:id => @query.id, :project_id => @query.project_id}
482 482 sort_clear
483 483 else
484 484 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
485 485 # Give it a name, required to be valid
486 486 @query = Query.new(:name => "_")
487 487 @query.project = @project
488 488 if params[:fields] and params[:fields].is_a? Array
489 489 params[:fields].each do |field|
490 490 @query.add_filter(field, params[:operators][field], params[:values][field])
491 491 end
492 492 else
493 493 @query.available_filters.keys.each do |field|
494 494 @query.add_short_filter(field, params[field]) if params[field]
495 495 end
496 496 end
497 497 @query.group_by = params[:group_by]
498 498 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
499 499 else
500 500 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
501 501 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
502 502 @query.project = @project
503 503 end
504 504 end
505 505 end
506 506 end
@@ -1,1069 +1,1081
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 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_trackers,
43 43 :time_entries,
44 44 :journals,
45 45 :journal_details
46 46
47 47 def setup
48 48 @controller = IssuesController.new
49 49 @request = ActionController::TestRequest.new
50 50 @response = ActionController::TestResponse.new
51 51 User.current = nil
52 52 end
53 53
54 54 def test_index_routing
55 55 assert_routing(
56 56 {:method => :get, :path => '/issues'},
57 57 :controller => 'issues', :action => 'index'
58 58 )
59 59 end
60 60
61 61 def test_index
62 62 Setting.default_language = 'en'
63 63
64 64 get :index
65 65 assert_response :success
66 66 assert_template 'index.rhtml'
67 67 assert_not_nil assigns(:issues)
68 68 assert_nil assigns(:project)
69 69 assert_tag :tag => 'a', :content => /Can't print recipes/
70 70 assert_tag :tag => 'a', :content => /Subproject issue/
71 71 # private projects hidden
72 72 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
73 73 assert_no_tag :tag => 'a', :content => /Issue on project 2/
74 74 # project column
75 75 assert_tag :tag => 'th', :content => /Project/
76 76 end
77 77
78 78 def test_index_should_not_list_issues_when_module_disabled
79 79 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
80 80 get :index
81 81 assert_response :success
82 82 assert_template 'index.rhtml'
83 83 assert_not_nil assigns(:issues)
84 84 assert_nil assigns(:project)
85 85 assert_no_tag :tag => 'a', :content => /Can't print recipes/
86 86 assert_tag :tag => 'a', :content => /Subproject issue/
87 87 end
88 88
89 89 def test_index_with_project_routing
90 90 assert_routing(
91 91 {:method => :get, :path => '/projects/23/issues'},
92 92 :controller => 'issues', :action => 'index', :project_id => '23'
93 93 )
94 94 end
95 95
96 96 def test_index_should_not_list_issues_when_module_disabled
97 97 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
98 98 get :index
99 99 assert_response :success
100 100 assert_template 'index.rhtml'
101 101 assert_not_nil assigns(:issues)
102 102 assert_nil assigns(:project)
103 103 assert_no_tag :tag => 'a', :content => /Can't print recipes/
104 104 assert_tag :tag => 'a', :content => /Subproject issue/
105 105 end
106 106
107 107 def test_index_with_project_routing
108 108 assert_routing(
109 109 {:method => :get, :path => 'projects/23/issues'},
110 110 :controller => 'issues', :action => 'index', :project_id => '23'
111 111 )
112 112 end
113 113
114 114 def test_index_with_project
115 115 Setting.display_subprojects_issues = 0
116 116 get :index, :project_id => 1
117 117 assert_response :success
118 118 assert_template 'index.rhtml'
119 119 assert_not_nil assigns(:issues)
120 120 assert_tag :tag => 'a', :content => /Can't print recipes/
121 121 assert_no_tag :tag => 'a', :content => /Subproject issue/
122 122 end
123 123
124 124 def test_index_with_project_and_subprojects
125 125 Setting.display_subprojects_issues = 1
126 126 get :index, :project_id => 1
127 127 assert_response :success
128 128 assert_template 'index.rhtml'
129 129 assert_not_nil assigns(:issues)
130 130 assert_tag :tag => 'a', :content => /Can't print recipes/
131 131 assert_tag :tag => 'a', :content => /Subproject issue/
132 132 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
133 133 end
134 134
135 135 def test_index_with_project_and_subprojects_should_show_private_subprojects
136 136 @request.session[:user_id] = 2
137 137 Setting.display_subprojects_issues = 1
138 138 get :index, :project_id => 1
139 139 assert_response :success
140 140 assert_template 'index.rhtml'
141 141 assert_not_nil assigns(:issues)
142 142 assert_tag :tag => 'a', :content => /Can't print recipes/
143 143 assert_tag :tag => 'a', :content => /Subproject issue/
144 144 assert_tag :tag => 'a', :content => /Issue of a private subproject/
145 145 end
146 146
147 147 def test_index_with_project_routing_formatted
148 148 assert_routing(
149 149 {:method => :get, :path => 'projects/23/issues.pdf'},
150 150 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
151 151 )
152 152 assert_routing(
153 153 {:method => :get, :path => 'projects/23/issues.atom'},
154 154 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
155 155 )
156 156 end
157 157
158 158 def test_index_with_project_and_filter
159 159 get :index, :project_id => 1, :set_filter => 1
160 160 assert_response :success
161 161 assert_template 'index.rhtml'
162 162 assert_not_nil assigns(:issues)
163 163 end
164 164
165 165 def test_index_with_query
166 166 get :index, :project_id => 1, :query_id => 5
167 167 assert_response :success
168 168 assert_template 'index.rhtml'
169 169 assert_not_nil assigns(:issues)
170 170 assert_nil assigns(:issue_count_by_group)
171 171 end
172 172
173 173 def test_index_with_grouped_query
174 174 get :index, :project_id => 1, :query_id => 6
175 175 assert_response :success
176 176 assert_template 'index.rhtml'
177 177 assert_not_nil assigns(:issues)
178 178 assert_not_nil assigns(:issue_count_by_group)
179 179 end
180 180
181 181 def test_index_csv_with_project
182 182 get :index, :format => 'csv'
183 183 assert_response :success
184 184 assert_not_nil assigns(:issues)
185 185 assert_equal 'text/csv', @response.content_type
186 186
187 187 get :index, :project_id => 1, :format => 'csv'
188 188 assert_response :success
189 189 assert_not_nil assigns(:issues)
190 190 assert_equal 'text/csv', @response.content_type
191 191 end
192 192
193 193 def test_index_formatted
194 194 assert_routing(
195 195 {:method => :get, :path => 'issues.pdf'},
196 196 :controller => 'issues', :action => 'index', :format => 'pdf'
197 197 )
198 198 assert_routing(
199 199 {:method => :get, :path => 'issues.atom'},
200 200 :controller => 'issues', :action => 'index', :format => 'atom'
201 201 )
202 202 end
203 203
204 204 def test_index_pdf
205 205 get :index, :format => 'pdf'
206 206 assert_response :success
207 207 assert_not_nil assigns(:issues)
208 208 assert_equal 'application/pdf', @response.content_type
209 209
210 210 get :index, :project_id => 1, :format => 'pdf'
211 211 assert_response :success
212 212 assert_not_nil assigns(:issues)
213 213 assert_equal 'application/pdf', @response.content_type
214 214
215 215 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
216 216 assert_response :success
217 217 assert_not_nil assigns(:issues)
218 218 assert_equal 'application/pdf', @response.content_type
219 219 end
220 220
221 221 def test_index_sort
222 222 get :index, :sort => 'tracker,id:desc'
223 223 assert_response :success
224 224
225 225 sort_params = @request.session['issues_index_sort']
226 226 assert sort_params.is_a?(String)
227 227 assert_equal 'tracker,id:desc', sort_params
228 228
229 229 issues = assigns(:issues)
230 230 assert_not_nil issues
231 231 assert !issues.empty?
232 232 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
233 233 end
234 234
235 235 def test_gantt
236 236 get :gantt, :project_id => 1
237 237 assert_response :success
238 238 assert_template 'gantt.rhtml'
239 239 assert_not_nil assigns(:gantt)
240 240 events = assigns(:gantt).events
241 241 assert_not_nil events
242 242 # Issue with start and due dates
243 243 i = Issue.find(1)
244 244 assert_not_nil i.due_date
245 245 assert events.include?(Issue.find(1))
246 246 # Issue with without due date but targeted to a version with date
247 247 i = Issue.find(2)
248 248 assert_nil i.due_date
249 249 assert events.include?(i)
250 250 end
251 251
252 252 def test_cross_project_gantt
253 253 get :gantt
254 254 assert_response :success
255 255 assert_template 'gantt.rhtml'
256 256 assert_not_nil assigns(:gantt)
257 257 events = assigns(:gantt).events
258 258 assert_not_nil events
259 259 end
260 260
261 261 def test_gantt_export_to_pdf
262 262 get :gantt, :project_id => 1, :format => 'pdf'
263 263 assert_response :success
264 264 assert_equal 'application/pdf', @response.content_type
265 265 assert @response.body.starts_with?('%PDF')
266 266 assert_not_nil assigns(:gantt)
267 267 end
268 268
269 269 def test_cross_project_gantt_export_to_pdf
270 270 get :gantt, :format => 'pdf'
271 271 assert_response :success
272 272 assert_equal 'application/pdf', @response.content_type
273 273 assert @response.body.starts_with?('%PDF')
274 274 assert_not_nil assigns(:gantt)
275 275 end
276 276
277 277 if Object.const_defined?(:Magick)
278 278 def test_gantt_image
279 279 get :gantt, :project_id => 1, :format => 'png'
280 280 assert_response :success
281 281 assert_equal 'image/png', @response.content_type
282 282 end
283 283 else
284 284 puts "RMagick not installed. Skipping tests !!!"
285 285 end
286 286
287 287 def test_calendar
288 288 get :calendar, :project_id => 1
289 289 assert_response :success
290 290 assert_template 'calendar'
291 291 assert_not_nil assigns(:calendar)
292 292 end
293 293
294 294 def test_cross_project_calendar
295 295 get :calendar
296 296 assert_response :success
297 297 assert_template 'calendar'
298 298 assert_not_nil assigns(:calendar)
299 299 end
300 300
301 301 def test_changes
302 302 get :changes, :project_id => 1
303 303 assert_response :success
304 304 assert_not_nil assigns(:journals)
305 305 assert_equal 'application/atom+xml', @response.content_type
306 306 end
307 307
308 308 def test_show_routing
309 309 assert_routing(
310 310 {:method => :get, :path => '/issues/64'},
311 311 :controller => 'issues', :action => 'show', :id => '64'
312 312 )
313 313 end
314 314
315 315 def test_show_routing_formatted
316 316 assert_routing(
317 317 {:method => :get, :path => '/issues/2332.pdf'},
318 318 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
319 319 )
320 320 assert_routing(
321 321 {:method => :get, :path => '/issues/23123.atom'},
322 322 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
323 323 )
324 324 end
325 325
326 326 def test_show_by_anonymous
327 327 get :show, :id => 1
328 328 assert_response :success
329 329 assert_template 'show.rhtml'
330 330 assert_not_nil assigns(:issue)
331 331 assert_equal Issue.find(1), assigns(:issue)
332 332
333 333 # anonymous role is allowed to add a note
334 334 assert_tag :tag => 'form',
335 335 :descendant => { :tag => 'fieldset',
336 336 :child => { :tag => 'legend',
337 337 :content => /Notes/ } }
338 338 end
339 339
340 340 def test_show_by_manager
341 341 @request.session[:user_id] = 2
342 342 get :show, :id => 1
343 343 assert_response :success
344 344
345 345 assert_tag :tag => 'form',
346 346 :descendant => { :tag => 'fieldset',
347 347 :child => { :tag => 'legend',
348 348 :content => /Change properties/ } },
349 349 :descendant => { :tag => 'fieldset',
350 350 :child => { :tag => 'legend',
351 351 :content => /Log time/ } },
352 352 :descendant => { :tag => 'fieldset',
353 353 :child => { :tag => 'legend',
354 354 :content => /Notes/ } }
355 355 end
356 356
357 357 def test_show_should_not_disclose_relations_to_invisible_issues
358 358 Setting.cross_project_issue_relations = '1'
359 359 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
360 360 # Relation to a private project issue
361 361 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
362 362
363 363 get :show, :id => 1
364 364 assert_response :success
365 365
366 366 assert_tag :div, :attributes => { :id => 'relations' },
367 367 :descendant => { :tag => 'a', :content => /#2$/ }
368 368 assert_no_tag :div, :attributes => { :id => 'relations' },
369 369 :descendant => { :tag => 'a', :content => /#4$/ }
370 370 end
371 371
372 372 def test_new_routing
373 373 assert_routing(
374 374 {:method => :get, :path => '/projects/1/issues/new'},
375 375 :controller => 'issues', :action => 'new', :project_id => '1'
376 376 )
377 377 assert_recognizes(
378 378 {:controller => 'issues', :action => 'new', :project_id => '1'},
379 379 {:method => :post, :path => '/projects/1/issues'}
380 380 )
381 381 end
382 382
383 383 def test_show_export_to_pdf
384 384 get :show, :id => 3, :format => 'pdf'
385 385 assert_response :success
386 386 assert_equal 'application/pdf', @response.content_type
387 387 assert @response.body.starts_with?('%PDF')
388 388 assert_not_nil assigns(:issue)
389 389 end
390 390
391 391 def test_get_new
392 392 @request.session[:user_id] = 2
393 393 get :new, :project_id => 1, :tracker_id => 1
394 394 assert_response :success
395 395 assert_template 'new'
396 396
397 397 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
398 398 :value => 'Default string' }
399 399 end
400 400
401 401 def test_get_new_without_tracker_id
402 402 @request.session[:user_id] = 2
403 403 get :new, :project_id => 1
404 404 assert_response :success
405 405 assert_template 'new'
406 406
407 407 issue = assigns(:issue)
408 408 assert_not_nil issue
409 409 assert_equal Project.find(1).trackers.first, issue.tracker
410 410 end
411 411
412 412 def test_get_new_with_no_default_status_should_display_an_error
413 413 @request.session[:user_id] = 2
414 414 IssueStatus.delete_all
415 415
416 416 get :new, :project_id => 1
417 417 assert_response 500
418 418 assert_not_nil flash[:error]
419 419 assert_tag :tag => 'div', :attributes => { :class => /error/ },
420 420 :content => /No default issue/
421 421 end
422 422
423 423 def test_get_new_with_no_tracker_should_display_an_error
424 424 @request.session[:user_id] = 2
425 425 Tracker.delete_all
426 426
427 427 get :new, :project_id => 1
428 428 assert_response 500
429 429 assert_not_nil flash[:error]
430 430 assert_tag :tag => 'div', :attributes => { :class => /error/ },
431 431 :content => /No tracker/
432 432 end
433 433
434 434 def test_update_new_form
435 435 @request.session[:user_id] = 2
436 436 xhr :post, :new, :project_id => 1,
437 437 :issue => {:tracker_id => 2,
438 438 :subject => 'This is the test_new issue',
439 439 :description => 'This is the description',
440 440 :priority_id => 5}
441 441 assert_response :success
442 442 assert_template 'new'
443 443 end
444 444
445 445 def test_post_new
446 446 @request.session[:user_id] = 2
447 447 post :new, :project_id => 1,
448 448 :issue => {:tracker_id => 3,
449 449 :subject => 'This is the test_new issue',
450 450 :description => 'This is the description',
451 451 :priority_id => 5,
452 452 :estimated_hours => '',
453 453 :custom_field_values => {'2' => 'Value for field 2'}}
454 454 assert_redirected_to :action => 'show'
455 455
456 456 issue = Issue.find_by_subject('This is the test_new issue')
457 457 assert_not_nil issue
458 458 assert_equal 2, issue.author_id
459 459 assert_equal 3, issue.tracker_id
460 460 assert_nil issue.estimated_hours
461 461 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
462 462 assert_not_nil v
463 463 assert_equal 'Value for field 2', v.value
464 464 end
465 465
466 466 def test_post_new_and_continue
467 467 @request.session[:user_id] = 2
468 468 post :new, :project_id => 1,
469 469 :issue => {:tracker_id => 3,
470 470 :subject => 'This is first issue',
471 471 :priority_id => 5},
472 472 :continue => ''
473 473 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
474 474 end
475 475
476 476 def test_post_new_without_custom_fields_param
477 477 @request.session[:user_id] = 2
478 478 post :new, :project_id => 1,
479 479 :issue => {:tracker_id => 1,
480 480 :subject => 'This is the test_new issue',
481 481 :description => 'This is the description',
482 482 :priority_id => 5}
483 483 assert_redirected_to :action => 'show'
484 484 end
485 485
486 486 def test_post_new_with_required_custom_field_and_without_custom_fields_param
487 487 field = IssueCustomField.find_by_name('Database')
488 488 field.update_attribute(:is_required, true)
489 489
490 490 @request.session[:user_id] = 2
491 491 post :new, :project_id => 1,
492 492 :issue => {:tracker_id => 1,
493 493 :subject => 'This is the test_new issue',
494 494 :description => 'This is the description',
495 495 :priority_id => 5}
496 496 assert_response :success
497 497 assert_template 'new'
498 498 issue = assigns(:issue)
499 499 assert_not_nil issue
500 500 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
501 501 end
502 502
503 503 def test_post_new_with_watchers
504 504 @request.session[:user_id] = 2
505 505 ActionMailer::Base.deliveries.clear
506 506
507 507 assert_difference 'Watcher.count', 2 do
508 508 post :new, :project_id => 1,
509 509 :issue => {:tracker_id => 1,
510 510 :subject => 'This is a new issue with watchers',
511 511 :description => 'This is the description',
512 512 :priority_id => 5,
513 513 :watcher_user_ids => ['2', '3']}
514 514 end
515 515 issue = Issue.find_by_subject('This is a new issue with watchers')
516 516 assert_not_nil issue
517 517 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
518 518
519 519 # Watchers added
520 520 assert_equal [2, 3], issue.watcher_user_ids.sort
521 521 assert issue.watched_by?(User.find(3))
522 522 # Watchers notified
523 523 mail = ActionMailer::Base.deliveries.last
524 524 assert_kind_of TMail::Mail, mail
525 525 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
526 526 end
527 527
528 528 def test_post_new_should_send_a_notification
529 529 ActionMailer::Base.deliveries.clear
530 530 @request.session[:user_id] = 2
531 531 post :new, :project_id => 1,
532 532 :issue => {:tracker_id => 3,
533 533 :subject => 'This is the test_new issue',
534 534 :description => 'This is the description',
535 535 :priority_id => 5,
536 536 :estimated_hours => '',
537 537 :custom_field_values => {'2' => 'Value for field 2'}}
538 538 assert_redirected_to :action => 'show'
539 539
540 540 assert_equal 1, ActionMailer::Base.deliveries.size
541 541 end
542 542
543 543 def test_post_should_preserve_fields_values_on_validation_failure
544 544 @request.session[:user_id] = 2
545 545 post :new, :project_id => 1,
546 546 :issue => {:tracker_id => 1,
547 547 # empty subject
548 548 :subject => '',
549 549 :description => 'This is a description',
550 550 :priority_id => 6,
551 551 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
552 552 assert_response :success
553 553 assert_template 'new'
554 554
555 555 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
556 556 :content => 'This is a description'
557 557 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
558 558 :child => { :tag => 'option', :attributes => { :selected => 'selected',
559 559 :value => '6' },
560 560 :content => 'High' }
561 561 # Custom fields
562 562 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
563 563 :child => { :tag => 'option', :attributes => { :selected => 'selected',
564 564 :value => 'Oracle' },
565 565 :content => 'Oracle' }
566 566 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
567 567 :value => 'Value for field 2'}
568 568 end
569 569
570 570 def test_copy_routing
571 571 assert_routing(
572 572 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
573 573 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
574 574 )
575 575 end
576 576
577 577 def test_copy_issue
578 578 @request.session[:user_id] = 2
579 579 get :new, :project_id => 1, :copy_from => 1
580 580 assert_template 'new'
581 581 assert_not_nil assigns(:issue)
582 582 orig = Issue.find(1)
583 583 assert_equal orig.subject, assigns(:issue).subject
584 584 end
585 585
586 586 def test_edit_routing
587 587 assert_routing(
588 588 {:method => :get, :path => '/issues/1/edit'},
589 589 :controller => 'issues', :action => 'edit', :id => '1'
590 590 )
591 591 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
592 592 {:controller => 'issues', :action => 'edit', :id => '1'},
593 593 {:method => :post, :path => '/issues/1/edit'}
594 594 )
595 595 end
596 596
597 597 def test_get_edit
598 598 @request.session[:user_id] = 2
599 599 get :edit, :id => 1
600 600 assert_response :success
601 601 assert_template 'edit'
602 602 assert_not_nil assigns(:issue)
603 603 assert_equal Issue.find(1), assigns(:issue)
604 604 end
605 605
606 606 def test_get_edit_with_params
607 607 @request.session[:user_id] = 2
608 608 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
609 609 assert_response :success
610 610 assert_template 'edit'
611 611
612 612 issue = assigns(:issue)
613 613 assert_not_nil issue
614 614
615 615 assert_equal 5, issue.status_id
616 616 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
617 617 :child => { :tag => 'option',
618 618 :content => 'Closed',
619 619 :attributes => { :selected => 'selected' } }
620 620
621 621 assert_equal 7, issue.priority_id
622 622 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
623 623 :child => { :tag => 'option',
624 624 :content => 'Urgent',
625 625 :attributes => { :selected => 'selected' } }
626 626 end
627 627
628 628 def test_reply_routing
629 629 assert_routing(
630 630 {:method => :post, :path => '/issues/1/quoted'},
631 631 :controller => 'issues', :action => 'reply', :id => '1'
632 632 )
633 633 end
634 634
635 635 def test_reply_to_issue
636 636 @request.session[:user_id] = 2
637 637 get :reply, :id => 1
638 638 assert_response :success
639 639 assert_select_rjs :show, "update"
640 640 end
641 641
642 642 def test_reply_to_note
643 643 @request.session[:user_id] = 2
644 644 get :reply, :id => 1, :journal_id => 2
645 645 assert_response :success
646 646 assert_select_rjs :show, "update"
647 647 end
648 648
649 649 def test_post_edit_without_custom_fields_param
650 650 @request.session[:user_id] = 2
651 651 ActionMailer::Base.deliveries.clear
652 652
653 653 issue = Issue.find(1)
654 654 assert_equal '125', issue.custom_value_for(2).value
655 655 old_subject = issue.subject
656 656 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
657 657
658 658 assert_difference('Journal.count') do
659 659 assert_difference('JournalDetail.count', 2) do
660 660 post :edit, :id => 1, :issue => {:subject => new_subject,
661 661 :priority_id => '6',
662 662 :category_id => '1' # no change
663 663 }
664 664 end
665 665 end
666 666 assert_redirected_to :action => 'show', :id => '1'
667 667 issue.reload
668 668 assert_equal new_subject, issue.subject
669 669 # Make sure custom fields were not cleared
670 670 assert_equal '125', issue.custom_value_for(2).value
671 671
672 672 mail = ActionMailer::Base.deliveries.last
673 673 assert_kind_of TMail::Mail, mail
674 674 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
675 675 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
676 676 end
677 677
678 678 def test_post_edit_with_custom_field_change
679 679 @request.session[:user_id] = 2
680 680 issue = Issue.find(1)
681 681 assert_equal '125', issue.custom_value_for(2).value
682 682
683 683 assert_difference('Journal.count') do
684 684 assert_difference('JournalDetail.count', 3) do
685 685 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
686 686 :priority_id => '6',
687 687 :category_id => '1', # no change
688 688 :custom_field_values => { '2' => 'New custom value' }
689 689 }
690 690 end
691 691 end
692 692 assert_redirected_to :action => 'show', :id => '1'
693 693 issue.reload
694 694 assert_equal 'New custom value', issue.custom_value_for(2).value
695 695
696 696 mail = ActionMailer::Base.deliveries.last
697 697 assert_kind_of TMail::Mail, mail
698 698 assert mail.body.include?("Searchable field changed from 125 to New custom value")
699 699 end
700 700
701 701 def test_post_edit_with_status_and_assignee_change
702 702 issue = Issue.find(1)
703 703 assert_equal 1, issue.status_id
704 704 @request.session[:user_id] = 2
705 705 assert_difference('TimeEntry.count', 0) do
706 706 post :edit,
707 707 :id => 1,
708 708 :issue => { :status_id => 2, :assigned_to_id => 3 },
709 709 :notes => 'Assigned to dlopper',
710 710 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.activities.first }
711 711 end
712 712 assert_redirected_to :action => 'show', :id => '1'
713 713 issue.reload
714 714 assert_equal 2, issue.status_id
715 715 j = issue.journals.find(:first, :order => 'id DESC')
716 716 assert_equal 'Assigned to dlopper', j.notes
717 717 assert_equal 2, j.details.size
718 718
719 719 mail = ActionMailer::Base.deliveries.last
720 720 assert mail.body.include?("Status changed from New to Assigned")
721 721 # subject should contain the new status
722 722 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
723 723 end
724 724
725 725 def test_post_edit_with_note_only
726 726 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
727 727 # anonymous user
728 728 post :edit,
729 729 :id => 1,
730 730 :notes => notes
731 731 assert_redirected_to :action => 'show', :id => '1'
732 732 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
733 733 assert_equal notes, j.notes
734 734 assert_equal 0, j.details.size
735 735 assert_equal User.anonymous, j.user
736 736
737 737 mail = ActionMailer::Base.deliveries.last
738 738 assert mail.body.include?(notes)
739 739 end
740 740
741 741 def test_post_edit_with_note_and_spent_time
742 742 @request.session[:user_id] = 2
743 743 spent_hours_before = Issue.find(1).spent_hours
744 744 assert_difference('TimeEntry.count') do
745 745 post :edit,
746 746 :id => 1,
747 747 :notes => '2.5 hours added',
748 748 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.activities.first }
749 749 end
750 750 assert_redirected_to :action => 'show', :id => '1'
751 751
752 752 issue = Issue.find(1)
753 753
754 754 j = issue.journals.find(:first, :order => 'id DESC')
755 755 assert_equal '2.5 hours added', j.notes
756 756 assert_equal 0, j.details.size
757 757
758 758 t = issue.time_entries.find(:first, :order => 'id DESC')
759 759 assert_not_nil t
760 760 assert_equal 2.5, t.hours
761 761 assert_equal spent_hours_before + 2.5, issue.spent_hours
762 762 end
763 763
764 764 def test_post_edit_with_attachment_only
765 765 set_tmp_attachments_directory
766 766
767 767 # Delete all fixtured journals, a race condition can occur causing the wrong
768 768 # journal to get fetched in the next find.
769 769 Journal.delete_all
770 770
771 771 # anonymous user
772 772 post :edit,
773 773 :id => 1,
774 774 :notes => '',
775 775 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
776 776 assert_redirected_to :action => 'show', :id => '1'
777 777 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
778 778 assert j.notes.blank?
779 779 assert_equal 1, j.details.size
780 780 assert_equal 'testfile.txt', j.details.first.value
781 781 assert_equal User.anonymous, j.user
782 782
783 783 mail = ActionMailer::Base.deliveries.last
784 784 assert mail.body.include?('testfile.txt')
785 785 end
786 786
787 787 def test_post_edit_with_no_change
788 788 issue = Issue.find(1)
789 789 issue.journals.clear
790 790 ActionMailer::Base.deliveries.clear
791 791
792 792 post :edit,
793 793 :id => 1,
794 794 :notes => ''
795 795 assert_redirected_to :action => 'show', :id => '1'
796 796
797 797 issue.reload
798 798 assert issue.journals.empty?
799 799 # No email should be sent
800 800 assert ActionMailer::Base.deliveries.empty?
801 801 end
802 802
803 803 def test_post_edit_should_send_a_notification
804 804 @request.session[:user_id] = 2
805 805 ActionMailer::Base.deliveries.clear
806 806 issue = Issue.find(1)
807 807 old_subject = issue.subject
808 808 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
809 809
810 810 post :edit, :id => 1, :issue => {:subject => new_subject,
811 811 :priority_id => '6',
812 812 :category_id => '1' # no change
813 813 }
814 814 assert_equal 1, ActionMailer::Base.deliveries.size
815 815 end
816 816
817 817 def test_post_edit_with_invalid_spent_time
818 818 @request.session[:user_id] = 2
819 819 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
820 820
821 821 assert_no_difference('Journal.count') do
822 822 post :edit,
823 823 :id => 1,
824 824 :notes => notes,
825 825 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
826 826 end
827 827 assert_response :success
828 828 assert_template 'edit'
829 829
830 830 assert_tag :textarea, :attributes => { :name => 'notes' },
831 831 :content => notes
832 832 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
833 833 end
834 834
835 835 def test_get_bulk_edit
836 836 @request.session[:user_id] = 2
837 837 get :bulk_edit, :ids => [1, 2]
838 838 assert_response :success
839 839 assert_template 'bulk_edit'
840 840 end
841 841
842 842 def test_bulk_edit
843 843 @request.session[:user_id] = 2
844 844 # update issues priority
845 845 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
846 846 :assigned_to_id => '',
847 847 :custom_field_values => {'2' => ''},
848 848 :notes => 'Bulk editing'
849 849 assert_response 302
850 850 # check that the issues were updated
851 851 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
852 852
853 853 issue = Issue.find(1)
854 854 journal = issue.journals.find(:first, :order => 'created_on DESC')
855 855 assert_equal '125', issue.custom_value_for(2).value
856 856 assert_equal 'Bulk editing', journal.notes
857 857 assert_equal 1, journal.details.size
858 858 end
859 859
860 860 def test_bullk_edit_should_send_a_notification
861 861 @request.session[:user_id] = 2
862 862 ActionMailer::Base.deliveries.clear
863 863 post(:bulk_edit,
864 864 {
865 865 :ids => [1, 2],
866 866 :priority_id => 7,
867 867 :assigned_to_id => '',
868 868 :custom_field_values => {'2' => ''},
869 869 :notes => 'Bulk editing'
870 870 })
871 871
872 872 assert_response 302
873 873 assert_equal 2, ActionMailer::Base.deliveries.size
874 874 end
875 875
876 def test_bulk_edit_status
877 @request.session[:user_id] = 2
878 # update issues priority
879 post :bulk_edit, :ids => [1, 2], :priority_id => '',
880 :assigned_to_id => '',
881 :status_id => '5',
882 :notes => 'Bulk editing status'
883 assert_response 302
884 issue = Issue.find(1)
885 assert issue.closed?
886 end
887
876 888 def test_bulk_edit_custom_field
877 889 @request.session[:user_id] = 2
878 890 # update issues priority
879 891 post :bulk_edit, :ids => [1, 2], :priority_id => '',
880 892 :assigned_to_id => '',
881 893 :custom_field_values => {'2' => '777'},
882 894 :notes => 'Bulk editing custom field'
883 895 assert_response 302
884 896
885 897 issue = Issue.find(1)
886 898 journal = issue.journals.find(:first, :order => 'created_on DESC')
887 899 assert_equal '777', issue.custom_value_for(2).value
888 900 assert_equal 1, journal.details.size
889 901 assert_equal '125', journal.details.first.old_value
890 902 assert_equal '777', journal.details.first.value
891 903 end
892 904
893 905 def test_bulk_unassign
894 906 assert_not_nil Issue.find(2).assigned_to
895 907 @request.session[:user_id] = 2
896 908 # unassign issues
897 909 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
898 910 assert_response 302
899 911 # check that the issues were updated
900 912 assert_nil Issue.find(2).assigned_to
901 913 end
902 914
903 915 def test_move_routing
904 916 assert_routing(
905 917 {:method => :get, :path => '/issues/1/move'},
906 918 :controller => 'issues', :action => 'move', :id => '1'
907 919 )
908 920 assert_recognizes(
909 921 {:controller => 'issues', :action => 'move', :id => '1'},
910 922 {:method => :post, :path => '/issues/1/move'}
911 923 )
912 924 end
913 925
914 926 def test_move_one_issue_to_another_project
915 927 @request.session[:user_id] = 2
916 928 post :move, :id => 1, :new_project_id => 2
917 929 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
918 930 assert_equal 2, Issue.find(1).project_id
919 931 end
920 932
921 933 def test_bulk_move_to_another_project
922 934 @request.session[:user_id] = 2
923 935 post :move, :ids => [1, 2], :new_project_id => 2
924 936 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
925 937 # Issues moved to project 2
926 938 assert_equal 2, Issue.find(1).project_id
927 939 assert_equal 2, Issue.find(2).project_id
928 940 # No tracker change
929 941 assert_equal 1, Issue.find(1).tracker_id
930 942 assert_equal 2, Issue.find(2).tracker_id
931 943 end
932 944
933 945 def test_bulk_move_to_another_tracker
934 946 @request.session[:user_id] = 2
935 947 post :move, :ids => [1, 2], :new_tracker_id => 2
936 948 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
937 949 assert_equal 2, Issue.find(1).tracker_id
938 950 assert_equal 2, Issue.find(2).tracker_id
939 951 end
940 952
941 953 def test_bulk_copy_to_another_project
942 954 @request.session[:user_id] = 2
943 955 assert_difference 'Issue.count', 2 do
944 956 assert_no_difference 'Project.find(1).issues.count' do
945 957 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
946 958 end
947 959 end
948 960 assert_redirected_to 'projects/ecookbook/issues'
949 961 end
950 962
951 963 def test_context_menu_one_issue
952 964 @request.session[:user_id] = 2
953 965 get :context_menu, :ids => [1]
954 966 assert_response :success
955 967 assert_template 'context_menu'
956 968 assert_tag :tag => 'a', :content => 'Edit',
957 969 :attributes => { :href => '/issues/1/edit',
958 970 :class => 'icon-edit' }
959 971 assert_tag :tag => 'a', :content => 'Closed',
960 972 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
961 973 :class => '' }
962 974 assert_tag :tag => 'a', :content => 'Immediate',
963 975 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
964 976 :class => '' }
965 977 assert_tag :tag => 'a', :content => 'Dave Lopper',
966 978 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
967 979 :class => '' }
968 980 assert_tag :tag => 'a', :content => 'Copy',
969 981 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
970 982 :class => 'icon-copy' }
971 983 assert_tag :tag => 'a', :content => 'Move',
972 984 :attributes => { :href => '/issues/move?ids%5B%5D=1',
973 985 :class => 'icon-move' }
974 986 assert_tag :tag => 'a', :content => 'Delete',
975 987 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
976 988 :class => 'icon-del' }
977 989 end
978 990
979 991 def test_context_menu_one_issue_by_anonymous
980 992 get :context_menu, :ids => [1]
981 993 assert_response :success
982 994 assert_template 'context_menu'
983 995 assert_tag :tag => 'a', :content => 'Delete',
984 996 :attributes => { :href => '#',
985 997 :class => 'icon-del disabled' }
986 998 end
987 999
988 1000 def test_context_menu_multiple_issues_of_same_project
989 1001 @request.session[:user_id] = 2
990 1002 get :context_menu, :ids => [1, 2]
991 1003 assert_response :success
992 1004 assert_template 'context_menu'
993 1005 assert_tag :tag => 'a', :content => 'Edit',
994 1006 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
995 1007 :class => 'icon-edit' }
996 1008 assert_tag :tag => 'a', :content => 'Immediate',
997 1009 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
998 1010 :class => '' }
999 1011 assert_tag :tag => 'a', :content => 'Dave Lopper',
1000 1012 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1001 1013 :class => '' }
1002 1014 assert_tag :tag => 'a', :content => 'Move',
1003 1015 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1004 1016 :class => 'icon-move' }
1005 1017 assert_tag :tag => 'a', :content => 'Delete',
1006 1018 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1007 1019 :class => 'icon-del' }
1008 1020 end
1009 1021
1010 1022 def test_context_menu_multiple_issues_of_different_project
1011 1023 @request.session[:user_id] = 2
1012 1024 get :context_menu, :ids => [1, 2, 4]
1013 1025 assert_response :success
1014 1026 assert_template 'context_menu'
1015 1027 assert_tag :tag => 'a', :content => 'Delete',
1016 1028 :attributes => { :href => '#',
1017 1029 :class => 'icon-del disabled' }
1018 1030 end
1019 1031
1020 1032 def test_destroy_routing
1021 1033 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1022 1034 {:controller => 'issues', :action => 'destroy', :id => '1'},
1023 1035 {:method => :post, :path => '/issues/1/destroy'}
1024 1036 )
1025 1037 end
1026 1038
1027 1039 def test_destroy_issue_with_no_time_entries
1028 1040 assert_nil TimeEntry.find_by_issue_id(2)
1029 1041 @request.session[:user_id] = 2
1030 1042 post :destroy, :id => 2
1031 1043 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1032 1044 assert_nil Issue.find_by_id(2)
1033 1045 end
1034 1046
1035 1047 def test_destroy_issues_with_time_entries
1036 1048 @request.session[:user_id] = 2
1037 1049 post :destroy, :ids => [1, 3]
1038 1050 assert_response :success
1039 1051 assert_template 'destroy'
1040 1052 assert_not_nil assigns(:hours)
1041 1053 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1042 1054 end
1043 1055
1044 1056 def test_destroy_issues_and_destroy_time_entries
1045 1057 @request.session[:user_id] = 2
1046 1058 post :destroy, :ids => [1, 3], :todo => 'destroy'
1047 1059 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1048 1060 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1049 1061 assert_nil TimeEntry.find_by_id([1, 2])
1050 1062 end
1051 1063
1052 1064 def test_destroy_issues_and_assign_time_entries_to_project
1053 1065 @request.session[:user_id] = 2
1054 1066 post :destroy, :ids => [1, 3], :todo => 'nullify'
1055 1067 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1056 1068 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1057 1069 assert_nil TimeEntry.find(1).issue_id
1058 1070 assert_nil TimeEntry.find(2).issue_id
1059 1071 end
1060 1072
1061 1073 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1062 1074 @request.session[:user_id] = 2
1063 1075 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1064 1076 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1065 1077 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1066 1078 assert_equal 2, TimeEntry.find(1).issue_id
1067 1079 assert_equal 2, TimeEntry.find(2).issue_id
1068 1080 end
1069 1081 end
General Comments 0
You need to be logged in to leave comments. Login now