##// END OF EJS Templates
Fixed: PDF export of a issue list grouped by a custom field raises an error (#4600)....
Jean-Philippe Lang -
r3219:320c191f04d8
parent child
Show More
@@ -1,571 +1,572
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, :delete],
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.xml { }
63 64 format.atom { limit = Setting.feeds_limit.to_i }
64 65 format.csv { limit = Setting.issues_export_limit.to_i }
65 66 format.pdf { limit = Setting.issues_export_limit.to_i }
66 67 end
67 68
68 69 @issue_count = @query.issue_count
69 70 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
70 71 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 72 :order => sort_clause,
72 73 :offset => @issue_pages.current.offset,
73 74 :limit => limit)
74 75 @issue_count_by_group = @query.issue_count_by_group
75 76
76 77 respond_to do |format|
77 78 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
78 79 format.xml { render :layout => false }
79 80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 81 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
81 82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 83 end
83 84 else
84 85 # Send html if the query is not valid
85 86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 87 end
87 88 rescue ActiveRecord::RecordNotFound
88 89 render_404
89 90 end
90 91
91 92 def changes
92 93 retrieve_query
93 94 sort_init 'id', 'desc'
94 95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95 96
96 97 if @query.valid?
97 98 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
98 99 :limit => 25)
99 100 end
100 101 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
101 102 render :layout => false, :content_type => 'application/atom+xml'
102 103 rescue ActiveRecord::RecordNotFound
103 104 render_404
104 105 end
105 106
106 107 def show
107 108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 109 @journals.each_with_index {|j,i| j.indice = i+1}
109 110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 111 @changesets = @issue.changesets
111 112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
112 113 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
113 114 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
114 115 @priorities = IssuePriority.all
115 116 @time_entry = TimeEntry.new
116 117 respond_to do |format|
117 118 format.html { render :template => 'issues/show.rhtml' }
118 119 format.xml { render :layout => false }
119 120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
120 121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
121 122 end
122 123 end
123 124
124 125 # Add a new issue
125 126 # The new issue will be created from an existing one if copy_from parameter is given
126 127 def new
127 128 @issue = Issue.new
128 129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
129 130 @issue.project = @project
130 131 # Tracker must be set before custom field values
131 132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
132 133 if @issue.tracker.nil?
133 134 render_error l(:error_no_tracker_in_project)
134 135 return
135 136 end
136 137 if params[:issue].is_a?(Hash)
137 138 @issue.safe_attributes = params[:issue]
138 139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
139 140 end
140 141 @issue.author = User.current
141 142
142 143 default_status = IssueStatus.default
143 144 unless default_status
144 145 render_error l(:error_no_default_issue_status)
145 146 return
146 147 end
147 148 @issue.status = default_status
148 149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
149 150
150 151 if request.get? || request.xhr?
151 152 @issue.start_date ||= Date.today
152 153 else
153 154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
154 155 # Check that the user is allowed to apply the requested status
155 156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
156 157 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
157 158 if @issue.save
158 159 attach_files(@issue, params[:attachments])
159 160 flash[:notice] = l(:notice_successful_create)
160 161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 162 respond_to do |format|
162 163 format.html {
163 164 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
164 165 { :action => 'show', :id => @issue })
165 166 }
166 167 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
167 168 end
168 169 return
169 170 else
170 171 respond_to do |format|
171 172 format.html { }
172 173 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
173 174 end
174 175 end
175 176 end
176 177 @priorities = IssuePriority.all
177 178 render :layout => !request.xhr?
178 179 end
179 180
180 181 # Attributes that can be updated on workflow transition (without :edit permission)
181 182 # TODO: make it configurable (at least per role)
182 183 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
183 184
184 185 def edit
185 186 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
186 187 @priorities = IssuePriority.all
187 188 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
188 189 @time_entry = TimeEntry.new
189 190
190 191 @notes = params[:notes]
191 192 journal = @issue.init_journal(User.current, @notes)
192 193 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
193 194 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
194 195 attrs = params[:issue].dup
195 196 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
196 197 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
197 198 @issue.safe_attributes = attrs
198 199 end
199 200
200 201 if request.get?
201 202 # nop
202 203 else
203 204 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
204 205 @time_entry.attributes = params[:time_entry]
205 206 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
206 207 attachments = attach_files(@issue, params[:attachments])
207 208 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
208 209 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
209 210 if @issue.save
210 211 # Log spend time
211 212 if User.current.allowed_to?(:log_time, @project)
212 213 @time_entry.save
213 214 end
214 215 if !journal.new_record?
215 216 # Only send notification if something was actually changed
216 217 flash[:notice] = l(:notice_successful_update)
217 218 end
218 219 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
219 220 respond_to do |format|
220 221 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
221 222 format.xml { head :ok }
222 223 end
223 224 return
224 225 end
225 226 end
226 227 # failure
227 228 respond_to do |format|
228 229 format.html { }
229 230 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
230 231 end
231 232 end
232 233 rescue ActiveRecord::StaleObjectError
233 234 # Optimistic locking exception
234 235 flash.now[:error] = l(:notice_locking_conflict)
235 236 # Remove the previously added attachments if issue was not updated
236 237 attachments.each(&:destroy)
237 238 end
238 239
239 240 def reply
240 241 journal = Journal.find(params[:journal_id]) if params[:journal_id]
241 242 if journal
242 243 user = journal.user
243 244 text = journal.notes
244 245 else
245 246 user = @issue.author
246 247 text = @issue.description
247 248 end
248 249 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
249 250 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
250 251 render(:update) { |page|
251 252 page.<< "$('notes').value = \"#{content}\";"
252 253 page.show 'update'
253 254 page << "Form.Element.focus('notes');"
254 255 page << "Element.scrollTo('update');"
255 256 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
256 257 }
257 258 end
258 259
259 260 # Bulk edit a set of issues
260 261 def bulk_edit
261 262 if request.post?
262 263 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
263 264 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
264 265 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
265 266 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
266 267 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
267 268 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
268 269 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
269 270
270 271 unsaved_issue_ids = []
271 272 @issues.each do |issue|
272 273 journal = issue.init_journal(User.current, params[:notes])
273 274 issue.tracker = tracker if tracker
274 275 issue.priority = priority if priority
275 276 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
276 277 issue.category = category if category || params[:category_id] == 'none'
277 278 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
278 279 issue.start_date = params[:start_date] unless params[:start_date].blank?
279 280 issue.due_date = params[:due_date] unless params[:due_date].blank?
280 281 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
281 282 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
282 283 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
283 284 # Don't save any change to the issue if the user is not authorized to apply the requested status
284 285 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
285 286 # Keep unsaved issue ids to display them in flash error
286 287 unsaved_issue_ids << issue.id
287 288 end
288 289 end
289 290 if unsaved_issue_ids.empty?
290 291 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
291 292 else
292 293 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
293 294 :total => @issues.size,
294 295 :ids => '#' + unsaved_issue_ids.join(', #'))
295 296 end
296 297 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
297 298 return
298 299 end
299 300 @available_statuses = Workflow.available_statuses(@project)
300 301 @custom_fields = @project.all_issue_custom_fields
301 302 end
302 303
303 304 def move
304 305 @copy = params[:copy_options] && params[:copy_options][:copy]
305 306 @allowed_projects = []
306 307 # find projects to which the user is allowed to move the issue
307 308 if User.current.admin?
308 309 # admin is allowed to move issues to any active (visible) project
309 310 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
310 311 else
311 312 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
312 313 end
313 314 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
314 315 @target_project ||= @project
315 316 @trackers = @target_project.trackers
316 317 @available_statuses = Workflow.available_statuses(@project)
317 318 if request.post?
318 319 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
319 320 unsaved_issue_ids = []
320 321 moved_issues = []
321 322 @issues.each do |issue|
322 323 changed_attributes = {}
323 324 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
324 325 unless params[valid_attribute].blank?
325 326 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
326 327 end
327 328 end
328 329 issue.init_journal(User.current)
329 330 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
330 331 moved_issues << r
331 332 else
332 333 unsaved_issue_ids << issue.id
333 334 end
334 335 end
335 336 if unsaved_issue_ids.empty?
336 337 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
337 338 else
338 339 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
339 340 :total => @issues.size,
340 341 :ids => '#' + unsaved_issue_ids.join(', #'))
341 342 end
342 343 if params[:follow]
343 344 if @issues.size == 1 && moved_issues.size == 1
344 345 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
345 346 else
346 347 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
347 348 end
348 349 else
349 350 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
350 351 end
351 352 return
352 353 end
353 354 render :layout => false if request.xhr?
354 355 end
355 356
356 357 def destroy
357 358 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
358 359 if @hours > 0
359 360 case params[:todo]
360 361 when 'destroy'
361 362 # nothing to do
362 363 when 'nullify'
363 364 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
364 365 when 'reassign'
365 366 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
366 367 if reassign_to.nil?
367 368 flash.now[:error] = l(:error_issue_not_found_in_project)
368 369 return
369 370 else
370 371 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
371 372 end
372 373 else
373 374 unless params[:format] == 'xml'
374 375 # display the destroy form if it's a user request
375 376 return
376 377 end
377 378 end
378 379 end
379 380 @issues.each(&:destroy)
380 381 respond_to do |format|
381 382 format.html { redirect_to :action => 'index', :project_id => @project }
382 383 format.xml { head :ok }
383 384 end
384 385 end
385 386
386 387 def gantt
387 388 @gantt = Redmine::Helpers::Gantt.new(params)
388 389 retrieve_query
389 390 if @query.valid?
390 391 events = []
391 392 # Issues that have start and due dates
392 393 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
393 394 :order => "start_date, due_date",
394 395 :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]
395 396 )
396 397 # Issues that don't have a due date but that are assigned to a version with a date
397 398 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
398 399 :order => "start_date, effective_date",
399 400 :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]
400 401 )
401 402 # Versions
402 403 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
403 404
404 405 @gantt.events = events
405 406 end
406 407
407 408 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
408 409
409 410 respond_to do |format|
410 411 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
411 412 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
412 413 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
413 414 end
414 415 end
415 416
416 417 def calendar
417 418 if params[:year] and params[:year].to_i > 1900
418 419 @year = params[:year].to_i
419 420 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
420 421 @month = params[:month].to_i
421 422 end
422 423 end
423 424 @year ||= Date.today.year
424 425 @month ||= Date.today.month
425 426
426 427 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
427 428 retrieve_query
428 429 if @query.valid?
429 430 events = []
430 431 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
431 432 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
432 433 )
433 434 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
434 435
435 436 @calendar.events = events
436 437 end
437 438
438 439 render :layout => false if request.xhr?
439 440 end
440 441
441 442 def context_menu
442 443 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
443 444 if (@issues.size == 1)
444 445 @issue = @issues.first
445 446 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
446 447 end
447 448 projects = @issues.collect(&:project).compact.uniq
448 449 @project = projects.first if projects.size == 1
449 450
450 451 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
451 452 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
452 453 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
453 454 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
454 455 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
455 456 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
456 457 }
457 458 if @project
458 459 @assignables = @project.assignable_users
459 460 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
460 461 @trackers = @project.trackers
461 462 end
462 463
463 464 @priorities = IssuePriority.all.reverse
464 465 @statuses = IssueStatus.find(:all, :order => 'position')
465 466 @back = params[:back_url] || request.env['HTTP_REFERER']
466 467
467 468 render :layout => false
468 469 end
469 470
470 471 def update_form
471 472 if params[:id].blank?
472 473 @issue = Issue.new
473 474 @issue.project = @project
474 475 else
475 476 @issue = @project.issues.visible.find(params[:id])
476 477 end
477 478 @issue.attributes = params[:issue]
478 479 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
479 480 @priorities = IssuePriority.all
480 481
481 482 render :partial => 'attributes'
482 483 end
483 484
484 485 def preview
485 486 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
486 487 @attachements = @issue.attachments if @issue
487 488 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
488 489 render :partial => 'common/preview'
489 490 end
490 491
491 492 private
492 493 def find_issue
493 494 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
494 495 @project = @issue.project
495 496 rescue ActiveRecord::RecordNotFound
496 497 render_404
497 498 end
498 499
499 500 # Filter for bulk operations
500 501 def find_issues
501 502 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
502 503 raise ActiveRecord::RecordNotFound if @issues.empty?
503 504 projects = @issues.collect(&:project).compact.uniq
504 505 if projects.size == 1
505 506 @project = projects.first
506 507 else
507 508 # TODO: let users bulk edit/move/destroy issues from different projects
508 509 render_error 'Can not bulk edit/move/destroy issues from different projects'
509 510 return false
510 511 end
511 512 rescue ActiveRecord::RecordNotFound
512 513 render_404
513 514 end
514 515
515 516 def find_project
516 517 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
517 518 @project = Project.find(project_id)
518 519 rescue ActiveRecord::RecordNotFound
519 520 render_404
520 521 end
521 522
522 523 def find_optional_project
523 524 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
524 525 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
525 526 allowed ? true : deny_access
526 527 rescue ActiveRecord::RecordNotFound
527 528 render_404
528 529 end
529 530
530 531 # Retrieve query from session or build a new query
531 532 def retrieve_query
532 533 if !params[:query_id].blank?
533 534 cond = "project_id IS NULL"
534 535 cond << " OR project_id = #{@project.id}" if @project
535 536 @query = Query.find(params[:query_id], :conditions => cond)
536 537 @query.project = @project
537 538 session[:query] = {:id => @query.id, :project_id => @query.project_id}
538 539 sort_clear
539 540 else
540 541 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
541 542 # Give it a name, required to be valid
542 543 @query = Query.new(:name => "_")
543 544 @query.project = @project
544 545 if params[:fields] and params[:fields].is_a? Array
545 546 params[:fields].each do |field|
546 547 @query.add_filter(field, params[:operators][field], params[:values][field])
547 548 end
548 549 else
549 550 @query.available_filters.keys.each do |field|
550 551 @query.add_short_filter(field, params[field]) if params[field]
551 552 end
552 553 end
553 554 @query.group_by = params[:group_by]
554 555 @query.column_names = params[:query] && params[:query][:column_names]
555 556 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
556 557 else
557 558 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
558 559 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
559 560 @query.project = @project
560 561 end
561 562 end
562 563 end
563 564
564 565 # Rescues an invalid query statement. Just in case...
565 566 def query_statement_invalid(exception)
566 567 logger.error "Query::StatementInvalid: #{exception.message}" if logger
567 568 session.delete(:query)
568 569 sort_clear
569 570 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
570 571 end
571 572 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,1337 +1,1345
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_post_new_should_ignore_non_safe_attributes
645 653 @request.session[:user_id] = 2
646 654 assert_nothing_raised do
647 655 post :new, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
648 656 end
649 657 end
650 658
651 659 def test_copy_routing
652 660 assert_routing(
653 661 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
654 662 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
655 663 )
656 664 end
657 665
658 666 def test_copy_issue
659 667 @request.session[:user_id] = 2
660 668 get :new, :project_id => 1, :copy_from => 1
661 669 assert_template 'new'
662 670 assert_not_nil assigns(:issue)
663 671 orig = Issue.find(1)
664 672 assert_equal orig.subject, assigns(:issue).subject
665 673 end
666 674
667 675 def test_edit_routing
668 676 assert_routing(
669 677 {:method => :get, :path => '/issues/1/edit'},
670 678 :controller => 'issues', :action => 'edit', :id => '1'
671 679 )
672 680 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
673 681 {:controller => 'issues', :action => 'edit', :id => '1'},
674 682 {:method => :post, :path => '/issues/1/edit'}
675 683 )
676 684 end
677 685
678 686 def test_get_edit
679 687 @request.session[:user_id] = 2
680 688 get :edit, :id => 1
681 689 assert_response :success
682 690 assert_template 'edit'
683 691 assert_not_nil assigns(:issue)
684 692 assert_equal Issue.find(1), assigns(:issue)
685 693 end
686 694
687 695 def test_get_edit_with_params
688 696 @request.session[:user_id] = 2
689 697 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
690 698 assert_response :success
691 699 assert_template 'edit'
692 700
693 701 issue = assigns(:issue)
694 702 assert_not_nil issue
695 703
696 704 assert_equal 5, issue.status_id
697 705 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
698 706 :child => { :tag => 'option',
699 707 :content => 'Closed',
700 708 :attributes => { :selected => 'selected' } }
701 709
702 710 assert_equal 7, issue.priority_id
703 711 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
704 712 :child => { :tag => 'option',
705 713 :content => 'Urgent',
706 714 :attributes => { :selected => 'selected' } }
707 715 end
708 716
709 717 def test_update_edit_form
710 718 @request.session[:user_id] = 2
711 719 xhr :post, :update_form, :project_id => 1,
712 720 :id => 1,
713 721 :issue => {:tracker_id => 2,
714 722 :subject => 'This is the test_new issue',
715 723 :description => 'This is the description',
716 724 :priority_id => 5}
717 725 assert_response :success
718 726 assert_template 'attributes'
719 727
720 728 issue = assigns(:issue)
721 729 assert_kind_of Issue, issue
722 730 assert_equal 1, issue.id
723 731 assert_equal 1, issue.project_id
724 732 assert_equal 2, issue.tracker_id
725 733 assert_equal 'This is the test_new issue', issue.subject
726 734 end
727 735
728 736 def test_reply_routing
729 737 assert_routing(
730 738 {:method => :post, :path => '/issues/1/quoted'},
731 739 :controller => 'issues', :action => 'reply', :id => '1'
732 740 )
733 741 end
734 742
735 743 def test_reply_to_issue
736 744 @request.session[:user_id] = 2
737 745 get :reply, :id => 1
738 746 assert_response :success
739 747 assert_select_rjs :show, "update"
740 748 end
741 749
742 750 def test_reply_to_note
743 751 @request.session[:user_id] = 2
744 752 get :reply, :id => 1, :journal_id => 2
745 753 assert_response :success
746 754 assert_select_rjs :show, "update"
747 755 end
748 756
749 757 def test_post_edit_without_custom_fields_param
750 758 @request.session[:user_id] = 2
751 759 ActionMailer::Base.deliveries.clear
752 760
753 761 issue = Issue.find(1)
754 762 assert_equal '125', issue.custom_value_for(2).value
755 763 old_subject = issue.subject
756 764 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
757 765
758 766 assert_difference('Journal.count') do
759 767 assert_difference('JournalDetail.count', 2) do
760 768 post :edit, :id => 1, :issue => {:subject => new_subject,
761 769 :priority_id => '6',
762 770 :category_id => '1' # no change
763 771 }
764 772 end
765 773 end
766 774 assert_redirected_to :action => 'show', :id => '1'
767 775 issue.reload
768 776 assert_equal new_subject, issue.subject
769 777 # Make sure custom fields were not cleared
770 778 assert_equal '125', issue.custom_value_for(2).value
771 779
772 780 mail = ActionMailer::Base.deliveries.last
773 781 assert_kind_of TMail::Mail, mail
774 782 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
775 783 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
776 784 end
777 785
778 786 def test_post_edit_with_custom_field_change
779 787 @request.session[:user_id] = 2
780 788 issue = Issue.find(1)
781 789 assert_equal '125', issue.custom_value_for(2).value
782 790
783 791 assert_difference('Journal.count') do
784 792 assert_difference('JournalDetail.count', 3) do
785 793 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
786 794 :priority_id => '6',
787 795 :category_id => '1', # no change
788 796 :custom_field_values => { '2' => 'New custom value' }
789 797 }
790 798 end
791 799 end
792 800 assert_redirected_to :action => 'show', :id => '1'
793 801 issue.reload
794 802 assert_equal 'New custom value', issue.custom_value_for(2).value
795 803
796 804 mail = ActionMailer::Base.deliveries.last
797 805 assert_kind_of TMail::Mail, mail
798 806 assert mail.body.include?("Searchable field changed from 125 to New custom value")
799 807 end
800 808
801 809 def test_post_edit_with_status_and_assignee_change
802 810 issue = Issue.find(1)
803 811 assert_equal 1, issue.status_id
804 812 @request.session[:user_id] = 2
805 813 assert_difference('TimeEntry.count', 0) do
806 814 post :edit,
807 815 :id => 1,
808 816 :issue => { :status_id => 2, :assigned_to_id => 3 },
809 817 :notes => 'Assigned to dlopper',
810 818 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
811 819 end
812 820 assert_redirected_to :action => 'show', :id => '1'
813 821 issue.reload
814 822 assert_equal 2, issue.status_id
815 823 j = Journal.find(:first, :order => 'id DESC')
816 824 assert_equal 'Assigned to dlopper', j.notes
817 825 assert_equal 2, j.details.size
818 826
819 827 mail = ActionMailer::Base.deliveries.last
820 828 assert mail.body.include?("Status changed from New to Assigned")
821 829 # subject should contain the new status
822 830 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
823 831 end
824 832
825 833 def test_post_edit_with_note_only
826 834 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
827 835 # anonymous user
828 836 post :edit,
829 837 :id => 1,
830 838 :notes => notes
831 839 assert_redirected_to :action => 'show', :id => '1'
832 840 j = Journal.find(:first, :order => 'id DESC')
833 841 assert_equal notes, j.notes
834 842 assert_equal 0, j.details.size
835 843 assert_equal User.anonymous, j.user
836 844
837 845 mail = ActionMailer::Base.deliveries.last
838 846 assert mail.body.include?(notes)
839 847 end
840 848
841 849 def test_post_edit_with_note_and_spent_time
842 850 @request.session[:user_id] = 2
843 851 spent_hours_before = Issue.find(1).spent_hours
844 852 assert_difference('TimeEntry.count') do
845 853 post :edit,
846 854 :id => 1,
847 855 :notes => '2.5 hours added',
848 856 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
849 857 end
850 858 assert_redirected_to :action => 'show', :id => '1'
851 859
852 860 issue = Issue.find(1)
853 861
854 862 j = Journal.find(:first, :order => 'id DESC')
855 863 assert_equal '2.5 hours added', j.notes
856 864 assert_equal 0, j.details.size
857 865
858 866 t = issue.time_entries.find(:first, :order => 'id DESC')
859 867 assert_not_nil t
860 868 assert_equal 2.5, t.hours
861 869 assert_equal spent_hours_before + 2.5, issue.spent_hours
862 870 end
863 871
864 872 def test_post_edit_with_attachment_only
865 873 set_tmp_attachments_directory
866 874
867 875 # Delete all fixtured journals, a race condition can occur causing the wrong
868 876 # journal to get fetched in the next find.
869 877 Journal.delete_all
870 878
871 879 # anonymous user
872 880 post :edit,
873 881 :id => 1,
874 882 :notes => '',
875 883 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
876 884 assert_redirected_to :action => 'show', :id => '1'
877 885 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
878 886 assert j.notes.blank?
879 887 assert_equal 1, j.details.size
880 888 assert_equal 'testfile.txt', j.details.first.value
881 889 assert_equal User.anonymous, j.user
882 890
883 891 mail = ActionMailer::Base.deliveries.last
884 892 assert mail.body.include?('testfile.txt')
885 893 end
886 894
887 895 def test_post_edit_with_no_change
888 896 issue = Issue.find(1)
889 897 issue.journals.clear
890 898 ActionMailer::Base.deliveries.clear
891 899
892 900 post :edit,
893 901 :id => 1,
894 902 :notes => ''
895 903 assert_redirected_to :action => 'show', :id => '1'
896 904
897 905 issue.reload
898 906 assert issue.journals.empty?
899 907 # No email should be sent
900 908 assert ActionMailer::Base.deliveries.empty?
901 909 end
902 910
903 911 def test_post_edit_should_send_a_notification
904 912 @request.session[:user_id] = 2
905 913 ActionMailer::Base.deliveries.clear
906 914 issue = Issue.find(1)
907 915 old_subject = issue.subject
908 916 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
909 917
910 918 post :edit, :id => 1, :issue => {:subject => new_subject,
911 919 :priority_id => '6',
912 920 :category_id => '1' # no change
913 921 }
914 922 assert_equal 1, ActionMailer::Base.deliveries.size
915 923 end
916 924
917 925 def test_post_edit_with_invalid_spent_time
918 926 @request.session[:user_id] = 2
919 927 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
920 928
921 929 assert_no_difference('Journal.count') do
922 930 post :edit,
923 931 :id => 1,
924 932 :notes => notes,
925 933 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
926 934 end
927 935 assert_response :success
928 936 assert_template 'edit'
929 937
930 938 assert_tag :textarea, :attributes => { :name => 'notes' },
931 939 :content => notes
932 940 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
933 941 end
934 942
935 943 def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
936 944 issue = Issue.find(2)
937 945 @request.session[:user_id] = 2
938 946
939 947 post :edit,
940 948 :id => issue.id,
941 949 :issue => {
942 950 :fixed_version_id => 4
943 951 }
944 952
945 953 assert_response :redirect
946 954 issue.reload
947 955 assert_equal 4, issue.fixed_version_id
948 956 assert_not_equal issue.project_id, issue.fixed_version.project_id
949 957 end
950 958
951 959 def test_post_edit_should_redirect_back_using_the_back_url_parameter
952 960 issue = Issue.find(2)
953 961 @request.session[:user_id] = 2
954 962
955 963 post :edit,
956 964 :id => issue.id,
957 965 :issue => {
958 966 :fixed_version_id => 4
959 967 },
960 968 :back_url => '/issues'
961 969
962 970 assert_response :redirect
963 971 assert_redirected_to '/issues'
964 972 end
965 973
966 974 def test_post_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
967 975 issue = Issue.find(2)
968 976 @request.session[:user_id] = 2
969 977
970 978 post :edit,
971 979 :id => issue.id,
972 980 :issue => {
973 981 :fixed_version_id => 4
974 982 },
975 983 :back_url => 'http://google.com'
976 984
977 985 assert_response :redirect
978 986 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
979 987 end
980 988
981 989 def test_get_bulk_edit
982 990 @request.session[:user_id] = 2
983 991 get :bulk_edit, :ids => [1, 2]
984 992 assert_response :success
985 993 assert_template 'bulk_edit'
986 994
987 995 # Project specific custom field, date type
988 996 field = CustomField.find(9)
989 997 assert !field.is_for_all?
990 998 assert_equal 'date', field.field_format
991 999 assert_tag :input, :attributes => {:name => 'custom_field_values[9]'}
992 1000
993 1001 # System wide custom field
994 1002 assert CustomField.find(1).is_for_all?
995 1003 assert_tag :select, :attributes => {:name => 'custom_field_values[1]'}
996 1004 end
997 1005
998 1006 def test_bulk_edit
999 1007 @request.session[:user_id] = 2
1000 1008 # update issues priority
1001 1009 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
1002 1010 :assigned_to_id => '',
1003 1011 :custom_field_values => {'2' => ''},
1004 1012 :notes => 'Bulk editing'
1005 1013 assert_response 302
1006 1014 # check that the issues were updated
1007 1015 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1008 1016
1009 1017 issue = Issue.find(1)
1010 1018 journal = issue.journals.find(:first, :order => 'created_on DESC')
1011 1019 assert_equal '125', issue.custom_value_for(2).value
1012 1020 assert_equal 'Bulk editing', journal.notes
1013 1021 assert_equal 1, journal.details.size
1014 1022 end
1015 1023
1016 1024 def test_bullk_edit_should_send_a_notification
1017 1025 @request.session[:user_id] = 2
1018 1026 ActionMailer::Base.deliveries.clear
1019 1027 post(:bulk_edit,
1020 1028 {
1021 1029 :ids => [1, 2],
1022 1030 :priority_id => 7,
1023 1031 :assigned_to_id => '',
1024 1032 :custom_field_values => {'2' => ''},
1025 1033 :notes => 'Bulk editing'
1026 1034 })
1027 1035
1028 1036 assert_response 302
1029 1037 assert_equal 2, ActionMailer::Base.deliveries.size
1030 1038 end
1031 1039
1032 1040 def test_bulk_edit_status
1033 1041 @request.session[:user_id] = 2
1034 1042 # update issues priority
1035 1043 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1036 1044 :assigned_to_id => '',
1037 1045 :status_id => '5',
1038 1046 :notes => 'Bulk editing status'
1039 1047 assert_response 302
1040 1048 issue = Issue.find(1)
1041 1049 assert issue.closed?
1042 1050 end
1043 1051
1044 1052 def test_bulk_edit_custom_field
1045 1053 @request.session[:user_id] = 2
1046 1054 # update issues priority
1047 1055 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1048 1056 :assigned_to_id => '',
1049 1057 :custom_field_values => {'2' => '777'},
1050 1058 :notes => 'Bulk editing custom field'
1051 1059 assert_response 302
1052 1060
1053 1061 issue = Issue.find(1)
1054 1062 journal = issue.journals.find(:first, :order => 'created_on DESC')
1055 1063 assert_equal '777', issue.custom_value_for(2).value
1056 1064 assert_equal 1, journal.details.size
1057 1065 assert_equal '125', journal.details.first.old_value
1058 1066 assert_equal '777', journal.details.first.value
1059 1067 end
1060 1068
1061 1069 def test_bulk_unassign
1062 1070 assert_not_nil Issue.find(2).assigned_to
1063 1071 @request.session[:user_id] = 2
1064 1072 # unassign issues
1065 1073 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1066 1074 assert_response 302
1067 1075 # check that the issues were updated
1068 1076 assert_nil Issue.find(2).assigned_to
1069 1077 end
1070 1078
1071 1079 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
1072 1080 @request.session[:user_id] = 2
1073 1081
1074 1082 post :bulk_edit,
1075 1083 :ids => [1,2],
1076 1084 :fixed_version_id => 4
1077 1085
1078 1086 assert_response :redirect
1079 1087 issues = Issue.find([1,2])
1080 1088 issues.each do |issue|
1081 1089 assert_equal 4, issue.fixed_version_id
1082 1090 assert_not_equal issue.project_id, issue.fixed_version.project_id
1083 1091 end
1084 1092 end
1085 1093
1086 1094 def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter
1087 1095 @request.session[:user_id] = 2
1088 1096 post :bulk_edit, :ids => [1,2], :back_url => '/issues'
1089 1097
1090 1098 assert_response :redirect
1091 1099 assert_redirected_to '/issues'
1092 1100 end
1093 1101
1094 1102 def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1095 1103 @request.session[:user_id] = 2
1096 1104 post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com'
1097 1105
1098 1106 assert_response :redirect
1099 1107 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1100 1108 end
1101 1109
1102 1110 def test_move_routing
1103 1111 assert_routing(
1104 1112 {:method => :get, :path => '/issues/1/move'},
1105 1113 :controller => 'issues', :action => 'move', :id => '1'
1106 1114 )
1107 1115 assert_recognizes(
1108 1116 {:controller => 'issues', :action => 'move', :id => '1'},
1109 1117 {:method => :post, :path => '/issues/1/move'}
1110 1118 )
1111 1119 end
1112 1120
1113 1121 def test_move_one_issue_to_another_project
1114 1122 @request.session[:user_id] = 2
1115 1123 post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1116 1124 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1117 1125 assert_equal 2, Issue.find(1).project_id
1118 1126 end
1119 1127
1120 1128 def test_move_one_issue_to_another_project_should_follow_when_needed
1121 1129 @request.session[:user_id] = 2
1122 1130 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1123 1131 assert_redirected_to '/issues/1'
1124 1132 end
1125 1133
1126 1134 def test_bulk_move_to_another_project
1127 1135 @request.session[:user_id] = 2
1128 1136 post :move, :ids => [1, 2], :new_project_id => 2
1129 1137 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1130 1138 # Issues moved to project 2
1131 1139 assert_equal 2, Issue.find(1).project_id
1132 1140 assert_equal 2, Issue.find(2).project_id
1133 1141 # No tracker change
1134 1142 assert_equal 1, Issue.find(1).tracker_id
1135 1143 assert_equal 2, Issue.find(2).tracker_id
1136 1144 end
1137 1145
1138 1146 def test_bulk_move_to_another_tracker
1139 1147 @request.session[:user_id] = 2
1140 1148 post :move, :ids => [1, 2], :new_tracker_id => 2
1141 1149 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1142 1150 assert_equal 2, Issue.find(1).tracker_id
1143 1151 assert_equal 2, Issue.find(2).tracker_id
1144 1152 end
1145 1153
1146 1154 def test_bulk_copy_to_another_project
1147 1155 @request.session[:user_id] = 2
1148 1156 assert_difference 'Issue.count', 2 do
1149 1157 assert_no_difference 'Project.find(1).issues.count' do
1150 1158 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1151 1159 end
1152 1160 end
1153 1161 assert_redirected_to 'projects/ecookbook/issues'
1154 1162 end
1155 1163
1156 1164 context "#move via bulk copy" do
1157 1165 should "allow not changing the issue's attributes" do
1158 1166 @request.session[:user_id] = 2
1159 1167 issue_before_move = Issue.find(1)
1160 1168 assert_difference 'Issue.count', 1 do
1161 1169 assert_no_difference 'Project.find(1).issues.count' do
1162 1170 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1163 1171 end
1164 1172 end
1165 1173 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
1166 1174 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
1167 1175 assert_equal issue_before_move.status_id, issue_after_move.status_id
1168 1176 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
1169 1177 end
1170 1178
1171 1179 should "allow changing the issue's attributes" do
1172 1180 @request.session[:user_id] = 2
1173 1181 assert_difference 'Issue.count', 2 do
1174 1182 assert_no_difference 'Project.find(1).issues.count' do
1175 1183 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'
1176 1184 end
1177 1185 end
1178 1186
1179 1187 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1180 1188 assert_equal 2, copied_issues.size
1181 1189 copied_issues.each do |issue|
1182 1190 assert_equal 2, issue.project_id, "Project is incorrect"
1183 1191 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1184 1192 assert_equal 3, issue.status_id, "Status is incorrect"
1185 1193 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1186 1194 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1187 1195 end
1188 1196 end
1189 1197 end
1190 1198
1191 1199 def test_copy_to_another_project_should_follow_when_needed
1192 1200 @request.session[:user_id] = 2
1193 1201 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1194 1202 issue = Issue.first(:order => 'id DESC')
1195 1203 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1196 1204 end
1197 1205
1198 1206 def test_context_menu_one_issue
1199 1207 @request.session[:user_id] = 2
1200 1208 get :context_menu, :ids => [1]
1201 1209 assert_response :success
1202 1210 assert_template 'context_menu'
1203 1211 assert_tag :tag => 'a', :content => 'Edit',
1204 1212 :attributes => { :href => '/issues/1/edit',
1205 1213 :class => 'icon-edit' }
1206 1214 assert_tag :tag => 'a', :content => 'Closed',
1207 1215 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1208 1216 :class => '' }
1209 1217 assert_tag :tag => 'a', :content => 'Immediate',
1210 1218 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1211 1219 :class => '' }
1212 1220 # Versions
1213 1221 assert_tag :tag => 'a', :content => '2.0',
1214 1222 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&amp;ids%5B%5D=1',
1215 1223 :class => '' }
1216 1224 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1217 1225 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
1218 1226 :class => '' }
1219 1227
1220 1228 assert_tag :tag => 'a', :content => 'Dave Lopper',
1221 1229 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1222 1230 :class => '' }
1223 1231 assert_tag :tag => 'a', :content => 'Duplicate',
1224 1232 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1225 1233 :class => 'icon-duplicate' }
1226 1234 assert_tag :tag => 'a', :content => 'Copy',
1227 1235 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
1228 1236 :class => 'icon-copy' }
1229 1237 assert_tag :tag => 'a', :content => 'Move',
1230 1238 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1231 1239 :class => 'icon-move' }
1232 1240 assert_tag :tag => 'a', :content => 'Delete',
1233 1241 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1234 1242 :class => 'icon-del' }
1235 1243 end
1236 1244
1237 1245 def test_context_menu_one_issue_by_anonymous
1238 1246 get :context_menu, :ids => [1]
1239 1247 assert_response :success
1240 1248 assert_template 'context_menu'
1241 1249 assert_tag :tag => 'a', :content => 'Delete',
1242 1250 :attributes => { :href => '#',
1243 1251 :class => 'icon-del disabled' }
1244 1252 end
1245 1253
1246 1254 def test_context_menu_multiple_issues_of_same_project
1247 1255 @request.session[:user_id] = 2
1248 1256 get :context_menu, :ids => [1, 2]
1249 1257 assert_response :success
1250 1258 assert_template 'context_menu'
1251 1259 assert_tag :tag => 'a', :content => 'Edit',
1252 1260 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1253 1261 :class => 'icon-edit' }
1254 1262 assert_tag :tag => 'a', :content => 'Immediate',
1255 1263 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1256 1264 :class => '' }
1257 1265 assert_tag :tag => 'a', :content => 'Dave Lopper',
1258 1266 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1259 1267 :class => '' }
1260 1268 assert_tag :tag => 'a', :content => 'Copy',
1261 1269 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1262 1270 :class => 'icon-copy' }
1263 1271 assert_tag :tag => 'a', :content => 'Move',
1264 1272 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1265 1273 :class => 'icon-move' }
1266 1274 assert_tag :tag => 'a', :content => 'Delete',
1267 1275 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1268 1276 :class => 'icon-del' }
1269 1277 end
1270 1278
1271 1279 def test_context_menu_multiple_issues_of_different_project
1272 1280 @request.session[:user_id] = 2
1273 1281 get :context_menu, :ids => [1, 2, 4]
1274 1282 assert_response :success
1275 1283 assert_template 'context_menu'
1276 1284 assert_tag :tag => 'a', :content => 'Delete',
1277 1285 :attributes => { :href => '#',
1278 1286 :class => 'icon-del disabled' }
1279 1287 end
1280 1288
1281 1289 def test_destroy_routing
1282 1290 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1283 1291 {:controller => 'issues', :action => 'destroy', :id => '1'},
1284 1292 {:method => :post, :path => '/issues/1/destroy'}
1285 1293 )
1286 1294 end
1287 1295
1288 1296 def test_destroy_issue_with_no_time_entries
1289 1297 assert_nil TimeEntry.find_by_issue_id(2)
1290 1298 @request.session[:user_id] = 2
1291 1299 post :destroy, :id => 2
1292 1300 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1293 1301 assert_nil Issue.find_by_id(2)
1294 1302 end
1295 1303
1296 1304 def test_destroy_issues_with_time_entries
1297 1305 @request.session[:user_id] = 2
1298 1306 post :destroy, :ids => [1, 3]
1299 1307 assert_response :success
1300 1308 assert_template 'destroy'
1301 1309 assert_not_nil assigns(:hours)
1302 1310 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1303 1311 end
1304 1312
1305 1313 def test_destroy_issues_and_destroy_time_entries
1306 1314 @request.session[:user_id] = 2
1307 1315 post :destroy, :ids => [1, 3], :todo => 'destroy'
1308 1316 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1309 1317 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1310 1318 assert_nil TimeEntry.find_by_id([1, 2])
1311 1319 end
1312 1320
1313 1321 def test_destroy_issues_and_assign_time_entries_to_project
1314 1322 @request.session[:user_id] = 2
1315 1323 post :destroy, :ids => [1, 3], :todo => 'nullify'
1316 1324 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1317 1325 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1318 1326 assert_nil TimeEntry.find(1).issue_id
1319 1327 assert_nil TimeEntry.find(2).issue_id
1320 1328 end
1321 1329
1322 1330 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1323 1331 @request.session[:user_id] = 2
1324 1332 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1325 1333 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1326 1334 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1327 1335 assert_equal 2, TimeEntry.find(1).issue_id
1328 1336 assert_equal 2, TimeEntry.find(2).issue_id
1329 1337 end
1330 1338
1331 1339 def test_default_search_scope
1332 1340 get :index
1333 1341 assert_tag :div, :attributes => {:id => 'quick-search'},
1334 1342 :child => {:tag => 'form',
1335 1343 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1336 1344 end
1337 1345 end
General Comments 0
You need to be logged in to leave comments. Login now