##// END OF EJS Templates
Merged r3333 from trunk....
Jean-Philippe Lang -
r3220:3d7cb0f40e09
parent child
Show More
@@ -1,541 +1,542
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, :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 include QueriesHelper
43 44 helper :sort
44 45 include SortHelper
45 46 include IssuesHelper
46 47 helper :timelog
47 48 include Redmine::Export::PDF
48 49
49 50 verify :method => :post,
50 51 :only => :destroy,
51 52 :render => { :nothing => true, :status => :method_not_allowed }
52 53
53 54 def index
54 55 retrieve_query
55 56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 57 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57 58
58 59 if @query.valid?
59 60 limit = per_page_option
60 61 respond_to do |format|
61 62 format.html { }
62 63 format.atom { limit = Setting.feeds_limit.to_i }
63 64 format.csv { limit = Setting.issues_export_limit.to_i }
64 65 format.pdf { limit = Setting.issues_export_limit.to_i }
65 66 end
66 67
67 68 @issue_count = @query.issue_count
68 69 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
69 70 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 71 :order => sort_clause,
71 72 :offset => @issue_pages.current.offset,
72 73 :limit => limit)
73 74 @issue_count_by_group = @query.issue_count_by_group
74 75
75 76 respond_to do |format|
76 77 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
77 78 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
78 79 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
79 80 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
80 81 end
81 82 else
82 83 # Send html if the query is not valid
83 84 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
84 85 end
85 86 rescue ActiveRecord::RecordNotFound
86 87 render_404
87 88 end
88 89
89 90 def changes
90 91 retrieve_query
91 92 sort_init 'id', 'desc'
92 93 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
93 94
94 95 if @query.valid?
95 96 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
96 97 :limit => 25)
97 98 end
98 99 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
99 100 render :layout => false, :content_type => 'application/atom+xml'
100 101 rescue ActiveRecord::RecordNotFound
101 102 render_404
102 103 end
103 104
104 105 def show
105 106 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
106 107 @journals.each_with_index {|j,i| j.indice = i+1}
107 108 @journals.reverse! if User.current.wants_comments_in_reverse_order?
108 109 @changesets = @issue.changesets
109 110 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
110 111 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 112 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 113 @priorities = IssuePriority.all
113 114 @time_entry = TimeEntry.new
114 115 respond_to do |format|
115 116 format.html { render :template => 'issues/show.rhtml' }
116 117 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
117 118 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
118 119 end
119 120 end
120 121
121 122 # Add a new issue
122 123 # The new issue will be created from an existing one if copy_from parameter is given
123 124 def new
124 125 @issue = Issue.new
125 126 @issue.copy_from(params[:copy_from]) if params[:copy_from]
126 127 @issue.project = @project
127 128 # Tracker must be set before custom field values
128 129 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
129 130 if @issue.tracker.nil?
130 131 render_error l(:error_no_tracker_in_project)
131 132 return
132 133 end
133 134 if params[:issue].is_a?(Hash)
134 135 @issue.attributes = params[:issue]
135 136 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
136 137 end
137 138 @issue.author = User.current
138 139
139 140 default_status = IssueStatus.default
140 141 unless default_status
141 142 render_error l(:error_no_default_issue_status)
142 143 return
143 144 end
144 145 @issue.status = default_status
145 146 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
146 147
147 148 if request.get? || request.xhr?
148 149 @issue.start_date ||= Date.today
149 150 else
150 151 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
151 152 # Check that the user is allowed to apply the requested status
152 153 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
153 154 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
154 155 if @issue.save
155 156 attach_files(@issue, params[:attachments])
156 157 flash[:notice] = l(:notice_successful_create)
157 158 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
158 159 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
159 160 { :action => 'show', :id => @issue })
160 161 return
161 162 end
162 163 end
163 164 @priorities = IssuePriority.all
164 165 render :layout => !request.xhr?
165 166 end
166 167
167 168 # Attributes that can be updated on workflow transition (without :edit permission)
168 169 # TODO: make it configurable (at least per role)
169 170 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
170 171
171 172 def edit
172 173 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
173 174 @priorities = IssuePriority.all
174 175 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
175 176 @time_entry = TimeEntry.new
176 177
177 178 @notes = params[:notes]
178 179 journal = @issue.init_journal(User.current, @notes)
179 180 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
180 181 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
181 182 attrs = params[:issue].dup
182 183 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
183 184 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
184 185 @issue.attributes = attrs
185 186 end
186 187
187 188 if request.post?
188 189 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
189 190 @time_entry.attributes = params[:time_entry]
190 191 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
191 192 attachments = attach_files(@issue, params[:attachments])
192 193 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
193 194 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
194 195 if @issue.save
195 196 # Log spend time
196 197 if User.current.allowed_to?(:log_time, @project)
197 198 @time_entry.save
198 199 end
199 200 if !journal.new_record?
200 201 # Only send notification if something was actually changed
201 202 flash[:notice] = l(:notice_successful_update)
202 203 end
203 204 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
204 205 redirect_back_or_default({:action => 'show', :id => @issue})
205 206 end
206 207 end
207 208 end
208 209 rescue ActiveRecord::StaleObjectError
209 210 # Optimistic locking exception
210 211 flash.now[:error] = l(:notice_locking_conflict)
211 212 # Remove the previously added attachments if issue was not updated
212 213 attachments.each(&:destroy)
213 214 end
214 215
215 216 def reply
216 217 journal = Journal.find(params[:journal_id]) if params[:journal_id]
217 218 if journal
218 219 user = journal.user
219 220 text = journal.notes
220 221 else
221 222 user = @issue.author
222 223 text = @issue.description
223 224 end
224 225 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
225 226 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
226 227 render(:update) { |page|
227 228 page.<< "$('notes').value = \"#{content}\";"
228 229 page.show 'update'
229 230 page << "Form.Element.focus('notes');"
230 231 page << "Element.scrollTo('update');"
231 232 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
232 233 }
233 234 end
234 235
235 236 # Bulk edit a set of issues
236 237 def bulk_edit
237 238 if request.post?
238 239 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
239 240 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
240 241 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
241 242 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
242 243 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
243 244 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
244 245 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
245 246
246 247 unsaved_issue_ids = []
247 248 @issues.each do |issue|
248 249 journal = issue.init_journal(User.current, params[:notes])
249 250 issue.tracker = tracker if tracker
250 251 issue.priority = priority if priority
251 252 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
252 253 issue.category = category if category || params[:category_id] == 'none'
253 254 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
254 255 issue.start_date = params[:start_date] unless params[:start_date].blank?
255 256 issue.due_date = params[:due_date] unless params[:due_date].blank?
256 257 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
257 258 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
258 259 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
259 260 # Don't save any change to the issue if the user is not authorized to apply the requested status
260 261 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
261 262 # Keep unsaved issue ids to display them in flash error
262 263 unsaved_issue_ids << issue.id
263 264 end
264 265 end
265 266 if unsaved_issue_ids.empty?
266 267 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
267 268 else
268 269 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
269 270 :total => @issues.size,
270 271 :ids => '#' + unsaved_issue_ids.join(', #'))
271 272 end
272 273 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
273 274 return
274 275 end
275 276 @available_statuses = Workflow.available_statuses(@project)
276 277 @custom_fields = @project.all_issue_custom_fields
277 278 end
278 279
279 280 def move
280 281 @copy = params[:copy_options] && params[:copy_options][:copy]
281 282 @allowed_projects = []
282 283 # find projects to which the user is allowed to move the issue
283 284 if User.current.admin?
284 285 # admin is allowed to move issues to any active (visible) project
285 286 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
286 287 else
287 288 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
288 289 end
289 290 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
290 291 @target_project ||= @project
291 292 @trackers = @target_project.trackers
292 293 @available_statuses = Workflow.available_statuses(@project)
293 294 if request.post?
294 295 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
295 296 unsaved_issue_ids = []
296 297 moved_issues = []
297 298 @issues.each do |issue|
298 299 changed_attributes = {}
299 300 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
300 301 unless params[valid_attribute].blank?
301 302 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
302 303 end
303 304 end
304 305 issue.init_journal(User.current)
305 306 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
306 307 moved_issues << r
307 308 else
308 309 unsaved_issue_ids << issue.id
309 310 end
310 311 end
311 312 if unsaved_issue_ids.empty?
312 313 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
313 314 else
314 315 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
315 316 :total => @issues.size,
316 317 :ids => '#' + unsaved_issue_ids.join(', #'))
317 318 end
318 319 if params[:follow]
319 320 if @issues.size == 1 && moved_issues.size == 1
320 321 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
321 322 else
322 323 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
323 324 end
324 325 else
325 326 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
326 327 end
327 328 return
328 329 end
329 330 render :layout => false if request.xhr?
330 331 end
331 332
332 333 def destroy
333 334 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
334 335 if @hours > 0
335 336 case params[:todo]
336 337 when 'destroy'
337 338 # nothing to do
338 339 when 'nullify'
339 340 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
340 341 when 'reassign'
341 342 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
342 343 if reassign_to.nil?
343 344 flash.now[:error] = l(:error_issue_not_found_in_project)
344 345 return
345 346 else
346 347 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
347 348 end
348 349 else
349 350 # display the destroy form
350 351 return
351 352 end
352 353 end
353 354 @issues.each(&:destroy)
354 355 redirect_to :action => 'index', :project_id => @project
355 356 end
356 357
357 358 def gantt
358 359 @gantt = Redmine::Helpers::Gantt.new(params)
359 360 retrieve_query
360 361 if @query.valid?
361 362 events = []
362 363 # Issues that have start and due dates
363 364 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
364 365 :order => "start_date, due_date",
365 366 :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]
366 367 )
367 368 # Issues that don't have a due date but that are assigned to a version with a date
368 369 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
369 370 :order => "start_date, effective_date",
370 371 :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]
371 372 )
372 373 # Versions
373 374 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
374 375
375 376 @gantt.events = events
376 377 end
377 378
378 379 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
379 380
380 381 respond_to do |format|
381 382 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
382 383 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
383 384 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
384 385 end
385 386 end
386 387
387 388 def calendar
388 389 if params[:year] and params[:year].to_i > 1900
389 390 @year = params[:year].to_i
390 391 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
391 392 @month = params[:month].to_i
392 393 end
393 394 end
394 395 @year ||= Date.today.year
395 396 @month ||= Date.today.month
396 397
397 398 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
398 399 retrieve_query
399 400 if @query.valid?
400 401 events = []
401 402 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
402 403 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
403 404 )
404 405 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
405 406
406 407 @calendar.events = events
407 408 end
408 409
409 410 render :layout => false if request.xhr?
410 411 end
411 412
412 413 def context_menu
413 414 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
414 415 if (@issues.size == 1)
415 416 @issue = @issues.first
416 417 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
417 418 end
418 419 projects = @issues.collect(&:project).compact.uniq
419 420 @project = projects.first if projects.size == 1
420 421
421 422 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
422 423 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
423 424 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
424 425 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
425 426 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
426 427 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
427 428 }
428 429 if @project
429 430 @assignables = @project.assignable_users
430 431 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
431 432 @trackers = @project.trackers
432 433 end
433 434
434 435 @priorities = IssuePriority.all.reverse
435 436 @statuses = IssueStatus.find(:all, :order => 'position')
436 437 @back = params[:back_url] || request.env['HTTP_REFERER']
437 438
438 439 render :layout => false
439 440 end
440 441
441 442 def update_form
442 443 if params[:id].blank?
443 444 @issue = Issue.new
444 445 @issue.project = @project
445 446 else
446 447 @issue = @project.issues.visible.find(params[:id])
447 448 end
448 449 @issue.attributes = params[:issue]
449 450 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
450 451 @priorities = IssuePriority.all
451 452
452 453 render :partial => 'attributes'
453 454 end
454 455
455 456 def preview
456 457 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
457 458 @attachements = @issue.attachments if @issue
458 459 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
459 460 render :partial => 'common/preview'
460 461 end
461 462
462 463 private
463 464 def find_issue
464 465 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
465 466 @project = @issue.project
466 467 rescue ActiveRecord::RecordNotFound
467 468 render_404
468 469 end
469 470
470 471 # Filter for bulk operations
471 472 def find_issues
472 473 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
473 474 raise ActiveRecord::RecordNotFound if @issues.empty?
474 475 projects = @issues.collect(&:project).compact.uniq
475 476 if projects.size == 1
476 477 @project = projects.first
477 478 else
478 479 # TODO: let users bulk edit/move/destroy issues from different projects
479 480 render_error 'Can not bulk edit/move/destroy issues from different projects'
480 481 return false
481 482 end
482 483 rescue ActiveRecord::RecordNotFound
483 484 render_404
484 485 end
485 486
486 487 def find_project
487 488 @project = Project.find(params[:project_id])
488 489 rescue ActiveRecord::RecordNotFound
489 490 render_404
490 491 end
491 492
492 493 def find_optional_project
493 494 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
494 495 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
495 496 allowed ? true : deny_access
496 497 rescue ActiveRecord::RecordNotFound
497 498 render_404
498 499 end
499 500
500 501 # Retrieve query from session or build a new query
501 502 def retrieve_query
502 503 if !params[:query_id].blank?
503 504 cond = "project_id IS NULL"
504 505 cond << " OR project_id = #{@project.id}" if @project
505 506 @query = Query.find(params[:query_id], :conditions => cond)
506 507 @query.project = @project
507 508 session[:query] = {:id => @query.id, :project_id => @query.project_id}
508 509 sort_clear
509 510 else
510 511 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
511 512 # Give it a name, required to be valid
512 513 @query = Query.new(:name => "_")
513 514 @query.project = @project
514 515 if params[:fields] and params[:fields].is_a? Array
515 516 params[:fields].each do |field|
516 517 @query.add_filter(field, params[:operators][field], params[:values][field])
517 518 end
518 519 else
519 520 @query.available_filters.keys.each do |field|
520 521 @query.add_short_filter(field, params[field]) if params[field]
521 522 end
522 523 end
523 524 @query.group_by = params[:group_by]
524 525 @query.column_names = params[:query] && params[:query][:column_names]
525 526 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
526 527 else
527 528 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
528 529 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
529 530 @query.project = @project
530 531 end
531 532 end
532 533 end
533 534
534 535 # Rescues an invalid query statement. Just in case...
535 536 def query_statement_invalid(exception)
536 537 logger.error "Query::StatementInvalid: #{exception.message}" if logger
537 538 session.delete(:query)
538 539 sort_clear
539 540 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
540 541 end
541 542 end
@@ -1,487 +1,489
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2009 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'iconv'
21 21 require 'rfpdf/fpdf'
22 22 require 'rfpdf/chinese'
23 23
24 24 module Redmine
25 25 module Export
26 26 module PDF
27 27 include ActionView::Helpers::TextHelper
28 28 include ActionView::Helpers::NumberHelper
29 29
30 30 class IFPDF < FPDF
31 31 include Redmine::I18n
32 32 attr_accessor :footer_date
33 33
34 34 def initialize(lang)
35 35 super()
36 36 set_language_if_valid lang
37 37 case current_language.to_s.downcase
38 38 when 'ja'
39 39 extend(PDF_Japanese)
40 40 AddSJISFont()
41 41 @font_for_content = 'SJIS'
42 42 @font_for_footer = 'SJIS'
43 43 when 'zh'
44 44 extend(PDF_Chinese)
45 45 AddGBFont()
46 46 @font_for_content = 'GB'
47 47 @font_for_footer = 'GB'
48 48 when 'zh-tw'
49 49 extend(PDF_Chinese)
50 50 AddBig5Font()
51 51 @font_for_content = 'Big5'
52 52 @font_for_footer = 'Big5'
53 53 else
54 54 @font_for_content = 'Arial'
55 55 @font_for_footer = 'Helvetica'
56 56 end
57 57 SetCreator(Redmine::Info.app_name)
58 58 SetFont(@font_for_content)
59 59 end
60 60
61 61 def SetFontStyle(style, size)
62 62 SetFont(@font_for_content, style, size)
63 63 end
64 64
65 65 def SetTitle(txt)
66 66 txt = begin
67 67 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
68 68 hextxt = "<FEFF" # FEFF is BOM
69 69 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
70 70 hextxt << ">"
71 71 rescue
72 72 txt
73 73 end || ''
74 74 super(txt)
75 75 end
76 76
77 77 def textstring(s)
78 78 # Format a text string
79 79 if s =~ /^</ # This means the string is hex-dumped.
80 80 return s
81 81 else
82 82 return '('+escape(s)+')'
83 83 end
84 84 end
85 85
86 86 def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
87 87 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
88 88 # these quotation marks are not correctly rendered in the pdf
89 89 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
90 90 txt = begin
91 91 # 0x5c char handling
92 92 txtar = txt.split('\\')
93 93 txtar << '' if txt[-1] == ?\\
94 94 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
95 95 rescue
96 96 txt
97 97 end || ''
98 98 super w,h,txt,border,ln,align,fill,link
99 99 end
100 100
101 101 def Footer
102 102 SetFont(@font_for_footer, 'I', 8)
103 103 SetY(-15)
104 104 SetX(15)
105 105 Cell(0, 5, @footer_date, 0, 0, 'L')
106 106 SetY(-15)
107 107 SetX(-30)
108 108 Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
109 109 end
110 110 end
111 111
112 112 # Returns a PDF string of a list of issues
113 113 def issues_to_pdf(issues, project, query)
114 114 pdf = IFPDF.new(current_language)
115 115 title = query.new_record? ? l(:label_issue_plural) : query.name
116 116 title = "#{project} - #{title}" if project
117 117 pdf.SetTitle(title)
118 118 pdf.AliasNbPages
119 119 pdf.footer_date = format_date(Date.today)
120 120 pdf.AddPage("L")
121 121
122 122 row_height = 6
123 123 col_width = []
124 124 unless query.columns.empty?
125 125 col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
126 126 ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
127 127 col_width = col_width.collect {|w| w * ratio}
128 128 end
129 129
130 130 # title
131 131 pdf.SetFontStyle('B',11)
132 132 pdf.Cell(190,10, title)
133 133 pdf.Ln
134 134
135 135 # headers
136 136 pdf.SetFontStyle('B',8)
137 137 pdf.SetFillColor(230, 230, 230)
138 138 pdf.Cell(15, row_height, "#", 1, 0, 'L', 1)
139 139 query.columns.each_with_index do |column, i|
140 140 pdf.Cell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
141 141 end
142 142 pdf.Ln
143 143
144 144 # rows
145 145 pdf.SetFontStyle('',8)
146 146 pdf.SetFillColor(255, 255, 255)
147 group = false
147 previous_group = false
148 148 issues.each do |issue|
149 if query.grouped? && issue.send(query.group_by) != group
150 group = issue.send(query.group_by)
149 if query.grouped? && (group = query.group_by_column.value(issue)) != previous_group
151 150 pdf.SetFontStyle('B',9)
152 pdf.Cell(277, row_height, "#{group.blank? ? 'None' : group.to_s}", 1, 1, 'L')
151 pdf.Cell(277, row_height,
152 (group.blank? ? 'None' : group.to_s) + " (#{@issue_count_by_group[group]})",
153 1, 1, 'L')
153 154 pdf.SetFontStyle('',8)
155 previous_group = group
154 156 end
155 157 pdf.Cell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
156 158 query.columns.each_with_index do |column, i|
157 159 s = if column.is_a?(QueryCustomFieldColumn)
158 160 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
159 161 show_value(cv)
160 162 else
161 163 value = issue.send(column.name)
162 164 if value.is_a?(Date)
163 165 format_date(value)
164 166 elsif value.is_a?(Time)
165 167 format_time(value)
166 168 else
167 169 value
168 170 end
169 171 end
170 172 pdf.Cell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
171 173 end
172 174 pdf.Ln
173 175 end
174 176 if issues.size == Setting.issues_export_limit.to_i
175 177 pdf.SetFontStyle('B',10)
176 178 pdf.Cell(0, row_height, '...')
177 179 end
178 180 pdf.Output
179 181 end
180 182
181 183 # Returns a PDF string of a single issue
182 184 def issue_to_pdf(issue)
183 185 pdf = IFPDF.new(current_language)
184 186 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
185 187 pdf.AliasNbPages
186 188 pdf.footer_date = format_date(Date.today)
187 189 pdf.AddPage
188 190
189 191 pdf.SetFontStyle('B',11)
190 192 pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
191 193 pdf.Ln
192 194
193 195 y0 = pdf.GetY
194 196
195 197 pdf.SetFontStyle('B',9)
196 198 pdf.Cell(35,5, l(:field_status) + ":","LT")
197 199 pdf.SetFontStyle('',9)
198 200 pdf.Cell(60,5, issue.status.to_s,"RT")
199 201 pdf.SetFontStyle('B',9)
200 202 pdf.Cell(35,5, l(:field_priority) + ":","LT")
201 203 pdf.SetFontStyle('',9)
202 204 pdf.Cell(60,5, issue.priority.to_s,"RT")
203 205 pdf.Ln
204 206
205 207 pdf.SetFontStyle('B',9)
206 208 pdf.Cell(35,5, l(:field_author) + ":","L")
207 209 pdf.SetFontStyle('',9)
208 210 pdf.Cell(60,5, issue.author.to_s,"R")
209 211 pdf.SetFontStyle('B',9)
210 212 pdf.Cell(35,5, l(:field_category) + ":","L")
211 213 pdf.SetFontStyle('',9)
212 214 pdf.Cell(60,5, issue.category.to_s,"R")
213 215 pdf.Ln
214 216
215 217 pdf.SetFontStyle('B',9)
216 218 pdf.Cell(35,5, l(:field_created_on) + ":","L")
217 219 pdf.SetFontStyle('',9)
218 220 pdf.Cell(60,5, format_date(issue.created_on),"R")
219 221 pdf.SetFontStyle('B',9)
220 222 pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
221 223 pdf.SetFontStyle('',9)
222 224 pdf.Cell(60,5, issue.assigned_to.to_s,"R")
223 225 pdf.Ln
224 226
225 227 pdf.SetFontStyle('B',9)
226 228 pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
227 229 pdf.SetFontStyle('',9)
228 230 pdf.Cell(60,5, format_date(issue.updated_on),"RB")
229 231 pdf.SetFontStyle('B',9)
230 232 pdf.Cell(35,5, l(:field_due_date) + ":","LB")
231 233 pdf.SetFontStyle('',9)
232 234 pdf.Cell(60,5, format_date(issue.due_date),"RB")
233 235 pdf.Ln
234 236
235 237 for custom_value in issue.custom_field_values
236 238 pdf.SetFontStyle('B',9)
237 239 pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
238 240 pdf.SetFontStyle('',9)
239 241 pdf.MultiCell(155,5, (show_value custom_value),"R")
240 242 end
241 243
242 244 pdf.SetFontStyle('B',9)
243 245 pdf.Cell(35,5, l(:field_subject) + ":","LTB")
244 246 pdf.SetFontStyle('',9)
245 247 pdf.Cell(155,5, issue.subject,"RTB")
246 248 pdf.Ln
247 249
248 250 pdf.SetFontStyle('B',9)
249 251 pdf.Cell(35,5, l(:field_description) + ":")
250 252 pdf.SetFontStyle('',9)
251 253 pdf.MultiCell(155,5, @issue.description,"BR")
252 254
253 255 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
254 256 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
255 257 pdf.Ln
256 258
257 259 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
258 260 pdf.SetFontStyle('B',9)
259 261 pdf.Cell(190,5, l(:label_associated_revisions), "B")
260 262 pdf.Ln
261 263 for changeset in issue.changesets
262 264 pdf.SetFontStyle('B',8)
263 265 pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
264 266 pdf.Ln
265 267 unless changeset.comments.blank?
266 268 pdf.SetFontStyle('',8)
267 269 pdf.MultiCell(190,5, changeset.comments)
268 270 end
269 271 pdf.Ln
270 272 end
271 273 end
272 274
273 275 pdf.SetFontStyle('B',9)
274 276 pdf.Cell(190,5, l(:label_history), "B")
275 277 pdf.Ln
276 278 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
277 279 pdf.SetFontStyle('B',8)
278 280 pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
279 281 pdf.Ln
280 282 pdf.SetFontStyle('I',8)
281 283 for detail in journal.details
282 284 pdf.Cell(190,5, "- " + show_detail(detail, true))
283 285 pdf.Ln
284 286 end
285 287 if journal.notes?
286 288 pdf.SetFontStyle('',8)
287 289 pdf.MultiCell(190,5, journal.notes)
288 290 end
289 291 pdf.Ln
290 292 end
291 293
292 294 if issue.attachments.any?
293 295 pdf.SetFontStyle('B',9)
294 296 pdf.Cell(190,5, l(:label_attachment_plural), "B")
295 297 pdf.Ln
296 298 for attachment in issue.attachments
297 299 pdf.SetFontStyle('',8)
298 300 pdf.Cell(80,5, attachment.filename)
299 301 pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
300 302 pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
301 303 pdf.Cell(65,5, attachment.author.name,0,0,"R")
302 304 pdf.Ln
303 305 end
304 306 end
305 307 pdf.Output
306 308 end
307 309
308 310 # Returns a PDF string of a gantt chart
309 311 def gantt_to_pdf(gantt, project)
310 312 pdf = IFPDF.new(current_language)
311 313 pdf.SetTitle("#{l(:label_gantt)} #{project}")
312 314 pdf.AliasNbPages
313 315 pdf.footer_date = format_date(Date.today)
314 316 pdf.AddPage("L")
315 317 pdf.SetFontStyle('B',12)
316 318 pdf.SetX(15)
317 319 pdf.Cell(70, 20, project.to_s)
318 320 pdf.Ln
319 321 pdf.SetFontStyle('B',9)
320 322
321 323 subject_width = 70
322 324 header_heigth = 5
323 325
324 326 headers_heigth = header_heigth
325 327 show_weeks = false
326 328 show_days = false
327 329
328 330 if gantt.months < 7
329 331 show_weeks = true
330 332 headers_heigth = 2*header_heigth
331 333 if gantt.months < 3
332 334 show_days = true
333 335 headers_heigth = 3*header_heigth
334 336 end
335 337 end
336 338
337 339 g_width = 210
338 340 zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
339 341 g_height = 120
340 342 t_height = g_height + headers_heigth
341 343
342 344 y_start = pdf.GetY
343 345
344 346 # Months headers
345 347 month_f = gantt.date_from
346 348 left = subject_width
347 349 height = header_heigth
348 350 gantt.months.times do
349 351 width = ((month_f >> 1) - month_f) * zoom
350 352 pdf.SetY(y_start)
351 353 pdf.SetX(left)
352 354 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
353 355 left = left + width
354 356 month_f = month_f >> 1
355 357 end
356 358
357 359 # Weeks headers
358 360 if show_weeks
359 361 left = subject_width
360 362 height = header_heigth
361 363 if gantt.date_from.cwday == 1
362 364 # gantt.date_from is monday
363 365 week_f = gantt.date_from
364 366 else
365 367 # find next monday after gantt.date_from
366 368 week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
367 369 width = (7 - gantt.date_from.cwday + 1) * zoom-1
368 370 pdf.SetY(y_start + header_heigth)
369 371 pdf.SetX(left)
370 372 pdf.Cell(width + 1, height, "", "LTR")
371 373 left = left + width+1
372 374 end
373 375 while week_f <= gantt.date_to
374 376 width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
375 377 pdf.SetY(y_start + header_heigth)
376 378 pdf.SetX(left)
377 379 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
378 380 left = left + width
379 381 week_f = week_f+7
380 382 end
381 383 end
382 384
383 385 # Days headers
384 386 if show_days
385 387 left = subject_width
386 388 height = header_heigth
387 389 wday = gantt.date_from.cwday
388 390 pdf.SetFontStyle('B',7)
389 391 (gantt.date_to - gantt.date_from + 1).to_i.times do
390 392 width = zoom
391 393 pdf.SetY(y_start + 2 * header_heigth)
392 394 pdf.SetX(left)
393 395 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
394 396 left = left + width
395 397 wday = wday + 1
396 398 wday = 1 if wday > 7
397 399 end
398 400 end
399 401
400 402 pdf.SetY(y_start)
401 403 pdf.SetX(15)
402 404 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
403 405
404 406 # Tasks
405 407 top = headers_heigth + y_start
406 408 pdf.SetFontStyle('B',7)
407 409 gantt.events.each do |i|
408 410 pdf.SetY(top)
409 411 pdf.SetX(15)
410 412
411 413 if i.is_a? Issue
412 414 pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
413 415 else
414 416 pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
415 417 end
416 418
417 419 pdf.SetY(top)
418 420 pdf.SetX(subject_width)
419 421 pdf.Cell(g_width, 5, "", "LR")
420 422
421 423 pdf.SetY(top+1.5)
422 424
423 425 if i.is_a? Issue
424 426 i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
425 427 i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
426 428
427 429 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
428 430 i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
429 431 i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
430 432
431 433 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
432 434
433 435 i_left = ((i_start_date - gantt.date_from)*zoom)
434 436 i_width = ((i_end_date - i_start_date + 1)*zoom)
435 437 d_width = ((i_done_date - i_start_date)*zoom)
436 438 l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
437 439 l_width ||= 0
438 440
439 441 pdf.SetX(subject_width + i_left)
440 442 pdf.SetFillColor(200,200,200)
441 443 pdf.Cell(i_width, 2, "", 0, 0, "", 1)
442 444
443 445 if l_width > 0
444 446 pdf.SetY(top+1.5)
445 447 pdf.SetX(subject_width + i_left)
446 448 pdf.SetFillColor(255,100,100)
447 449 pdf.Cell(l_width, 2, "", 0, 0, "", 1)
448 450 end
449 451 if d_width > 0
450 452 pdf.SetY(top+1.5)
451 453 pdf.SetX(subject_width + i_left)
452 454 pdf.SetFillColor(100,100,255)
453 455 pdf.Cell(d_width, 2, "", 0, 0, "", 1)
454 456 end
455 457
456 458 pdf.SetY(top+1.5)
457 459 pdf.SetX(subject_width + i_left + i_width)
458 460 pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
459 461 else
460 462 i_left = ((i.start_date - gantt.date_from)*zoom)
461 463
462 464 pdf.SetX(subject_width + i_left)
463 465 pdf.SetFillColor(50,200,50)
464 466 pdf.Cell(2, 2, "", 0, 0, "", 1)
465 467
466 468 pdf.SetY(top+1.5)
467 469 pdf.SetX(subject_width + i_left + 3)
468 470 pdf.Cell(30, 2, "#{i.name}")
469 471 end
470 472
471 473 top = top + 5
472 474 pdf.SetDrawColor(200, 200, 200)
473 475 pdf.Line(15, top, subject_width+g_width, top)
474 476 if pdf.GetY() > 180
475 477 pdf.AddPage("L")
476 478 top = 20
477 479 pdf.Line(15, top, subject_width+g_width, top)
478 480 end
479 481 pdf.SetDrawColor(0, 0, 0)
480 482 end
481 483
482 484 pdf.Line(15, top, subject_width+g_width, top)
483 485 pdf.Output
484 486 end
485 487 end
486 488 end
487 489 end
@@ -1,1330 +1,1338
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 < ActionController::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_projects,
43 43 :custom_fields_trackers,
44 44 :time_entries,
45 45 :journals,
46 46 :journal_details,
47 47 :queries
48 48
49 49 def setup
50 50 @controller = IssuesController.new
51 51 @request = ActionController::TestRequest.new
52 52 @response = ActionController::TestResponse.new
53 53 User.current = nil
54 54 end
55 55
56 56 def test_index_routing
57 57 assert_routing(
58 58 {:method => :get, :path => '/issues'},
59 59 :controller => 'issues', :action => 'index'
60 60 )
61 61 end
62 62
63 63 def test_index
64 64 Setting.default_language = 'en'
65 65
66 66 get :index
67 67 assert_response :success
68 68 assert_template 'index.rhtml'
69 69 assert_not_nil assigns(:issues)
70 70 assert_nil assigns(:project)
71 71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 72 assert_tag :tag => 'a', :content => /Subproject issue/
73 73 # private projects hidden
74 74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
75 75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
76 76 # project column
77 77 assert_tag :tag => 'th', :content => /Project/
78 78 end
79 79
80 80 def test_index_should_not_list_issues_when_module_disabled
81 81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
82 82 get :index
83 83 assert_response :success
84 84 assert_template 'index.rhtml'
85 85 assert_not_nil assigns(:issues)
86 86 assert_nil assigns(:project)
87 87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
88 88 assert_tag :tag => 'a', :content => /Subproject issue/
89 89 end
90 90
91 91 def test_index_with_project_routing
92 92 assert_routing(
93 93 {:method => :get, :path => '/projects/23/issues'},
94 94 :controller => 'issues', :action => 'index', :project_id => '23'
95 95 )
96 96 end
97 97
98 98 def test_index_should_not_list_issues_when_module_disabled
99 99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
100 100 get :index
101 101 assert_response :success
102 102 assert_template 'index.rhtml'
103 103 assert_not_nil assigns(:issues)
104 104 assert_nil assigns(:project)
105 105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
106 106 assert_tag :tag => 'a', :content => /Subproject issue/
107 107 end
108 108
109 109 def test_index_with_project_routing
110 110 assert_routing(
111 111 {:method => :get, :path => 'projects/23/issues'},
112 112 :controller => 'issues', :action => 'index', :project_id => '23'
113 113 )
114 114 end
115 115
116 116 def test_index_with_project
117 117 Setting.display_subprojects_issues = 0
118 118 get :index, :project_id => 1
119 119 assert_response :success
120 120 assert_template 'index.rhtml'
121 121 assert_not_nil assigns(:issues)
122 122 assert_tag :tag => 'a', :content => /Can't print recipes/
123 123 assert_no_tag :tag => 'a', :content => /Subproject issue/
124 124 end
125 125
126 126 def test_index_with_project_and_subprojects
127 127 Setting.display_subprojects_issues = 1
128 128 get :index, :project_id => 1
129 129 assert_response :success
130 130 assert_template 'index.rhtml'
131 131 assert_not_nil assigns(:issues)
132 132 assert_tag :tag => 'a', :content => /Can't print recipes/
133 133 assert_tag :tag => 'a', :content => /Subproject issue/
134 134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
135 135 end
136 136
137 137 def test_index_with_project_and_subprojects_should_show_private_subprojects
138 138 @request.session[:user_id] = 2
139 139 Setting.display_subprojects_issues = 1
140 140 get :index, :project_id => 1
141 141 assert_response :success
142 142 assert_template 'index.rhtml'
143 143 assert_not_nil assigns(:issues)
144 144 assert_tag :tag => 'a', :content => /Can't print recipes/
145 145 assert_tag :tag => 'a', :content => /Subproject issue/
146 146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
147 147 end
148 148
149 149 def test_index_with_project_routing_formatted
150 150 assert_routing(
151 151 {:method => :get, :path => 'projects/23/issues.pdf'},
152 152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
153 153 )
154 154 assert_routing(
155 155 {:method => :get, :path => 'projects/23/issues.atom'},
156 156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
157 157 )
158 158 end
159 159
160 160 def test_index_with_project_and_filter
161 161 get :index, :project_id => 1, :set_filter => 1
162 162 assert_response :success
163 163 assert_template 'index.rhtml'
164 164 assert_not_nil assigns(:issues)
165 165 end
166 166
167 167 def test_index_with_query
168 168 get :index, :project_id => 1, :query_id => 5
169 169 assert_response :success
170 170 assert_template 'index.rhtml'
171 171 assert_not_nil assigns(:issues)
172 172 assert_nil assigns(:issue_count_by_group)
173 173 end
174 174
175 175 def test_index_with_query_grouped_by_tracker
176 176 get :index, :project_id => 1, :query_id => 6
177 177 assert_response :success
178 178 assert_template 'index.rhtml'
179 179 assert_not_nil assigns(:issues)
180 180 assert_not_nil assigns(:issue_count_by_group)
181 181 end
182 182
183 183 def test_index_with_query_grouped_by_list_custom_field
184 184 get :index, :project_id => 1, :query_id => 9
185 185 assert_response :success
186 186 assert_template 'index.rhtml'
187 187 assert_not_nil assigns(:issues)
188 188 assert_not_nil assigns(:issue_count_by_group)
189 189 end
190 190
191 191 def test_index_sort_by_field_not_included_in_columns
192 192 Setting.issue_list_default_columns = %w(subject author)
193 193 get :index, :sort => 'tracker'
194 194 end
195 195
196 196 def test_index_csv_with_project
197 197 Setting.default_language = 'en'
198 198
199 199 get :index, :format => 'csv'
200 200 assert_response :success
201 201 assert_not_nil assigns(:issues)
202 202 assert_equal 'text/csv', @response.content_type
203 203 assert @response.body.starts_with?("#,")
204 204
205 205 get :index, :project_id => 1, :format => 'csv'
206 206 assert_response :success
207 207 assert_not_nil assigns(:issues)
208 208 assert_equal 'text/csv', @response.content_type
209 209 end
210 210
211 211 def test_index_formatted
212 212 assert_routing(
213 213 {:method => :get, :path => 'issues.pdf'},
214 214 :controller => 'issues', :action => 'index', :format => 'pdf'
215 215 )
216 216 assert_routing(
217 217 {:method => :get, :path => 'issues.atom'},
218 218 :controller => 'issues', :action => 'index', :format => 'atom'
219 219 )
220 220 end
221 221
222 222 def test_index_pdf
223 223 get :index, :format => 'pdf'
224 224 assert_response :success
225 225 assert_not_nil assigns(:issues)
226 226 assert_equal 'application/pdf', @response.content_type
227 227
228 228 get :index, :project_id => 1, :format => 'pdf'
229 229 assert_response :success
230 230 assert_not_nil assigns(:issues)
231 231 assert_equal 'application/pdf', @response.content_type
232 232
233 233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
234 234 assert_response :success
235 235 assert_not_nil assigns(:issues)
236 236 assert_equal 'application/pdf', @response.content_type
237 237 end
238 238
239 def test_index_pdf_with_query_grouped_by_list_custom_field
240 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
241 assert_response :success
242 assert_not_nil assigns(:issues)
243 assert_not_nil assigns(:issue_count_by_group)
244 assert_equal 'application/pdf', @response.content_type
245 end
246
239 247 def test_index_sort
240 248 get :index, :sort => 'tracker,id:desc'
241 249 assert_response :success
242 250
243 251 sort_params = @request.session['issues_index_sort']
244 252 assert sort_params.is_a?(String)
245 253 assert_equal 'tracker,id:desc', sort_params
246 254
247 255 issues = assigns(:issues)
248 256 assert_not_nil issues
249 257 assert !issues.empty?
250 258 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
251 259 end
252 260
253 261 def test_index_with_columns
254 262 columns = ['tracker', 'subject', 'assigned_to']
255 263 get :index, :set_filter => 1, :query => { 'column_names' => columns}
256 264 assert_response :success
257 265
258 266 # query should use specified columns
259 267 query = assigns(:query)
260 268 assert_kind_of Query, query
261 269 assert_equal columns, query.column_names.map(&:to_s)
262 270
263 271 # columns should be stored in session
264 272 assert_kind_of Hash, session[:query]
265 273 assert_kind_of Array, session[:query][:column_names]
266 274 assert_equal columns, session[:query][:column_names].map(&:to_s)
267 275 end
268 276
269 277 def test_gantt
270 278 get :gantt, :project_id => 1
271 279 assert_response :success
272 280 assert_template 'gantt.rhtml'
273 281 assert_not_nil assigns(:gantt)
274 282 events = assigns(:gantt).events
275 283 assert_not_nil events
276 284 # Issue with start and due dates
277 285 i = Issue.find(1)
278 286 assert_not_nil i.due_date
279 287 assert events.include?(Issue.find(1))
280 288 # Issue with without due date but targeted to a version with date
281 289 i = Issue.find(2)
282 290 assert_nil i.due_date
283 291 assert events.include?(i)
284 292 end
285 293
286 294 def test_cross_project_gantt
287 295 get :gantt
288 296 assert_response :success
289 297 assert_template 'gantt.rhtml'
290 298 assert_not_nil assigns(:gantt)
291 299 events = assigns(:gantt).events
292 300 assert_not_nil events
293 301 end
294 302
295 303 def test_gantt_export_to_pdf
296 304 get :gantt, :project_id => 1, :format => 'pdf'
297 305 assert_response :success
298 306 assert_equal 'application/pdf', @response.content_type
299 307 assert @response.body.starts_with?('%PDF')
300 308 assert_not_nil assigns(:gantt)
301 309 end
302 310
303 311 def test_cross_project_gantt_export_to_pdf
304 312 get :gantt, :format => 'pdf'
305 313 assert_response :success
306 314 assert_equal 'application/pdf', @response.content_type
307 315 assert @response.body.starts_with?('%PDF')
308 316 assert_not_nil assigns(:gantt)
309 317 end
310 318
311 319 if Object.const_defined?(:Magick)
312 320 def test_gantt_image
313 321 get :gantt, :project_id => 1, :format => 'png'
314 322 assert_response :success
315 323 assert_equal 'image/png', @response.content_type
316 324 end
317 325 else
318 326 puts "RMagick not installed. Skipping tests !!!"
319 327 end
320 328
321 329 def test_calendar
322 330 get :calendar, :project_id => 1
323 331 assert_response :success
324 332 assert_template 'calendar'
325 333 assert_not_nil assigns(:calendar)
326 334 end
327 335
328 336 def test_cross_project_calendar
329 337 get :calendar
330 338 assert_response :success
331 339 assert_template 'calendar'
332 340 assert_not_nil assigns(:calendar)
333 341 end
334 342
335 343 def test_changes
336 344 get :changes, :project_id => 1
337 345 assert_response :success
338 346 assert_not_nil assigns(:journals)
339 347 assert_equal 'application/atom+xml', @response.content_type
340 348 end
341 349
342 350 def test_show_routing
343 351 assert_routing(
344 352 {:method => :get, :path => '/issues/64'},
345 353 :controller => 'issues', :action => 'show', :id => '64'
346 354 )
347 355 end
348 356
349 357 def test_show_routing_formatted
350 358 assert_routing(
351 359 {:method => :get, :path => '/issues/2332.pdf'},
352 360 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
353 361 )
354 362 assert_routing(
355 363 {:method => :get, :path => '/issues/23123.atom'},
356 364 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
357 365 )
358 366 end
359 367
360 368 def test_show_by_anonymous
361 369 get :show, :id => 1
362 370 assert_response :success
363 371 assert_template 'show.rhtml'
364 372 assert_not_nil assigns(:issue)
365 373 assert_equal Issue.find(1), assigns(:issue)
366 374
367 375 # anonymous role is allowed to add a note
368 376 assert_tag :tag => 'form',
369 377 :descendant => { :tag => 'fieldset',
370 378 :child => { :tag => 'legend',
371 379 :content => /Notes/ } }
372 380 end
373 381
374 382 def test_show_by_manager
375 383 @request.session[:user_id] = 2
376 384 get :show, :id => 1
377 385 assert_response :success
378 386
379 387 assert_tag :tag => 'form',
380 388 :descendant => { :tag => 'fieldset',
381 389 :child => { :tag => 'legend',
382 390 :content => /Change properties/ } },
383 391 :descendant => { :tag => 'fieldset',
384 392 :child => { :tag => 'legend',
385 393 :content => /Log time/ } },
386 394 :descendant => { :tag => 'fieldset',
387 395 :child => { :tag => 'legend',
388 396 :content => /Notes/ } }
389 397 end
390 398
391 399 def test_show_should_deny_anonymous_access_without_permission
392 400 Role.anonymous.remove_permission!(:view_issues)
393 401 get :show, :id => 1
394 402 assert_response :redirect
395 403 end
396 404
397 405 def test_show_should_deny_non_member_access_without_permission
398 406 Role.non_member.remove_permission!(:view_issues)
399 407 @request.session[:user_id] = 9
400 408 get :show, :id => 1
401 409 assert_response 403
402 410 end
403 411
404 412 def test_show_should_deny_member_access_without_permission
405 413 Role.find(1).remove_permission!(:view_issues)
406 414 @request.session[:user_id] = 2
407 415 get :show, :id => 1
408 416 assert_response 403
409 417 end
410 418
411 419 def test_show_should_not_disclose_relations_to_invisible_issues
412 420 Setting.cross_project_issue_relations = '1'
413 421 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
414 422 # Relation to a private project issue
415 423 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
416 424
417 425 get :show, :id => 1
418 426 assert_response :success
419 427
420 428 assert_tag :div, :attributes => { :id => 'relations' },
421 429 :descendant => { :tag => 'a', :content => /#2$/ }
422 430 assert_no_tag :div, :attributes => { :id => 'relations' },
423 431 :descendant => { :tag => 'a', :content => /#4$/ }
424 432 end
425 433
426 434 def test_show_atom
427 435 get :show, :id => 2, :format => 'atom'
428 436 assert_response :success
429 437 assert_template 'changes.rxml'
430 438 # Inline image
431 439 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
432 440 end
433 441
434 442 def test_new_routing
435 443 assert_routing(
436 444 {:method => :get, :path => '/projects/1/issues/new'},
437 445 :controller => 'issues', :action => 'new', :project_id => '1'
438 446 )
439 447 assert_recognizes(
440 448 {:controller => 'issues', :action => 'new', :project_id => '1'},
441 449 {:method => :post, :path => '/projects/1/issues'}
442 450 )
443 451 end
444 452
445 453 def test_show_export_to_pdf
446 454 get :show, :id => 3, :format => 'pdf'
447 455 assert_response :success
448 456 assert_equal 'application/pdf', @response.content_type
449 457 assert @response.body.starts_with?('%PDF')
450 458 assert_not_nil assigns(:issue)
451 459 end
452 460
453 461 def test_get_new
454 462 @request.session[:user_id] = 2
455 463 get :new, :project_id => 1, :tracker_id => 1
456 464 assert_response :success
457 465 assert_template 'new'
458 466
459 467 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
460 468 :value => 'Default string' }
461 469 end
462 470
463 471 def test_get_new_without_tracker_id
464 472 @request.session[:user_id] = 2
465 473 get :new, :project_id => 1
466 474 assert_response :success
467 475 assert_template 'new'
468 476
469 477 issue = assigns(:issue)
470 478 assert_not_nil issue
471 479 assert_equal Project.find(1).trackers.first, issue.tracker
472 480 end
473 481
474 482 def test_get_new_with_no_default_status_should_display_an_error
475 483 @request.session[:user_id] = 2
476 484 IssueStatus.delete_all
477 485
478 486 get :new, :project_id => 1
479 487 assert_response 500
480 488 assert_not_nil flash[:error]
481 489 assert_tag :tag => 'div', :attributes => { :class => /error/ },
482 490 :content => /No default issue/
483 491 end
484 492
485 493 def test_get_new_with_no_tracker_should_display_an_error
486 494 @request.session[:user_id] = 2
487 495 Tracker.delete_all
488 496
489 497 get :new, :project_id => 1
490 498 assert_response 500
491 499 assert_not_nil flash[:error]
492 500 assert_tag :tag => 'div', :attributes => { :class => /error/ },
493 501 :content => /No tracker/
494 502 end
495 503
496 504 def test_update_new_form
497 505 @request.session[:user_id] = 2
498 506 xhr :post, :update_form, :project_id => 1,
499 507 :issue => {:tracker_id => 2,
500 508 :subject => 'This is the test_new issue',
501 509 :description => 'This is the description',
502 510 :priority_id => 5}
503 511 assert_response :success
504 512 assert_template 'attributes'
505 513
506 514 issue = assigns(:issue)
507 515 assert_kind_of Issue, issue
508 516 assert_equal 1, issue.project_id
509 517 assert_equal 2, issue.tracker_id
510 518 assert_equal 'This is the test_new issue', issue.subject
511 519 end
512 520
513 521 def test_post_new
514 522 @request.session[:user_id] = 2
515 523 assert_difference 'Issue.count' do
516 524 post :new, :project_id => 1,
517 525 :issue => {:tracker_id => 3,
518 526 :subject => 'This is the test_new issue',
519 527 :description => 'This is the description',
520 528 :priority_id => 5,
521 529 :estimated_hours => '',
522 530 :custom_field_values => {'2' => 'Value for field 2'}}
523 531 end
524 532 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
525 533
526 534 issue = Issue.find_by_subject('This is the test_new issue')
527 535 assert_not_nil issue
528 536 assert_equal 2, issue.author_id
529 537 assert_equal 3, issue.tracker_id
530 538 assert_nil issue.estimated_hours
531 539 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
532 540 assert_not_nil v
533 541 assert_equal 'Value for field 2', v.value
534 542 end
535 543
536 544 def test_post_new_and_continue
537 545 @request.session[:user_id] = 2
538 546 post :new, :project_id => 1,
539 547 :issue => {:tracker_id => 3,
540 548 :subject => 'This is first issue',
541 549 :priority_id => 5},
542 550 :continue => ''
543 551 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
544 552 end
545 553
546 554 def test_post_new_without_custom_fields_param
547 555 @request.session[:user_id] = 2
548 556 assert_difference 'Issue.count' do
549 557 post :new, :project_id => 1,
550 558 :issue => {:tracker_id => 1,
551 559 :subject => 'This is the test_new issue',
552 560 :description => 'This is the description',
553 561 :priority_id => 5}
554 562 end
555 563 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
556 564 end
557 565
558 566 def test_post_new_with_required_custom_field_and_without_custom_fields_param
559 567 field = IssueCustomField.find_by_name('Database')
560 568 field.update_attribute(:is_required, true)
561 569
562 570 @request.session[:user_id] = 2
563 571 post :new, :project_id => 1,
564 572 :issue => {:tracker_id => 1,
565 573 :subject => 'This is the test_new issue',
566 574 :description => 'This is the description',
567 575 :priority_id => 5}
568 576 assert_response :success
569 577 assert_template 'new'
570 578 issue = assigns(:issue)
571 579 assert_not_nil issue
572 580 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
573 581 end
574 582
575 583 def test_post_new_with_watchers
576 584 @request.session[:user_id] = 2
577 585 ActionMailer::Base.deliveries.clear
578 586
579 587 assert_difference 'Watcher.count', 2 do
580 588 post :new, :project_id => 1,
581 589 :issue => {:tracker_id => 1,
582 590 :subject => 'This is a new issue with watchers',
583 591 :description => 'This is the description',
584 592 :priority_id => 5,
585 593 :watcher_user_ids => ['2', '3']}
586 594 end
587 595 issue = Issue.find_by_subject('This is a new issue with watchers')
588 596 assert_not_nil issue
589 597 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
590 598
591 599 # Watchers added
592 600 assert_equal [2, 3], issue.watcher_user_ids.sort
593 601 assert issue.watched_by?(User.find(3))
594 602 # Watchers notified
595 603 mail = ActionMailer::Base.deliveries.last
596 604 assert_kind_of TMail::Mail, mail
597 605 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
598 606 end
599 607
600 608 def test_post_new_should_send_a_notification
601 609 ActionMailer::Base.deliveries.clear
602 610 @request.session[:user_id] = 2
603 611 assert_difference 'Issue.count' do
604 612 post :new, :project_id => 1,
605 613 :issue => {:tracker_id => 3,
606 614 :subject => 'This is the test_new issue',
607 615 :description => 'This is the description',
608 616 :priority_id => 5,
609 617 :estimated_hours => '',
610 618 :custom_field_values => {'2' => 'Value for field 2'}}
611 619 end
612 620 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
613 621
614 622 assert_equal 1, ActionMailer::Base.deliveries.size
615 623 end
616 624
617 625 def test_post_should_preserve_fields_values_on_validation_failure
618 626 @request.session[:user_id] = 2
619 627 post :new, :project_id => 1,
620 628 :issue => {:tracker_id => 1,
621 629 # empty subject
622 630 :subject => '',
623 631 :description => 'This is a description',
624 632 :priority_id => 6,
625 633 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
626 634 assert_response :success
627 635 assert_template 'new'
628 636
629 637 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
630 638 :content => 'This is a description'
631 639 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
632 640 :child => { :tag => 'option', :attributes => { :selected => 'selected',
633 641 :value => '6' },
634 642 :content => 'High' }
635 643 # Custom fields
636 644 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
637 645 :child => { :tag => 'option', :attributes => { :selected => 'selected',
638 646 :value => 'Oracle' },
639 647 :content => 'Oracle' }
640 648 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
641 649 :value => 'Value for field 2'}
642 650 end
643 651
644 652 def test_copy_routing
645 653 assert_routing(
646 654 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
647 655 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
648 656 )
649 657 end
650 658
651 659 def test_copy_issue
652 660 @request.session[:user_id] = 2
653 661 get :new, :project_id => 1, :copy_from => 1
654 662 assert_template 'new'
655 663 assert_not_nil assigns(:issue)
656 664 orig = Issue.find(1)
657 665 assert_equal orig.subject, assigns(:issue).subject
658 666 end
659 667
660 668 def test_edit_routing
661 669 assert_routing(
662 670 {:method => :get, :path => '/issues/1/edit'},
663 671 :controller => 'issues', :action => 'edit', :id => '1'
664 672 )
665 673 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
666 674 {:controller => 'issues', :action => 'edit', :id => '1'},
667 675 {:method => :post, :path => '/issues/1/edit'}
668 676 )
669 677 end
670 678
671 679 def test_get_edit
672 680 @request.session[:user_id] = 2
673 681 get :edit, :id => 1
674 682 assert_response :success
675 683 assert_template 'edit'
676 684 assert_not_nil assigns(:issue)
677 685 assert_equal Issue.find(1), assigns(:issue)
678 686 end
679 687
680 688 def test_get_edit_with_params
681 689 @request.session[:user_id] = 2
682 690 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
683 691 assert_response :success
684 692 assert_template 'edit'
685 693
686 694 issue = assigns(:issue)
687 695 assert_not_nil issue
688 696
689 697 assert_equal 5, issue.status_id
690 698 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
691 699 :child => { :tag => 'option',
692 700 :content => 'Closed',
693 701 :attributes => { :selected => 'selected' } }
694 702
695 703 assert_equal 7, issue.priority_id
696 704 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
697 705 :child => { :tag => 'option',
698 706 :content => 'Urgent',
699 707 :attributes => { :selected => 'selected' } }
700 708 end
701 709
702 710 def test_update_edit_form
703 711 @request.session[:user_id] = 2
704 712 xhr :post, :update_form, :project_id => 1,
705 713 :id => 1,
706 714 :issue => {:tracker_id => 2,
707 715 :subject => 'This is the test_new issue',
708 716 :description => 'This is the description',
709 717 :priority_id => 5}
710 718 assert_response :success
711 719 assert_template 'attributes'
712 720
713 721 issue = assigns(:issue)
714 722 assert_kind_of Issue, issue
715 723 assert_equal 1, issue.id
716 724 assert_equal 1, issue.project_id
717 725 assert_equal 2, issue.tracker_id
718 726 assert_equal 'This is the test_new issue', issue.subject
719 727 end
720 728
721 729 def test_reply_routing
722 730 assert_routing(
723 731 {:method => :post, :path => '/issues/1/quoted'},
724 732 :controller => 'issues', :action => 'reply', :id => '1'
725 733 )
726 734 end
727 735
728 736 def test_reply_to_issue
729 737 @request.session[:user_id] = 2
730 738 get :reply, :id => 1
731 739 assert_response :success
732 740 assert_select_rjs :show, "update"
733 741 end
734 742
735 743 def test_reply_to_note
736 744 @request.session[:user_id] = 2
737 745 get :reply, :id => 1, :journal_id => 2
738 746 assert_response :success
739 747 assert_select_rjs :show, "update"
740 748 end
741 749
742 750 def test_post_edit_without_custom_fields_param
743 751 @request.session[:user_id] = 2
744 752 ActionMailer::Base.deliveries.clear
745 753
746 754 issue = Issue.find(1)
747 755 assert_equal '125', issue.custom_value_for(2).value
748 756 old_subject = issue.subject
749 757 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
750 758
751 759 assert_difference('Journal.count') do
752 760 assert_difference('JournalDetail.count', 2) do
753 761 post :edit, :id => 1, :issue => {:subject => new_subject,
754 762 :priority_id => '6',
755 763 :category_id => '1' # no change
756 764 }
757 765 end
758 766 end
759 767 assert_redirected_to :action => 'show', :id => '1'
760 768 issue.reload
761 769 assert_equal new_subject, issue.subject
762 770 # Make sure custom fields were not cleared
763 771 assert_equal '125', issue.custom_value_for(2).value
764 772
765 773 mail = ActionMailer::Base.deliveries.last
766 774 assert_kind_of TMail::Mail, mail
767 775 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
768 776 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
769 777 end
770 778
771 779 def test_post_edit_with_custom_field_change
772 780 @request.session[:user_id] = 2
773 781 issue = Issue.find(1)
774 782 assert_equal '125', issue.custom_value_for(2).value
775 783
776 784 assert_difference('Journal.count') do
777 785 assert_difference('JournalDetail.count', 3) do
778 786 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
779 787 :priority_id => '6',
780 788 :category_id => '1', # no change
781 789 :custom_field_values => { '2' => 'New custom value' }
782 790 }
783 791 end
784 792 end
785 793 assert_redirected_to :action => 'show', :id => '1'
786 794 issue.reload
787 795 assert_equal 'New custom value', issue.custom_value_for(2).value
788 796
789 797 mail = ActionMailer::Base.deliveries.last
790 798 assert_kind_of TMail::Mail, mail
791 799 assert mail.body.include?("Searchable field changed from 125 to New custom value")
792 800 end
793 801
794 802 def test_post_edit_with_status_and_assignee_change
795 803 issue = Issue.find(1)
796 804 assert_equal 1, issue.status_id
797 805 @request.session[:user_id] = 2
798 806 assert_difference('TimeEntry.count', 0) do
799 807 post :edit,
800 808 :id => 1,
801 809 :issue => { :status_id => 2, :assigned_to_id => 3 },
802 810 :notes => 'Assigned to dlopper',
803 811 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
804 812 end
805 813 assert_redirected_to :action => 'show', :id => '1'
806 814 issue.reload
807 815 assert_equal 2, issue.status_id
808 816 j = Journal.find(:first, :order => 'id DESC')
809 817 assert_equal 'Assigned to dlopper', j.notes
810 818 assert_equal 2, j.details.size
811 819
812 820 mail = ActionMailer::Base.deliveries.last
813 821 assert mail.body.include?("Status changed from New to Assigned")
814 822 # subject should contain the new status
815 823 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
816 824 end
817 825
818 826 def test_post_edit_with_note_only
819 827 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
820 828 # anonymous user
821 829 post :edit,
822 830 :id => 1,
823 831 :notes => notes
824 832 assert_redirected_to :action => 'show', :id => '1'
825 833 j = Journal.find(:first, :order => 'id DESC')
826 834 assert_equal notes, j.notes
827 835 assert_equal 0, j.details.size
828 836 assert_equal User.anonymous, j.user
829 837
830 838 mail = ActionMailer::Base.deliveries.last
831 839 assert mail.body.include?(notes)
832 840 end
833 841
834 842 def test_post_edit_with_note_and_spent_time
835 843 @request.session[:user_id] = 2
836 844 spent_hours_before = Issue.find(1).spent_hours
837 845 assert_difference('TimeEntry.count') do
838 846 post :edit,
839 847 :id => 1,
840 848 :notes => '2.5 hours added',
841 849 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
842 850 end
843 851 assert_redirected_to :action => 'show', :id => '1'
844 852
845 853 issue = Issue.find(1)
846 854
847 855 j = Journal.find(:first, :order => 'id DESC')
848 856 assert_equal '2.5 hours added', j.notes
849 857 assert_equal 0, j.details.size
850 858
851 859 t = issue.time_entries.find(:first, :order => 'id DESC')
852 860 assert_not_nil t
853 861 assert_equal 2.5, t.hours
854 862 assert_equal spent_hours_before + 2.5, issue.spent_hours
855 863 end
856 864
857 865 def test_post_edit_with_attachment_only
858 866 set_tmp_attachments_directory
859 867
860 868 # Delete all fixtured journals, a race condition can occur causing the wrong
861 869 # journal to get fetched in the next find.
862 870 Journal.delete_all
863 871
864 872 # anonymous user
865 873 post :edit,
866 874 :id => 1,
867 875 :notes => '',
868 876 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
869 877 assert_redirected_to :action => 'show', :id => '1'
870 878 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
871 879 assert j.notes.blank?
872 880 assert_equal 1, j.details.size
873 881 assert_equal 'testfile.txt', j.details.first.value
874 882 assert_equal User.anonymous, j.user
875 883
876 884 mail = ActionMailer::Base.deliveries.last
877 885 assert mail.body.include?('testfile.txt')
878 886 end
879 887
880 888 def test_post_edit_with_no_change
881 889 issue = Issue.find(1)
882 890 issue.journals.clear
883 891 ActionMailer::Base.deliveries.clear
884 892
885 893 post :edit,
886 894 :id => 1,
887 895 :notes => ''
888 896 assert_redirected_to :action => 'show', :id => '1'
889 897
890 898 issue.reload
891 899 assert issue.journals.empty?
892 900 # No email should be sent
893 901 assert ActionMailer::Base.deliveries.empty?
894 902 end
895 903
896 904 def test_post_edit_should_send_a_notification
897 905 @request.session[:user_id] = 2
898 906 ActionMailer::Base.deliveries.clear
899 907 issue = Issue.find(1)
900 908 old_subject = issue.subject
901 909 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
902 910
903 911 post :edit, :id => 1, :issue => {:subject => new_subject,
904 912 :priority_id => '6',
905 913 :category_id => '1' # no change
906 914 }
907 915 assert_equal 1, ActionMailer::Base.deliveries.size
908 916 end
909 917
910 918 def test_post_edit_with_invalid_spent_time
911 919 @request.session[:user_id] = 2
912 920 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
913 921
914 922 assert_no_difference('Journal.count') do
915 923 post :edit,
916 924 :id => 1,
917 925 :notes => notes,
918 926 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
919 927 end
920 928 assert_response :success
921 929 assert_template 'edit'
922 930
923 931 assert_tag :textarea, :attributes => { :name => 'notes' },
924 932 :content => notes
925 933 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
926 934 end
927 935
928 936 def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
929 937 issue = Issue.find(2)
930 938 @request.session[:user_id] = 2
931 939
932 940 post :edit,
933 941 :id => issue.id,
934 942 :issue => {
935 943 :fixed_version_id => 4
936 944 }
937 945
938 946 assert_response :redirect
939 947 issue.reload
940 948 assert_equal 4, issue.fixed_version_id
941 949 assert_not_equal issue.project_id, issue.fixed_version.project_id
942 950 end
943 951
944 952 def test_post_edit_should_redirect_back_using_the_back_url_parameter
945 953 issue = Issue.find(2)
946 954 @request.session[:user_id] = 2
947 955
948 956 post :edit,
949 957 :id => issue.id,
950 958 :issue => {
951 959 :fixed_version_id => 4
952 960 },
953 961 :back_url => '/issues'
954 962
955 963 assert_response :redirect
956 964 assert_redirected_to '/issues'
957 965 end
958 966
959 967 def test_post_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
960 968 issue = Issue.find(2)
961 969 @request.session[:user_id] = 2
962 970
963 971 post :edit,
964 972 :id => issue.id,
965 973 :issue => {
966 974 :fixed_version_id => 4
967 975 },
968 976 :back_url => 'http://google.com'
969 977
970 978 assert_response :redirect
971 979 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
972 980 end
973 981
974 982 def test_get_bulk_edit
975 983 @request.session[:user_id] = 2
976 984 get :bulk_edit, :ids => [1, 2]
977 985 assert_response :success
978 986 assert_template 'bulk_edit'
979 987
980 988 # Project specific custom field, date type
981 989 field = CustomField.find(9)
982 990 assert !field.is_for_all?
983 991 assert_equal 'date', field.field_format
984 992 assert_tag :input, :attributes => {:name => 'custom_field_values[9]'}
985 993
986 994 # System wide custom field
987 995 assert CustomField.find(1).is_for_all?
988 996 assert_tag :select, :attributes => {:name => 'custom_field_values[1]'}
989 997 end
990 998
991 999 def test_bulk_edit
992 1000 @request.session[:user_id] = 2
993 1001 # update issues priority
994 1002 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
995 1003 :assigned_to_id => '',
996 1004 :custom_field_values => {'2' => ''},
997 1005 :notes => 'Bulk editing'
998 1006 assert_response 302
999 1007 # check that the issues were updated
1000 1008 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1001 1009
1002 1010 issue = Issue.find(1)
1003 1011 journal = issue.journals.find(:first, :order => 'created_on DESC')
1004 1012 assert_equal '125', issue.custom_value_for(2).value
1005 1013 assert_equal 'Bulk editing', journal.notes
1006 1014 assert_equal 1, journal.details.size
1007 1015 end
1008 1016
1009 1017 def test_bullk_edit_should_send_a_notification
1010 1018 @request.session[:user_id] = 2
1011 1019 ActionMailer::Base.deliveries.clear
1012 1020 post(:bulk_edit,
1013 1021 {
1014 1022 :ids => [1, 2],
1015 1023 :priority_id => 7,
1016 1024 :assigned_to_id => '',
1017 1025 :custom_field_values => {'2' => ''},
1018 1026 :notes => 'Bulk editing'
1019 1027 })
1020 1028
1021 1029 assert_response 302
1022 1030 assert_equal 2, ActionMailer::Base.deliveries.size
1023 1031 end
1024 1032
1025 1033 def test_bulk_edit_status
1026 1034 @request.session[:user_id] = 2
1027 1035 # update issues priority
1028 1036 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1029 1037 :assigned_to_id => '',
1030 1038 :status_id => '5',
1031 1039 :notes => 'Bulk editing status'
1032 1040 assert_response 302
1033 1041 issue = Issue.find(1)
1034 1042 assert issue.closed?
1035 1043 end
1036 1044
1037 1045 def test_bulk_edit_custom_field
1038 1046 @request.session[:user_id] = 2
1039 1047 # update issues priority
1040 1048 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1041 1049 :assigned_to_id => '',
1042 1050 :custom_field_values => {'2' => '777'},
1043 1051 :notes => 'Bulk editing custom field'
1044 1052 assert_response 302
1045 1053
1046 1054 issue = Issue.find(1)
1047 1055 journal = issue.journals.find(:first, :order => 'created_on DESC')
1048 1056 assert_equal '777', issue.custom_value_for(2).value
1049 1057 assert_equal 1, journal.details.size
1050 1058 assert_equal '125', journal.details.first.old_value
1051 1059 assert_equal '777', journal.details.first.value
1052 1060 end
1053 1061
1054 1062 def test_bulk_unassign
1055 1063 assert_not_nil Issue.find(2).assigned_to
1056 1064 @request.session[:user_id] = 2
1057 1065 # unassign issues
1058 1066 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1059 1067 assert_response 302
1060 1068 # check that the issues were updated
1061 1069 assert_nil Issue.find(2).assigned_to
1062 1070 end
1063 1071
1064 1072 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
1065 1073 @request.session[:user_id] = 2
1066 1074
1067 1075 post :bulk_edit,
1068 1076 :ids => [1,2],
1069 1077 :fixed_version_id => 4
1070 1078
1071 1079 assert_response :redirect
1072 1080 issues = Issue.find([1,2])
1073 1081 issues.each do |issue|
1074 1082 assert_equal 4, issue.fixed_version_id
1075 1083 assert_not_equal issue.project_id, issue.fixed_version.project_id
1076 1084 end
1077 1085 end
1078 1086
1079 1087 def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter
1080 1088 @request.session[:user_id] = 2
1081 1089 post :bulk_edit, :ids => [1,2], :back_url => '/issues'
1082 1090
1083 1091 assert_response :redirect
1084 1092 assert_redirected_to '/issues'
1085 1093 end
1086 1094
1087 1095 def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1088 1096 @request.session[:user_id] = 2
1089 1097 post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com'
1090 1098
1091 1099 assert_response :redirect
1092 1100 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1093 1101 end
1094 1102
1095 1103 def test_move_routing
1096 1104 assert_routing(
1097 1105 {:method => :get, :path => '/issues/1/move'},
1098 1106 :controller => 'issues', :action => 'move', :id => '1'
1099 1107 )
1100 1108 assert_recognizes(
1101 1109 {:controller => 'issues', :action => 'move', :id => '1'},
1102 1110 {:method => :post, :path => '/issues/1/move'}
1103 1111 )
1104 1112 end
1105 1113
1106 1114 def test_move_one_issue_to_another_project
1107 1115 @request.session[:user_id] = 2
1108 1116 post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1109 1117 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1110 1118 assert_equal 2, Issue.find(1).project_id
1111 1119 end
1112 1120
1113 1121 def test_move_one_issue_to_another_project_should_follow_when_needed
1114 1122 @request.session[:user_id] = 2
1115 1123 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1116 1124 assert_redirected_to '/issues/1'
1117 1125 end
1118 1126
1119 1127 def test_bulk_move_to_another_project
1120 1128 @request.session[:user_id] = 2
1121 1129 post :move, :ids => [1, 2], :new_project_id => 2
1122 1130 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1123 1131 # Issues moved to project 2
1124 1132 assert_equal 2, Issue.find(1).project_id
1125 1133 assert_equal 2, Issue.find(2).project_id
1126 1134 # No tracker change
1127 1135 assert_equal 1, Issue.find(1).tracker_id
1128 1136 assert_equal 2, Issue.find(2).tracker_id
1129 1137 end
1130 1138
1131 1139 def test_bulk_move_to_another_tracker
1132 1140 @request.session[:user_id] = 2
1133 1141 post :move, :ids => [1, 2], :new_tracker_id => 2
1134 1142 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1135 1143 assert_equal 2, Issue.find(1).tracker_id
1136 1144 assert_equal 2, Issue.find(2).tracker_id
1137 1145 end
1138 1146
1139 1147 def test_bulk_copy_to_another_project
1140 1148 @request.session[:user_id] = 2
1141 1149 assert_difference 'Issue.count', 2 do
1142 1150 assert_no_difference 'Project.find(1).issues.count' do
1143 1151 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1144 1152 end
1145 1153 end
1146 1154 assert_redirected_to 'projects/ecookbook/issues'
1147 1155 end
1148 1156
1149 1157 context "#move via bulk copy" do
1150 1158 should "allow not changing the issue's attributes" do
1151 1159 @request.session[:user_id] = 2
1152 1160 issue_before_move = Issue.find(1)
1153 1161 assert_difference 'Issue.count', 1 do
1154 1162 assert_no_difference 'Project.find(1).issues.count' do
1155 1163 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1156 1164 end
1157 1165 end
1158 1166 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
1159 1167 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
1160 1168 assert_equal issue_before_move.status_id, issue_after_move.status_id
1161 1169 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
1162 1170 end
1163 1171
1164 1172 should "allow changing the issue's attributes" do
1165 1173 @request.session[:user_id] = 2
1166 1174 assert_difference 'Issue.count', 2 do
1167 1175 assert_no_difference 'Project.find(1).issues.count' do
1168 1176 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1169 1177 end
1170 1178 end
1171 1179
1172 1180 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1173 1181 assert_equal 2, copied_issues.size
1174 1182 copied_issues.each do |issue|
1175 1183 assert_equal 2, issue.project_id, "Project is incorrect"
1176 1184 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1177 1185 assert_equal 3, issue.status_id, "Status is incorrect"
1178 1186 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1179 1187 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1180 1188 end
1181 1189 end
1182 1190 end
1183 1191
1184 1192 def test_copy_to_another_project_should_follow_when_needed
1185 1193 @request.session[:user_id] = 2
1186 1194 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1187 1195 issue = Issue.first(:order => 'id DESC')
1188 1196 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1189 1197 end
1190 1198
1191 1199 def test_context_menu_one_issue
1192 1200 @request.session[:user_id] = 2
1193 1201 get :context_menu, :ids => [1]
1194 1202 assert_response :success
1195 1203 assert_template 'context_menu'
1196 1204 assert_tag :tag => 'a', :content => 'Edit',
1197 1205 :attributes => { :href => '/issues/1/edit',
1198 1206 :class => 'icon-edit' }
1199 1207 assert_tag :tag => 'a', :content => 'Closed',
1200 1208 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1201 1209 :class => '' }
1202 1210 assert_tag :tag => 'a', :content => 'Immediate',
1203 1211 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1204 1212 :class => '' }
1205 1213 # Versions
1206 1214 assert_tag :tag => 'a', :content => '2.0',
1207 1215 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&amp;ids%5B%5D=1',
1208 1216 :class => '' }
1209 1217 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1210 1218 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
1211 1219 :class => '' }
1212 1220
1213 1221 assert_tag :tag => 'a', :content => 'Dave Lopper',
1214 1222 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1215 1223 :class => '' }
1216 1224 assert_tag :tag => 'a', :content => 'Duplicate',
1217 1225 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1218 1226 :class => 'icon-duplicate' }
1219 1227 assert_tag :tag => 'a', :content => 'Copy',
1220 1228 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
1221 1229 :class => 'icon-copy' }
1222 1230 assert_tag :tag => 'a', :content => 'Move',
1223 1231 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1224 1232 :class => 'icon-move' }
1225 1233 assert_tag :tag => 'a', :content => 'Delete',
1226 1234 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1227 1235 :class => 'icon-del' }
1228 1236 end
1229 1237
1230 1238 def test_context_menu_one_issue_by_anonymous
1231 1239 get :context_menu, :ids => [1]
1232 1240 assert_response :success
1233 1241 assert_template 'context_menu'
1234 1242 assert_tag :tag => 'a', :content => 'Delete',
1235 1243 :attributes => { :href => '#',
1236 1244 :class => 'icon-del disabled' }
1237 1245 end
1238 1246
1239 1247 def test_context_menu_multiple_issues_of_same_project
1240 1248 @request.session[:user_id] = 2
1241 1249 get :context_menu, :ids => [1, 2]
1242 1250 assert_response :success
1243 1251 assert_template 'context_menu'
1244 1252 assert_tag :tag => 'a', :content => 'Edit',
1245 1253 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1246 1254 :class => 'icon-edit' }
1247 1255 assert_tag :tag => 'a', :content => 'Immediate',
1248 1256 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1249 1257 :class => '' }
1250 1258 assert_tag :tag => 'a', :content => 'Dave Lopper',
1251 1259 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1252 1260 :class => '' }
1253 1261 assert_tag :tag => 'a', :content => 'Copy',
1254 1262 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1255 1263 :class => 'icon-copy' }
1256 1264 assert_tag :tag => 'a', :content => 'Move',
1257 1265 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1258 1266 :class => 'icon-move' }
1259 1267 assert_tag :tag => 'a', :content => 'Delete',
1260 1268 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1261 1269 :class => 'icon-del' }
1262 1270 end
1263 1271
1264 1272 def test_context_menu_multiple_issues_of_different_project
1265 1273 @request.session[:user_id] = 2
1266 1274 get :context_menu, :ids => [1, 2, 4]
1267 1275 assert_response :success
1268 1276 assert_template 'context_menu'
1269 1277 assert_tag :tag => 'a', :content => 'Delete',
1270 1278 :attributes => { :href => '#',
1271 1279 :class => 'icon-del disabled' }
1272 1280 end
1273 1281
1274 1282 def test_destroy_routing
1275 1283 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1276 1284 {:controller => 'issues', :action => 'destroy', :id => '1'},
1277 1285 {:method => :post, :path => '/issues/1/destroy'}
1278 1286 )
1279 1287 end
1280 1288
1281 1289 def test_destroy_issue_with_no_time_entries
1282 1290 assert_nil TimeEntry.find_by_issue_id(2)
1283 1291 @request.session[:user_id] = 2
1284 1292 post :destroy, :id => 2
1285 1293 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1286 1294 assert_nil Issue.find_by_id(2)
1287 1295 end
1288 1296
1289 1297 def test_destroy_issues_with_time_entries
1290 1298 @request.session[:user_id] = 2
1291 1299 post :destroy, :ids => [1, 3]
1292 1300 assert_response :success
1293 1301 assert_template 'destroy'
1294 1302 assert_not_nil assigns(:hours)
1295 1303 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1296 1304 end
1297 1305
1298 1306 def test_destroy_issues_and_destroy_time_entries
1299 1307 @request.session[:user_id] = 2
1300 1308 post :destroy, :ids => [1, 3], :todo => 'destroy'
1301 1309 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1302 1310 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1303 1311 assert_nil TimeEntry.find_by_id([1, 2])
1304 1312 end
1305 1313
1306 1314 def test_destroy_issues_and_assign_time_entries_to_project
1307 1315 @request.session[:user_id] = 2
1308 1316 post :destroy, :ids => [1, 3], :todo => 'nullify'
1309 1317 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1310 1318 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1311 1319 assert_nil TimeEntry.find(1).issue_id
1312 1320 assert_nil TimeEntry.find(2).issue_id
1313 1321 end
1314 1322
1315 1323 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1316 1324 @request.session[:user_id] = 2
1317 1325 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1318 1326 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1319 1327 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1320 1328 assert_equal 2, TimeEntry.find(1).issue_id
1321 1329 assert_equal 2, TimeEntry.find(2).issue_id
1322 1330 end
1323 1331
1324 1332 def test_default_search_scope
1325 1333 get :index
1326 1334 assert_tag :div, :attributes => {:id => 'quick-search'},
1327 1335 :child => {:tag => 'form',
1328 1336 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1329 1337 end
1330 1338 end
General Comments 0
You need to be logged in to leave comments. Login now