##// END OF EJS Templates
Fixes progress calculation on gantt (#7838)....
Jean-Philippe Lang -
r5116:86eed08fbf49
parent child
Show More
@@ -1,860 +1,860
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 module Redmine
19 19 module Helpers
20 20 # Simple class to handle gantt chart data
21 21 class Gantt
22 22 include ERB::Util
23 23 include Redmine::I18n
24 24
25 25 # :nodoc:
26 26 # Some utility methods for the PDF export
27 27 class PDF
28 28 MaxCharactorsForSubject = 45
29 29 TotalWidth = 280
30 30 LeftPaneWidth = 100
31 31
32 32 def self.right_pane_width
33 33 TotalWidth - LeftPaneWidth
34 34 end
35 35 end
36 36
37 37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
38 38 attr_accessor :query
39 39 attr_accessor :project
40 40 attr_accessor :view
41 41
42 42 def initialize(options={})
43 43 options = options.dup
44 44
45 45 if options[:year] && options[:year].to_i >0
46 46 @year_from = options[:year].to_i
47 47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
48 48 @month_from = options[:month].to_i
49 49 else
50 50 @month_from = 1
51 51 end
52 52 else
53 53 @month_from ||= Date.today.month
54 54 @year_from ||= Date.today.year
55 55 end
56 56
57 57 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
58 58 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
59 59 months = (options[:months] || User.current.pref[:gantt_months]).to_i
60 60 @months = (months > 0 && months < 25) ? months : 6
61 61
62 62 # Save gantt parameters as user preference (zoom and months count)
63 63 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
64 64 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
65 65 User.current.preference.save
66 66 end
67 67
68 68 @date_from = Date.civil(@year_from, @month_from, 1)
69 69 @date_to = (@date_from >> @months) - 1
70 70
71 71 @subjects = ''
72 72 @lines = ''
73 73 @number_of_rows = nil
74 74
75 75 @issue_ancestors = []
76 76
77 77 @truncated = false
78 78 if options.has_key?(:max_rows)
79 79 @max_rows = options[:max_rows]
80 80 else
81 81 @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
82 82 end
83 83 end
84 84
85 85 def common_params
86 86 { :controller => 'gantts', :action => 'show', :project_id => @project }
87 87 end
88 88
89 89 def params
90 90 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
91 91 end
92 92
93 93 def params_previous
94 94 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
95 95 end
96 96
97 97 def params_next
98 98 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
99 99 end
100 100
101 101 # Returns the number of rows that will be rendered on the Gantt chart
102 102 def number_of_rows
103 103 return @number_of_rows if @number_of_rows
104 104
105 105 rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
106 106 rows > @max_rows ? @max_rows : rows
107 107 end
108 108
109 109 # Returns the number of rows that will be used to list a project on
110 110 # the Gantt chart. This will recurse for each subproject.
111 111 def number_of_rows_on_project(project)
112 112 return 0 unless projects.include?(project)
113 113
114 114 count = 1
115 115 count += project_issues(project).size
116 116 count += project_versions(project).size
117 117 count
118 118 end
119 119
120 120 # Renders the subjects of the Gantt chart, the left side.
121 121 def subjects(options={})
122 122 render(options.merge(:only => :subjects)) unless @subjects_rendered
123 123 @subjects
124 124 end
125 125
126 126 # Renders the lines of the Gantt chart, the right side
127 127 def lines(options={})
128 128 render(options.merge(:only => :lines)) unless @lines_rendered
129 129 @lines
130 130 end
131 131
132 132 # Returns issues that will be rendered
133 133 def issues
134 134 @issues ||= @query.issues(
135 135 :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
136 136 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
137 137 :limit => @max_rows
138 138 )
139 139 end
140 140
141 141 # Return all the project nodes that will be displayed
142 142 def projects
143 143 return @projects if @projects
144 144
145 145 ids = issues.collect(&:project).uniq.collect(&:id)
146 146 if ids.any?
147 147 # All issues projects and their visible ancestors
148 148 @projects = Project.visible.all(
149 149 :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt",
150 150 :conditions => ["child.id IN (?)", ids],
151 151 :order => "#{Project.table_name}.lft ASC"
152 152 ).uniq
153 153 else
154 154 @projects = []
155 155 end
156 156 end
157 157
158 158 # Returns the issues that belong to +project+
159 159 def project_issues(project)
160 160 @issues_by_project ||= issues.group_by(&:project)
161 161 @issues_by_project[project] || []
162 162 end
163 163
164 164 # Returns the distinct versions of the issues that belong to +project+
165 165 def project_versions(project)
166 166 project_issues(project).collect(&:fixed_version).compact.uniq
167 167 end
168 168
169 169 # Returns the issues that belong to +project+ and are assigned to +version+
170 170 def version_issues(project, version)
171 171 project_issues(project).select {|issue| issue.fixed_version == version}
172 172 end
173 173
174 174 def render(options={})
175 175 options = {:top => 0, :top_increment => 20, :indent_increment => 20, :render => :subject, :format => :html}.merge(options)
176 176 indent = options[:indent] || 4
177 177
178 178 @subjects = '' unless options[:only] == :lines
179 179 @lines = '' unless options[:only] == :subjects
180 180 @number_of_rows = 0
181 181
182 182 Project.project_tree(projects) do |project, level|
183 183 options[:indent] = indent + level * options[:indent_increment]
184 184 render_project(project, options)
185 185 break if abort?
186 186 end
187 187
188 188 @subjects_rendered = true unless options[:only] == :lines
189 189 @lines_rendered = true unless options[:only] == :subjects
190 190
191 191 render_end(options)
192 192 end
193 193
194 194 def render_project(project, options={})
195 195 subject_for_project(project, options) unless options[:only] == :lines
196 196 line_for_project(project, options) unless options[:only] == :subjects
197 197
198 198 options[:top] += options[:top_increment]
199 199 options[:indent] += options[:indent_increment]
200 200 @number_of_rows += 1
201 201 return if abort?
202 202
203 203 issues = project_issues(project).select {|i| i.fixed_version.nil?}
204 204 sort_issues!(issues)
205 205 if issues
206 206 render_issues(issues, options)
207 207 return if abort?
208 208 end
209 209
210 210 versions = project_versions(project)
211 211 versions.each do |version|
212 212 render_version(project, version, options)
213 213 end
214 214
215 215 # Remove indent to hit the next sibling
216 216 options[:indent] -= options[:indent_increment]
217 217 end
218 218
219 219 def render_issues(issues, options={})
220 220 @issue_ancestors = []
221 221
222 222 issues.each do |i|
223 223 subject_for_issue(i, options) unless options[:only] == :lines
224 224 line_for_issue(i, options) unless options[:only] == :subjects
225 225
226 226 options[:top] += options[:top_increment]
227 227 @number_of_rows += 1
228 228 break if abort?
229 229 end
230 230
231 231 options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
232 232 end
233 233
234 234 def render_version(project, version, options={})
235 235 # Version header
236 236 subject_for_version(version, options) unless options[:only] == :lines
237 237 line_for_version(version, options) unless options[:only] == :subjects
238 238
239 239 options[:top] += options[:top_increment]
240 240 @number_of_rows += 1
241 241 return if abort?
242 242
243 243 issues = version_issues(project, version)
244 244 if issues
245 245 sort_issues!(issues)
246 246 # Indent issues
247 247 options[:indent] += options[:indent_increment]
248 248 render_issues(issues, options)
249 249 options[:indent] -= options[:indent_increment]
250 250 end
251 251 end
252 252
253 253 def render_end(options={})
254 254 case options[:format]
255 255 when :pdf
256 256 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
257 257 end
258 258 end
259 259
260 260 def subject_for_project(project, options)
261 261 case options[:format]
262 262 when :html
263 263 subject = "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
264 264 subject << view.link_to_project(project)
265 265 subject << '</span>'
266 266 html_subject(options, subject, :css => "project-name")
267 267 when :image
268 268 image_subject(options, project.name)
269 269 when :pdf
270 270 pdf_new_page?(options)
271 271 pdf_subject(options, project.name)
272 272 end
273 273 end
274 274
275 275 def line_for_project(project, options)
276 276 # Skip versions that don't have a start_date or due date
277 277 if project.is_a?(Project) && project.start_date && project.due_date
278 278 options[:zoom] ||= 1
279 279 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
280 280
281 281 coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
282 282 label = h(project)
283 283
284 284 case options[:format]
285 285 when :html
286 286 html_task(options, coords, :css => "project task", :label => label, :markers => true)
287 287 when :image
288 288 image_task(options, coords, :label => label, :markers => true, :height => 3)
289 289 when :pdf
290 290 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
291 291 end
292 292 else
293 293 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
294 294 ''
295 295 end
296 296 end
297 297
298 298 def subject_for_version(version, options)
299 299 case options[:format]
300 300 when :html
301 301 subject = "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
302 302 subject << view.link_to_version(version)
303 303 subject << '</span>'
304 304 html_subject(options, subject, :css => "version-name")
305 305 when :image
306 306 image_subject(options, version.to_s_with_project)
307 307 when :pdf
308 308 pdf_new_page?(options)
309 309 pdf_subject(options, version.to_s_with_project)
310 310 end
311 311 end
312 312
313 313 def line_for_version(version, options)
314 314 # Skip versions that don't have a start_date
315 315 if version.is_a?(Version) && version.start_date && version.due_date
316 316 options[:zoom] ||= 1
317 317 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
318 318
319 319 coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom])
320 320 label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
321 321 label = h("#{version.project} -") + label unless @project && @project == version.project
322 322
323 323 case options[:format]
324 324 when :html
325 325 html_task(options, coords, :css => "version task", :label => label, :markers => true)
326 326 when :image
327 327 image_task(options, coords, :label => label, :markers => true, :height => 3)
328 328 when :pdf
329 329 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
330 330 end
331 331 else
332 332 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
333 333 ''
334 334 end
335 335 end
336 336
337 337 def subject_for_issue(issue, options)
338 338 while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
339 339 @issue_ancestors.pop
340 340 options[:indent] -= options[:indent_increment]
341 341 end
342 342
343 343 output = case options[:format]
344 344 when :html
345 345 css_classes = ''
346 346 css_classes << ' issue-overdue' if issue.overdue?
347 347 css_classes << ' issue-behind-schedule' if issue.behind_schedule?
348 348 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
349 349
350 350 subject = "<span class='#{css_classes}'>"
351 351 if issue.assigned_to.present?
352 352 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
353 353 subject << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string).to_s
354 354 end
355 355 subject << view.link_to_issue(issue)
356 356 subject << '</span>'
357 357 html_subject(options, subject, :css => "issue-subject", :title => issue.subject) + "\n"
358 358 when :image
359 359 image_subject(options, issue.subject)
360 360 when :pdf
361 361 pdf_new_page?(options)
362 362 pdf_subject(options, issue.subject)
363 363 end
364 364
365 365 unless issue.leaf?
366 366 @issue_ancestors << issue
367 367 options[:indent] += options[:indent_increment]
368 368 end
369 369
370 370 output
371 371 end
372 372
373 373 def line_for_issue(issue, options)
374 374 # Skip issues that don't have a due_before (due_date or version's due_date)
375 375 if issue.is_a?(Issue) && issue.due_before
376 376 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
377 377 label = "#{ issue.status.name } #{ issue.done_ratio }%"
378 378
379 379 case options[:format]
380 380 when :html
381 381 html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), :label => label, :issue => issue, :markers => !issue.leaf?)
382 382 when :image
383 383 image_task(options, coords, :label => label)
384 384 when :pdf
385 385 pdf_task(options, coords, :label => label)
386 386 end
387 387 else
388 388 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
389 389 ''
390 390 end
391 391 end
392 392
393 393 # Generates a gantt image
394 394 # Only defined if RMagick is avalaible
395 395 def to_image(format='PNG')
396 396 date_to = (@date_from >> @months)-1
397 397 show_weeks = @zoom > 1
398 398 show_days = @zoom > 2
399 399
400 400 subject_width = 400
401 401 header_heigth = 18
402 402 # width of one day in pixels
403 403 zoom = @zoom*2
404 404 g_width = (@date_to - @date_from + 1)*zoom
405 405 g_height = 20 * number_of_rows + 30
406 406 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
407 407 height = g_height + headers_heigth
408 408
409 409 imgl = Magick::ImageList.new
410 410 imgl.new_image(subject_width+g_width+1, height)
411 411 gc = Magick::Draw.new
412 412
413 413 # Subjects
414 414 gc.stroke('transparent')
415 415 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
416 416
417 417 # Months headers
418 418 month_f = @date_from
419 419 left = subject_width
420 420 @months.times do
421 421 width = ((month_f >> 1) - month_f) * zoom
422 422 gc.fill('white')
423 423 gc.stroke('grey')
424 424 gc.stroke_width(1)
425 425 gc.rectangle(left, 0, left + width, height)
426 426 gc.fill('black')
427 427 gc.stroke('transparent')
428 428 gc.stroke_width(1)
429 429 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
430 430 left = left + width
431 431 month_f = month_f >> 1
432 432 end
433 433
434 434 # Weeks headers
435 435 if show_weeks
436 436 left = subject_width
437 437 height = header_heigth
438 438 if @date_from.cwday == 1
439 439 # date_from is monday
440 440 week_f = date_from
441 441 else
442 442 # find next monday after date_from
443 443 week_f = @date_from + (7 - @date_from.cwday + 1)
444 444 width = (7 - @date_from.cwday + 1) * zoom
445 445 gc.fill('white')
446 446 gc.stroke('grey')
447 447 gc.stroke_width(1)
448 448 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
449 449 left = left + width
450 450 end
451 451 while week_f <= date_to
452 452 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
453 453 gc.fill('white')
454 454 gc.stroke('grey')
455 455 gc.stroke_width(1)
456 456 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
457 457 gc.fill('black')
458 458 gc.stroke('transparent')
459 459 gc.stroke_width(1)
460 460 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
461 461 left = left + width
462 462 week_f = week_f+7
463 463 end
464 464 end
465 465
466 466 # Days details (week-end in grey)
467 467 if show_days
468 468 left = subject_width
469 469 height = g_height + header_heigth - 1
470 470 wday = @date_from.cwday
471 471 (date_to - @date_from + 1).to_i.times do
472 472 width = zoom
473 473 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
474 474 gc.stroke('#ddd')
475 475 gc.stroke_width(1)
476 476 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
477 477 left = left + width
478 478 wday = wday + 1
479 479 wday = 1 if wday > 7
480 480 end
481 481 end
482 482
483 483 # border
484 484 gc.fill('transparent')
485 485 gc.stroke('grey')
486 486 gc.stroke_width(1)
487 487 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
488 488 gc.stroke('black')
489 489 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
490 490
491 491 # content
492 492 top = headers_heigth + 20
493 493
494 494 gc.stroke('transparent')
495 495 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
496 496
497 497 # today red line
498 498 if Date.today >= @date_from and Date.today <= date_to
499 499 gc.stroke('red')
500 500 x = (Date.today-@date_from+1)*zoom + subject_width
501 501 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
502 502 end
503 503
504 504 gc.draw(imgl)
505 505 imgl.format = format
506 506 imgl.to_blob
507 507 end if Object.const_defined?(:Magick)
508 508
509 509 def to_pdf
510 510 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
511 511 pdf.SetTitle("#{l(:label_gantt)} #{project}")
512 512 pdf.AliasNbPages
513 513 pdf.footer_date = format_date(Date.today)
514 514 pdf.AddPage("L")
515 515 pdf.SetFontStyle('B',12)
516 516 pdf.SetX(15)
517 517 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
518 518 pdf.Ln
519 519 pdf.SetFontStyle('B',9)
520 520
521 521 subject_width = PDF::LeftPaneWidth
522 522 header_heigth = 5
523 523
524 524 headers_heigth = header_heigth
525 525 show_weeks = false
526 526 show_days = false
527 527
528 528 if self.months < 7
529 529 show_weeks = true
530 530 headers_heigth = 2*header_heigth
531 531 if self.months < 3
532 532 show_days = true
533 533 headers_heigth = 3*header_heigth
534 534 end
535 535 end
536 536
537 537 g_width = PDF.right_pane_width
538 538 zoom = (g_width) / (self.date_to - self.date_from + 1)
539 539 g_height = 120
540 540 t_height = g_height + headers_heigth
541 541
542 542 y_start = pdf.GetY
543 543
544 544 # Months headers
545 545 month_f = self.date_from
546 546 left = subject_width
547 547 height = header_heigth
548 548 self.months.times do
549 549 width = ((month_f >> 1) - month_f) * zoom
550 550 pdf.SetY(y_start)
551 551 pdf.SetX(left)
552 552 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
553 553 left = left + width
554 554 month_f = month_f >> 1
555 555 end
556 556
557 557 # Weeks headers
558 558 if show_weeks
559 559 left = subject_width
560 560 height = header_heigth
561 561 if self.date_from.cwday == 1
562 562 # self.date_from is monday
563 563 week_f = self.date_from
564 564 else
565 565 # find next monday after self.date_from
566 566 week_f = self.date_from + (7 - self.date_from.cwday + 1)
567 567 width = (7 - self.date_from.cwday + 1) * zoom-1
568 568 pdf.SetY(y_start + header_heigth)
569 569 pdf.SetX(left)
570 570 pdf.Cell(width + 1, height, "", "LTR")
571 571 left = left + width+1
572 572 end
573 573 while week_f <= self.date_to
574 574 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
575 575 pdf.SetY(y_start + header_heigth)
576 576 pdf.SetX(left)
577 577 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
578 578 left = left + width
579 579 week_f = week_f+7
580 580 end
581 581 end
582 582
583 583 # Days headers
584 584 if show_days
585 585 left = subject_width
586 586 height = header_heigth
587 587 wday = self.date_from.cwday
588 588 pdf.SetFontStyle('B',7)
589 589 (self.date_to - self.date_from + 1).to_i.times do
590 590 width = zoom
591 591 pdf.SetY(y_start + 2 * header_heigth)
592 592 pdf.SetX(left)
593 593 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
594 594 left = left + width
595 595 wday = wday + 1
596 596 wday = 1 if wday > 7
597 597 end
598 598 end
599 599
600 600 pdf.SetY(y_start)
601 601 pdf.SetX(15)
602 602 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
603 603
604 604 # Tasks
605 605 top = headers_heigth + y_start
606 606 options = {
607 607 :top => top,
608 608 :zoom => zoom,
609 609 :subject_width => subject_width,
610 610 :g_width => g_width,
611 611 :indent => 0,
612 612 :indent_increment => 5,
613 613 :top_increment => 5,
614 614 :format => :pdf,
615 615 :pdf => pdf
616 616 }
617 617 render(options)
618 618 pdf.Output
619 619 end
620 620
621 621 private
622 622
623 623 def coordinates(start_date, end_date, progress, zoom=nil)
624 624 zoom ||= @zoom
625 625
626 626 coords = {}
627 627 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
628 628 if start_date > self.date_from
629 629 coords[:start] = start_date - self.date_from
630 630 coords[:bar_start] = start_date - self.date_from
631 631 else
632 632 coords[:bar_start] = 0
633 633 end
634 634 if end_date < self.date_to
635 635 coords[:end] = end_date - self.date_from
636 636 coords[:bar_end] = end_date - self.date_from + 1
637 637 else
638 638 coords[:bar_end] = self.date_to - self.date_from + 1
639 639 end
640 640
641 641 if progress
642 progress_date = start_date + (end_date - start_date) * (progress / 100.0)
642 progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
643 643 if progress_date > self.date_from && progress_date > start_date
644 644 if progress_date < self.date_to
645 coords[:bar_progress_end] = progress_date - self.date_from + 1
645 coords[:bar_progress_end] = progress_date - self.date_from
646 646 else
647 647 coords[:bar_progress_end] = self.date_to - self.date_from + 1
648 648 end
649 649 end
650 650
651 651 if progress_date < Date.today
652 652 late_date = [Date.today, end_date].min
653 653 if late_date > self.date_from && late_date > start_date
654 654 if late_date < self.date_to
655 655 coords[:bar_late_end] = late_date - self.date_from + 1
656 656 else
657 657 coords[:bar_late_end] = self.date_to - self.date_from + 1
658 658 end
659 659 end
660 660 end
661 661 end
662 662 end
663 663
664 664 # Transforms dates into pixels witdh
665 665 coords.keys.each do |key|
666 666 coords[key] = (coords[key] * zoom).floor
667 667 end
668 668 coords
669 669 end
670 670
671 671 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
672 672 def sort_issues!(issues)
673 673 issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
674 674 end
675 675
676 676 # TODO: top level issues should be sorted by start date
677 677 def gantt_issue_compare(x, y, issues)
678 678 if x.root_id == y.root_id
679 679 x.lft <=> y.lft
680 680 else
681 681 x.root_id <=> y.root_id
682 682 end
683 683 end
684 684
685 685 def current_limit
686 686 if @max_rows
687 687 @max_rows - @number_of_rows
688 688 else
689 689 nil
690 690 end
691 691 end
692 692
693 693 def abort?
694 694 if @max_rows && @number_of_rows >= @max_rows
695 695 @truncated = true
696 696 end
697 697 end
698 698
699 699 def pdf_new_page?(options)
700 700 if options[:top] > 180
701 701 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
702 702 options[:pdf].AddPage("L")
703 703 options[:top] = 15
704 704 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
705 705 end
706 706 end
707 707
708 708 def html_subject(params, subject, options={})
709 709 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
710 710 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
711 711
712 712 output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
713 713 @subjects << output
714 714 output
715 715 end
716 716
717 717 def pdf_subject(params, subject, options={})
718 718 params[:pdf].SetY(params[:top])
719 719 params[:pdf].SetX(15)
720 720
721 721 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
722 722 params[:pdf].Cell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
723 723
724 724 params[:pdf].SetY(params[:top])
725 725 params[:pdf].SetX(params[:subject_width])
726 726 params[:pdf].Cell(params[:g_width], 5, "", "LR")
727 727 end
728 728
729 729 def image_subject(params, subject, options={})
730 730 params[:image].fill('black')
731 731 params[:image].stroke('transparent')
732 732 params[:image].stroke_width(1)
733 733 params[:image].text(params[:indent], params[:top] + 2, subject)
734 734 end
735 735
736 736 def html_task(params, coords, options={})
737 737 output = ''
738 738 # Renders the task bar, with progress and late
739 739 if coords[:bar_start] && coords[:bar_end]
740 740 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_todo'>&nbsp;</div>"
741 741
742 742 if coords[:bar_late_end]
743 743 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_late_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_late'>&nbsp;</div>"
744 744 end
745 745 if coords[:bar_progress_end]
746 746 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_progress_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_done'>&nbsp;</div>"
747 747 end
748 748 end
749 749 # Renders the markers
750 750 if options[:markers]
751 751 if coords[:start]
752 752 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>"
753 753 end
754 754 if coords[:end]
755 755 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:end] + params[:zoom] }px;width:15px;' class='#{options[:css]} marker ending'>&nbsp;</div>"
756 756 end
757 757 end
758 758 # Renders the label on the right
759 759 if options[:label]
760 760 output << "<div style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>"
761 761 output << options[:label]
762 762 output << "</div>"
763 763 end
764 764 # Renders the tooltip
765 765 if options[:issue] && coords[:bar_start] && coords[:bar_end]
766 766 output << "<div class='tooltip' style='position: absolute;top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] }px;height:12px;'>"
767 767 output << '<span class="tip">'
768 768 output << view.render_issue_tooltip(options[:issue])
769 769 output << "</span></div>"
770 770 end
771 771 @lines << output
772 772 output
773 773 end
774 774
775 775 def pdf_task(params, coords, options={})
776 776 height = options[:height] || 2
777 777
778 778 # Renders the task bar, with progress and late
779 779 if coords[:bar_start] && coords[:bar_end]
780 780 params[:pdf].SetY(params[:top]+1.5)
781 781 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
782 782 params[:pdf].SetFillColor(200,200,200)
783 783 params[:pdf].Cell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
784 784
785 785 if coords[:bar_late_end]
786 786 params[:pdf].SetY(params[:top]+1.5)
787 787 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
788 788 params[:pdf].SetFillColor(255,100,100)
789 789 params[:pdf].Cell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
790 790 end
791 791 if coords[:bar_progress_end]
792 792 params[:pdf].SetY(params[:top]+1.5)
793 793 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
794 794 params[:pdf].SetFillColor(90,200,90)
795 795 params[:pdf].Cell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
796 796 end
797 797 end
798 798 # Renders the markers
799 799 if options[:markers]
800 800 if coords[:start]
801 801 params[:pdf].SetY(params[:top] + 1)
802 802 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
803 803 params[:pdf].SetFillColor(50,50,200)
804 804 params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
805 805 end
806 806 if coords[:end]
807 807 params[:pdf].SetY(params[:top] + 1)
808 808 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
809 809 params[:pdf].SetFillColor(50,50,200)
810 810 params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
811 811 end
812 812 end
813 813 # Renders the label on the right
814 814 if options[:label]
815 815 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
816 816 params[:pdf].Cell(30, 2, options[:label])
817 817 end
818 818 end
819 819
820 820 def image_task(params, coords, options={})
821 821 height = options[:height] || 6
822 822
823 823 # Renders the task bar, with progress and late
824 824 if coords[:bar_start] && coords[:bar_end]
825 825 params[:image].fill('#aaa')
826 826 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_end], params[:top] - height)
827 827
828 828 if coords[:bar_late_end]
829 829 params[:image].fill('#f66')
830 830 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_late_end], params[:top] - height)
831 831 end
832 832 if coords[:bar_progress_end]
833 833 params[:image].fill('#00c600')
834 834 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_progress_end], params[:top] - height)
835 835 end
836 836 end
837 837 # Renders the markers
838 838 if options[:markers]
839 839 if coords[:start]
840 840 x = params[:subject_width] + coords[:start]
841 841 y = params[:top] - height / 2
842 842 params[:image].fill('blue')
843 843 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
844 844 end
845 845 if coords[:end]
846 846 x = params[:subject_width] + coords[:end] + params[:zoom]
847 847 y = params[:top] - height / 2
848 848 params[:image].fill('blue')
849 849 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
850 850 end
851 851 end
852 852 # Renders the label on the right
853 853 if options[:label]
854 854 params[:image].fill('black')
855 855 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
856 856 end
857 857 end
858 858 end
859 859 end
860 860 end
@@ -1,761 +1,779
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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.expand_path('../../../../../test_helper', __FILE__)
19 19
20 20 class Redmine::Helpers::GanttTest < ActiveSupport::TestCase
21 21 # Utility methods and classes so assert_select can be used.
22 22 class GanttViewTest < ActionView::Base
23 23 include ActionView::Helpers::UrlHelper
24 24 include ActionView::Helpers::TextHelper
25 25 include ActionController::UrlWriter
26 26 include ApplicationHelper
27 27 include ProjectsHelper
28 28 include IssuesHelper
29 29
30 30 def self.default_url_options
31 31 {:only_path => true }
32 32 end
33 33
34 34 end
35 35
36 36 include ActionController::Assertions::SelectorAssertions
37 37
38 38 def setup
39 39 @response = ActionController::TestResponse.new
40 40 # Fixtures
41 41 ProjectCustomField.delete_all
42 42 Project.destroy_all
43 43
44 44 User.current = User.find(1)
45 45 end
46 46
47 47 def build_view
48 48 @view = GanttViewTest.new
49 49 end
50 50
51 51 def html_document
52 52 HTML::Document.new(@response.body)
53 53 end
54 54
55 55 # Creates a Gantt chart for a 4 week span
56 56 def create_gantt(project=Project.generate!, options={})
57 57 @project = project
58 58 @gantt = Redmine::Helpers::Gantt.new(options)
59 59 @gantt.project = @project
60 60 @gantt.query = Query.generate_default!(:project => @project)
61 61 @gantt.view = build_view
62 62 @gantt.instance_variable_set('@date_from', options[:date_from] || 2.weeks.ago.to_date)
63 63 @gantt.instance_variable_set('@date_to', options[:date_to] || 2.weeks.from_now.to_date)
64 64 end
65 65
66 66 context "#number_of_rows" do
67 67
68 68 context "with one project" do
69 69 should "return the number of rows just for that project"
70 70 end
71 71
72 72 context "with no project" do
73 73 should "return the total number of rows for all the projects, resursively"
74 74 end
75 75
76 76 should "not exceed max_rows option" do
77 77 p = Project.generate!
78 78 5.times do
79 79 Issue.generate_for_project!(p)
80 80 end
81 81
82 82 create_gantt(p)
83 83 @gantt.render
84 84 assert_equal 6, @gantt.number_of_rows
85 85 assert !@gantt.truncated
86 86
87 87 create_gantt(p, :max_rows => 3)
88 88 @gantt.render
89 89 assert_equal 3, @gantt.number_of_rows
90 90 assert @gantt.truncated
91 91 end
92 92 end
93 93
94 94 context "#number_of_rows_on_project" do
95 95 setup do
96 96 create_gantt
97 97 end
98 98
99 99 should "count 0 for an empty the project" do
100 100 assert_equal 0, @gantt.number_of_rows_on_project(@project)
101 101 end
102 102
103 103 should "count the number of issues without a version" do
104 104 @project.issues << Issue.generate_for_project!(@project, :fixed_version => nil)
105 105 assert_equal 2, @gantt.number_of_rows_on_project(@project)
106 106 end
107 107
108 108 should "count the number of issues on versions, including cross-project" do
109 109 version = Version.generate!
110 110 @project.versions << version
111 111 @project.issues << Issue.generate_for_project!(@project, :fixed_version => version)
112 112
113 113 assert_equal 3, @gantt.number_of_rows_on_project(@project)
114 114 end
115 115 end
116 116
117 117 # TODO: more of an integration test
118 118 context "#subjects" do
119 119 setup do
120 120 create_gantt
121 121 @project.enabled_module_names = [:issue_tracking]
122 122 @tracker = Tracker.generate!
123 123 @project.trackers << @tracker
124 124 @version = Version.generate!(:effective_date => 1.week.from_now.to_date, :sharing => 'none')
125 125 @project.versions << @version
126 126
127 127 @issue = Issue.generate!(:fixed_version => @version,
128 128 :subject => "gantt#line_for_project",
129 129 :tracker => @tracker,
130 130 :project => @project,
131 131 :done_ratio => 30,
132 132 :start_date => Date.yesterday,
133 133 :due_date => 1.week.from_now.to_date)
134 134 @project.issues << @issue
135 135 end
136 136
137 137 context "project" do
138 138 should "be rendered" do
139 139 @response.body = @gantt.subjects
140 140 assert_select "div.project-name a", /#{@project.name}/
141 141 end
142 142
143 143 should "have an indent of 4" do
144 144 @response.body = @gantt.subjects
145 145 assert_select "div.project-name[style*=left:4px]"
146 146 end
147 147 end
148 148
149 149 context "version" do
150 150 should "be rendered" do
151 151 @response.body = @gantt.subjects
152 152 assert_select "div.version-name a", /#{@version.name}/
153 153 end
154 154
155 155 should "be indented 24 (one level)" do
156 156 @response.body = @gantt.subjects
157 157 assert_select "div.version-name[style*=left:24px]"
158 158 end
159 159
160 160 context "without assigned issues" do
161 161 setup do
162 162 @version = Version.generate!(:effective_date => 2.week.from_now.to_date, :sharing => 'none', :name => 'empty_version')
163 163 @project.versions << @version
164 164 end
165 165
166 166 should "not be rendered" do
167 167 @response.body = @gantt.subjects
168 168 assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0
169 169 end
170 170 end
171 171 end
172 172
173 173 context "issue" do
174 174 should "be rendered" do
175 175 @response.body = @gantt.subjects
176 176 assert_select "div.issue-subject", /#{@issue.subject}/
177 177 end
178 178
179 179 should "be indented 44 (two levels)" do
180 180 @response.body = @gantt.subjects
181 181 assert_select "div.issue-subject[style*=left:44px]"
182 182 end
183 183
184 184 context "assigned to a shared version of another project" do
185 185 setup do
186 186 p = Project.generate!
187 187 p.trackers << @tracker
188 188 p.enabled_module_names = [:issue_tracking]
189 189 @shared_version = Version.generate!(:sharing => 'system')
190 190 p.versions << @shared_version
191 191 # Reassign the issue to a shared version of another project
192 192
193 193 @issue = Issue.generate!(:fixed_version => @shared_version,
194 194 :subject => "gantt#assigned_to_shared_version",
195 195 :tracker => @tracker,
196 196 :project => @project,
197 197 :done_ratio => 30,
198 198 :start_date => Date.yesterday,
199 199 :due_date => 1.week.from_now.to_date)
200 200 @project.issues << @issue
201 201 end
202 202
203 203 should "be rendered" do
204 204 @response.body = @gantt.subjects
205 205 assert_select "div.issue-subject", /#{@issue.subject}/
206 206 end
207 207 end
208 208
209 209 context "with subtasks" do
210 210 setup do
211 211 attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version}
212 212 @child1 = Issue.generate!(attrs.merge(:subject => 'child1', :parent_issue_id => @issue.id, :start_date => Date.yesterday, :due_date => 2.day.from_now.to_date))
213 213 @child2 = Issue.generate!(attrs.merge(:subject => 'child2', :parent_issue_id => @issue.id, :start_date => Date.today, :due_date => 1.week.from_now.to_date))
214 214 @grandchild = Issue.generate!(attrs.merge(:subject => 'grandchild', :parent_issue_id => @child1.id, :start_date => Date.yesterday, :due_date => 2.day.from_now.to_date))
215 215 end
216 216
217 217 should "indent subtasks" do
218 218 @response.body = @gantt.subjects
219 219 # parent task 44px
220 220 assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/
221 221 # children 64px
222 222 assert_select "div.issue-subject[style*=left:64px]", /child1/
223 223 assert_select "div.issue-subject[style*=left:64px]", /child2/
224 224 # grandchild 84px
225 225 assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @response.body
226 226 end
227 227 end
228 228 end
229 229 end
230 230
231 231 context "#lines" do
232 232 setup do
233 233 create_gantt
234 234 @project.enabled_module_names = [:issue_tracking]
235 235 @tracker = Tracker.generate!
236 236 @project.trackers << @tracker
237 237 @version = Version.generate!(:effective_date => 1.week.from_now.to_date)
238 238 @project.versions << @version
239 239 @issue = Issue.generate!(:fixed_version => @version,
240 240 :subject => "gantt#line_for_project",
241 241 :tracker => @tracker,
242 242 :project => @project,
243 243 :done_ratio => 30,
244 244 :start_date => Date.yesterday,
245 245 :due_date => 1.week.from_now.to_date)
246 246 @project.issues << @issue
247 247
248 248 @response.body = @gantt.lines
249 249 end
250 250
251 251 context "project" do
252 252 should "be rendered" do
253 253 assert_select "div.project.task_todo"
254 254 assert_select "div.project.starting"
255 255 assert_select "div.project.ending"
256 256 assert_select "div.label.project", /#{@project.name}/
257 257 end
258 258 end
259 259
260 260 context "version" do
261 261 should "be rendered" do
262 262 assert_select "div.version.task_todo"
263 263 assert_select "div.version.starting"
264 264 assert_select "div.version.ending"
265 265 assert_select "div.label.version", /#{@version.name}/
266 266 end
267 267 end
268 268
269 269 context "issue" do
270 270 should "be rendered" do
271 271 assert_select "div.task_todo"
272 272 assert_select "div.task.label", /#{@issue.done_ratio}/
273 273 assert_select "div.tooltip", /#{@issue.subject}/
274 274 end
275 275 end
276 276 end
277 277
278 278 context "#render_project" do
279 279 should "be tested"
280 280 end
281 281
282 282 context "#render_issues" do
283 283 should "be tested"
284 284 end
285 285
286 286 context "#render_version" do
287 287 should "be tested"
288 288 end
289 289
290 290 context "#subject_for_project" do
291 291 setup do
292 292 create_gantt
293 293 end
294 294
295 295 context ":html format" do
296 296 should "add an absolute positioned div" do
297 297 @response.body = @gantt.subject_for_project(@project, {:format => :html})
298 298 assert_select "div[style*=absolute]"
299 299 end
300 300
301 301 should "use the indent option to move the div to the right" do
302 302 @response.body = @gantt.subject_for_project(@project, {:format => :html, :indent => 40})
303 303 assert_select "div[style*=left:40]"
304 304 end
305 305
306 306 should "include the project name" do
307 307 @response.body = @gantt.subject_for_project(@project, {:format => :html})
308 308 assert_select 'div', :text => /#{@project.name}/
309 309 end
310 310
311 311 should "include a link to the project" do
312 312 @response.body = @gantt.subject_for_project(@project, {:format => :html})
313 313 assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/
314 314 end
315 315
316 316 should "style overdue projects" do
317 317 @project.enabled_module_names = [:issue_tracking]
318 318 @project.versions << Version.generate!(:effective_date => Date.yesterday)
319 319
320 320 assert @project.overdue?, "Need an overdue project for this test"
321 321 @response.body = @gantt.subject_for_project(@project, {:format => :html})
322 322
323 323 assert_select 'div span.project-overdue'
324 324 end
325 325
326 326
327 327 end
328 328
329 329 should "test the PNG format"
330 330 should "test the PDF format"
331 331 end
332 332
333 333 context "#line_for_project" do
334 334 setup do
335 335 create_gantt
336 336 @project.enabled_module_names = [:issue_tracking]
337 337 @tracker = Tracker.generate!
338 338 @project.trackers << @tracker
339 339 @version = Version.generate!(:effective_date => Date.yesterday)
340 340 @project.versions << @version
341 341
342 342 @project.issues << Issue.generate!(:fixed_version => @version,
343 343 :subject => "gantt#line_for_project",
344 344 :tracker => @tracker,
345 345 :project => @project,
346 346 :done_ratio => 30,
347 347 :start_date => 1.week.ago.to_date,
348 348 :due_date => 1.week.from_now.to_date)
349 349 end
350 350
351 351 context ":html format" do
352 352 context "todo line" do
353 353 should "start from the starting point on the left" do
354 354 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
355 355 assert_select "div.project.task_todo[style*=left:28px]", true, @response.body
356 356 end
357 357
358 358 should "be the total width of the project" do
359 359 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
360 360 assert_select "div.project.task_todo[style*=width:58px]", true, @response.body
361 361 end
362 362
363 363 end
364 364
365 365 context "late line" do
366 366 should_eventually "start from the starting point on the left" do
367 367 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
368 368 assert_select "div.project.task_late[style*=left:28px]", true, @response.body
369 369 end
370 370
371 371 should_eventually "be the total delayed width of the project" do
372 372 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
373 373 assert_select "div.project.task_late[style*=width:30px]", true, @response.body
374 374 end
375 375 end
376 376
377 377 context "done line" do
378 378 should_eventually "start from the starting point on the left" do
379 379 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
380 380 assert_select "div.project.task_done[style*=left:28px]", true, @response.body
381 381 end
382 382
383 383 should_eventually "Be the total done width of the project" do
384 384 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
385 385 assert_select "div.project.task_done[style*=width:18px]", true, @response.body
386 386 end
387 387 end
388 388
389 389 context "starting marker" do
390 390 should "not appear if the starting point is off the gantt chart" do
391 391 # Shift the date range of the chart
392 392 @gantt.instance_variable_set('@date_from', Date.today)
393 393
394 394 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
395 395 assert_select "div.project.starting", false, @response.body
396 396 end
397 397
398 398 should "appear at the starting point" do
399 399 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
400 400 assert_select "div.project.starting[style*=left:28px]", true, @response.body
401 401 end
402 402 end
403 403
404 404 context "ending marker" do
405 405 should "not appear if the starting point is off the gantt chart" do
406 406 # Shift the date range of the chart
407 407 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
408 408
409 409 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
410 410 assert_select "div.project.ending", false, @response.body
411 411
412 412 end
413 413
414 414 should "appear at the end of the date range" do
415 415 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
416 416 assert_select "div.project.ending[style*=left:88px]", true, @response.body
417 417 end
418 418 end
419 419
420 420 context "status content" do
421 421 should "appear at the far left, even if it's far in the past" do
422 422 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
423 423
424 424 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
425 425 assert_select "div.project.label", /#{@project.name}/
426 426 end
427 427
428 428 should "show the project name" do
429 429 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
430 430 assert_select "div.project.label", /#{@project.name}/
431 431 end
432 432
433 433 should_eventually "show the percent complete" do
434 434 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
435 435 assert_select "div.project.label", /0%/
436 436 end
437 437 end
438 438 end
439 439
440 440 should "test the PNG format"
441 441 should "test the PDF format"
442 442 end
443 443
444 444 context "#subject_for_version" do
445 445 setup do
446 446 create_gantt
447 447 @project.enabled_module_names = [:issue_tracking]
448 448 @tracker = Tracker.generate!
449 449 @project.trackers << @tracker
450 450 @version = Version.generate!(:effective_date => Date.yesterday)
451 451 @project.versions << @version
452 452
453 453 @project.issues << Issue.generate!(:fixed_version => @version,
454 454 :subject => "gantt#subject_for_version",
455 455 :tracker => @tracker,
456 456 :project => @project,
457 457 :start_date => Date.today)
458 458
459 459 end
460 460
461 461 context ":html format" do
462 462 should "add an absolute positioned div" do
463 463 @response.body = @gantt.subject_for_version(@version, {:format => :html})
464 464 assert_select "div[style*=absolute]"
465 465 end
466 466
467 467 should "use the indent option to move the div to the right" do
468 468 @response.body = @gantt.subject_for_version(@version, {:format => :html, :indent => 40})
469 469 assert_select "div[style*=left:40]"
470 470 end
471 471
472 472 should "include the version name" do
473 473 @response.body = @gantt.subject_for_version(@version, {:format => :html})
474 474 assert_select 'div', :text => /#{@version.name}/
475 475 end
476 476
477 477 should "include a link to the version" do
478 478 @response.body = @gantt.subject_for_version(@version, {:format => :html})
479 479 assert_select 'a[href=?]', Regexp.escape("/versions/show/#{@version.to_param}"), :text => /#{@version.name}/
480 480 end
481 481
482 482 should "style late versions" do
483 483 assert @version.overdue?, "Need an overdue version for this test"
484 484 @response.body = @gantt.subject_for_version(@version, {:format => :html})
485 485
486 486 assert_select 'div span.version-behind-schedule'
487 487 end
488 488
489 489 should "style behind schedule versions" do
490 490 assert @version.behind_schedule?, "Need a behind schedule version for this test"
491 491 @response.body = @gantt.subject_for_version(@version, {:format => :html})
492 492
493 493 assert_select 'div span.version-behind-schedule'
494 494 end
495 495 end
496 496 should "test the PNG format"
497 497 should "test the PDF format"
498 498 end
499 499
500 500 context "#line_for_version" do
501 501 setup do
502 502 create_gantt
503 503 @project.enabled_module_names = [:issue_tracking]
504 504 @tracker = Tracker.generate!
505 505 @project.trackers << @tracker
506 506 @version = Version.generate!(:effective_date => 1.week.from_now.to_date)
507 507 @project.versions << @version
508 508
509 509 @project.issues << Issue.generate!(:fixed_version => @version,
510 510 :subject => "gantt#line_for_project",
511 511 :tracker => @tracker,
512 512 :project => @project,
513 513 :done_ratio => 30,
514 514 :start_date => 1.week.ago.to_date,
515 515 :due_date => 1.week.from_now.to_date)
516 516 end
517 517
518 518 context ":html format" do
519 519 context "todo line" do
520 520 should "start from the starting point on the left" do
521 521 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
522 522 assert_select "div.version.task_todo[style*=left:28px]", true, @response.body
523 523 end
524 524
525 525 should "be the total width of the version" do
526 526 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
527 527 assert_select "div.version.task_todo[style*=width:58px]", true, @response.body
528 528 end
529 529
530 530 end
531 531
532 532 context "late line" do
533 533 should "start from the starting point on the left" do
534 534 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
535 535 assert_select "div.version.task_late[style*=left:28px]", true, @response.body
536 536 end
537 537
538 538 should "be the total delayed width of the version" do
539 539 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
540 540 assert_select "div.version.task_late[style*=width:30px]", true, @response.body
541 541 end
542 542 end
543 543
544 544 context "done line" do
545 545 should "start from the starting point on the left" do
546 546 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
547 547 assert_select "div.version.task_done[style*=left:28px]", true, @response.body
548 548 end
549 549
550 should "Be the total done width of the version" do
550 should "be the total done width of the version" do
551 551 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
552 assert_select "div.version.task_done[style*=width:18px]", true, @response.body
552 assert_select "div.version.task_done[style*=width:16px]", true, @response.body
553 553 end
554 554 end
555 555
556 556 context "starting marker" do
557 557 should "not appear if the starting point is off the gantt chart" do
558 558 # Shift the date range of the chart
559 559 @gantt.instance_variable_set('@date_from', Date.today)
560 560
561 561 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
562 562 assert_select "div.version.starting", false
563 563 end
564 564
565 565 should "appear at the starting point" do
566 566 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
567 567 assert_select "div.version.starting[style*=left:28px]", true, @response.body
568 568 end
569 569 end
570 570
571 571 context "ending marker" do
572 572 should "not appear if the starting point is off the gantt chart" do
573 573 # Shift the date range of the chart
574 574 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
575 575
576 576 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
577 577 assert_select "div.version.ending", false
578 578
579 579 end
580 580
581 581 should "appear at the end of the date range" do
582 582 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
583 583 assert_select "div.version.ending[style*=left:88px]", true, @response.body
584 584 end
585 585 end
586 586
587 587 context "status content" do
588 588 should "appear at the far left, even if it's far in the past" do
589 589 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
590 590
591 591 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
592 592 assert_select "div.version.label", /#{@version.name}/
593 593 end
594 594
595 595 should "show the version name" do
596 596 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
597 597 assert_select "div.version.label", /#{@version.name}/
598 598 end
599 599
600 600 should "show the percent complete" do
601 601 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
602 602 assert_select "div.version.label", /30%/
603 603 end
604 604 end
605 605 end
606 606
607 607 should "test the PNG format"
608 608 should "test the PDF format"
609 609 end
610 610
611 611 context "#subject_for_issue" do
612 612 setup do
613 613 create_gantt
614 614 @project.enabled_module_names = [:issue_tracking]
615 615 @tracker = Tracker.generate!
616 616 @project.trackers << @tracker
617 617
618 618 @issue = Issue.generate!(:subject => "gantt#subject_for_issue",
619 619 :tracker => @tracker,
620 620 :project => @project,
621 621 :start_date => 3.days.ago.to_date,
622 622 :due_date => Date.yesterday)
623 623 @project.issues << @issue
624 624
625 625 end
626 626
627 627 context ":html format" do
628 628 should "add an absolute positioned div" do
629 629 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
630 630 assert_select "div[style*=absolute]"
631 631 end
632 632
633 633 should "use the indent option to move the div to the right" do
634 634 @response.body = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40})
635 635 assert_select "div[style*=left:40]"
636 636 end
637 637
638 638 should "include the issue subject" do
639 639 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
640 640 assert_select 'div', :text => /#{@issue.subject}/
641 641 end
642 642
643 643 should "include a link to the issue" do
644 644 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
645 645 assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/
646 646 end
647 647
648 648 should "style overdue issues" do
649 649 assert @issue.overdue?, "Need an overdue issue for this test"
650 650 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
651 651
652 652 assert_select 'div span.issue-overdue'
653 653 end
654 654
655 655 end
656 656 should "test the PNG format"
657 657 should "test the PDF format"
658 658 end
659 659
660 660 context "#line_for_issue" do
661 661 setup do
662 662 create_gantt
663 663 @project.enabled_module_names = [:issue_tracking]
664 664 @tracker = Tracker.generate!
665 665 @project.trackers << @tracker
666 666 @version = Version.generate!(:effective_date => 1.week.from_now.to_date)
667 667 @project.versions << @version
668 668 @issue = Issue.generate!(:fixed_version => @version,
669 669 :subject => "gantt#line_for_project",
670 670 :tracker => @tracker,
671 671 :project => @project,
672 672 :done_ratio => 30,
673 673 :start_date => 1.week.ago.to_date,
674 674 :due_date => 1.week.from_now.to_date)
675 675 @project.issues << @issue
676 676 end
677 677
678 678 context ":html format" do
679 679 context "todo line" do
680 680 should "start from the starting point on the left" do
681 681 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
682 682 assert_select "div.task_todo[style*=left:28px]", true, @response.body
683 683 end
684 684
685 685 should "be the total width of the issue" do
686 686 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
687 687 assert_select "div.task_todo[style*=width:58px]", true, @response.body
688 688 end
689 689
690 690 end
691 691
692 692 context "late line" do
693 693 should "start from the starting point on the left" do
694 694 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
695 695 assert_select "div.task_late[style*=left:28px]", true, @response.body
696 696 end
697 697
698 698 should "be the total delayed width of the issue" do
699 699 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
700 700 assert_select "div.task_late[style*=width:30px]", true, @response.body
701 701 end
702 702 end
703 703
704 704 context "done line" do
705 705 should "start from the starting point on the left" do
706 706 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
707 707 assert_select "div.task_done[style*=left:28px]", true, @response.body
708 708 end
709 709
710 should "Be the total done width of the issue" do
710 should "be the total done width of the issue" do
711 711 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
712 assert_select "div.task_done[style*=width:18px]", true, @response.body
712 # 15 days * 4 px * 30% - 2 px for borders = 16 px
713 assert_select "div.task_done[style*=width:16px]", true, @response.body
713 714 end
714 715
715 716 should "not be the total done width if the chart starts after issue start date" do
716 717 create_gantt(@project, :date_from => 5.days.ago.to_date)
717 718
718 719 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
719 720 assert_select "div.task_done[style*=left:0px]", true, @response.body
720 assert_select "div.task_done[style*=width:10px]", true, @response.body
721 assert_select "div.task_done[style*=width:8px]", true, @response.body
722 end
723
724 context "for completed issue" do
725 setup do
726 @issue.done_ratio = 100
727 end
728
729 should "be the total width of the issue" do
730 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
731 assert_select "div.task_done[style*=width:58px]", true, @response.body
732 end
733
734 should "be the total width of the issue with due_date=start_date" do
735 @issue.due_date = @issue.start_date
736 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
737 assert_select "div.task_done[style*=width:2px]", true, @response.body
738 end
721 739 end
722 740 end
723 741
724 742 context "status content" do
725 743 should "appear at the far left, even if it's far in the past" do
726 744 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
727 745
728 746 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
729 747 assert_select "div.task.label", true, @response.body
730 748 end
731 749
732 750 should "show the issue status" do
733 751 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
734 752 assert_select "div.task.label", /#{@issue.status.name}/
735 753 end
736 754
737 755 should "show the percent complete" do
738 756 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
739 757 assert_select "div.task.label", /30%/
740 758 end
741 759 end
742 760 end
743 761
744 762 should "have an issue tooltip" do
745 763 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
746 764 assert_select "div.tooltip", /#{@issue.subject}/
747 765 end
748 766
749 767 should "test the PNG format"
750 768 should "test the PDF format"
751 769 end
752 770
753 771 context "#to_image" do
754 772 should "be tested"
755 773 end
756 774
757 775 context "#to_pdf" do
758 776 should "be tested"
759 777 end
760 778
761 779 end
General Comments 0
You need to be logged in to leave comments. Login now