##// END OF EJS Templates
Fixed: error on gantt when no issue have a due date (#6350)....
Jean-Philippe Lang -
r4166:ed608cb7d39f
parent child
Show More
@@ -1,976 +1,976
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 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
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 end
71 71
72 72 def common_params
73 73 { :controller => 'gantts', :action => 'show', :project_id => @project }
74 74 end
75 75
76 76 def params
77 77 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
78 78 end
79 79
80 80 def params_previous
81 81 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
82 82 end
83 83
84 84 def params_next
85 85 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
86 86 end
87 87
88 88 ### Extracted from the HTML view/helpers
89 89 # Returns the number of rows that will be rendered on the Gantt chart
90 90 def number_of_rows
91 91 if @project
92 92 return number_of_rows_on_project(@project)
93 93 else
94 94 Project.roots.inject(0) do |total, project|
95 95 total += number_of_rows_on_project(project)
96 96 end
97 97 end
98 98 end
99 99
100 100 # Returns the number of rows that will be used to list a project on
101 101 # the Gantt chart. This will recurse for each subproject.
102 102 def number_of_rows_on_project(project)
103 103 # Remove the project requirement for Versions because it will
104 104 # restrict issues to only be on the current project. This
105 105 # ends up missing issues which are assigned to shared versions.
106 106 @query.project = nil if @query.project
107 107
108 108 # One Root project
109 109 count = 1
110 110 # Issues without a Version
111 111 count += project.issues.for_gantt.without_version.with_query(@query).count
112 112
113 113 # Versions
114 114 count += project.versions.count
115 115
116 116 # Issues on the Versions
117 117 project.versions.each do |version|
118 118 count += version.fixed_issues.for_gantt.with_query(@query).count
119 119 end
120 120
121 121 # Subprojects
122 122 project.children.each do |subproject|
123 123 count += number_of_rows_on_project(subproject)
124 124 end
125 125
126 126 count
127 127 end
128 128
129 129 # Renders the subjects of the Gantt chart, the left side.
130 130 def subjects(options={})
131 131 options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
132 132
133 133 output = ''
134 134 if @project
135 135 output << render_project(@project, options)
136 136 else
137 137 Project.roots.each do |project|
138 138 output << render_project(project, options)
139 139 end
140 140 end
141 141
142 142 output
143 143 end
144 144
145 145 # Renders the lines of the Gantt chart, the right side
146 146 def lines(options={})
147 147 options = {:indent => 4, :render => :line, :format => :html}.merge(options)
148 148 output = ''
149 149
150 150 if @project
151 151 output << render_project(@project, options)
152 152 else
153 153 Project.roots.each do |project|
154 154 output << render_project(project, options)
155 155 end
156 156 end
157 157
158 158 output
159 159 end
160 160
161 161 def render_project(project, options={})
162 162 options[:top] = 0 unless options.key? :top
163 163 options[:indent_increment] = 20 unless options.key? :indent_increment
164 164 options[:top_increment] = 20 unless options.key? :top_increment
165 165
166 166 output = ''
167 167 # Project Header
168 168 project_header = if options[:render] == :subject
169 169 subject_for_project(project, options)
170 170 else
171 171 # :line
172 172 line_for_project(project, options)
173 173 end
174 174 output << project_header if options[:format] == :html
175 175
176 176 options[:top] += options[:top_increment]
177 177 options[:indent] += options[:indent_increment]
178 178
179 179 # Second, Issues without a version
180 180 issues = project.issues.for_gantt.without_version.with_query(@query)
181 181 if issues
182 182 issue_rendering = render_issues(issues, options)
183 183 output << issue_rendering if options[:format] == :html
184 184 end
185 185
186 186 # Third, Versions
187 187 project.versions.sort.each do |version|
188 188 version_rendering = render_version(version, options)
189 189 output << version_rendering if options[:format] == :html
190 190 end
191 191
192 192 # Fourth, subprojects
193 193 project.children.each do |project|
194 194 subproject_rendering = render_project(project, options)
195 195 output << subproject_rendering if options[:format] == :html
196 196 end
197 197
198 198 # Remove indent to hit the next sibling
199 199 options[:indent] -= options[:indent_increment]
200 200
201 201 output
202 202 end
203 203
204 204 def render_issues(issues, options={})
205 205 output = ''
206 206 issues.each do |i|
207 207 issue_rendering = if options[:render] == :subject
208 208 subject_for_issue(i, options)
209 209 else
210 210 # :line
211 211 line_for_issue(i, options)
212 212 end
213 213 output << issue_rendering if options[:format] == :html
214 214 options[:top] += options[:top_increment]
215 215 end
216 216 output
217 217 end
218 218
219 219 def render_version(version, options={})
220 220 output = ''
221 221 # Version header
222 222 version_rendering = if options[:render] == :subject
223 223 subject_for_version(version, options)
224 224 else
225 225 # :line
226 226 line_for_version(version, options)
227 227 end
228 228
229 229 output << version_rendering if options[:format] == :html
230 230
231 231 options[:top] += options[:top_increment]
232 232
233 233 # Remove the project requirement for Versions because it will
234 234 # restrict issues to only be on the current project. This
235 235 # ends up missing issues which are assigned to shared versions.
236 236 @query.project = nil if @query.project
237 237
238 238 issues = version.fixed_issues.for_gantt.with_query(@query)
239 239 if issues
240 240 # Indent issues
241 241 options[:indent] += options[:indent_increment]
242 242 output << render_issues(issues, options)
243 243 options[:indent] -= options[:indent_increment]
244 244 end
245 245
246 246 output
247 247 end
248 248
249 249 def subject_for_project(project, options)
250 250 case options[:format]
251 251 when :html
252 252 output = ''
253 253
254 254 output << "<div class='project-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
255 255 if project.is_a? Project
256 256 output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
257 257 output << view.link_to_project(project)
258 258 output << '</span>'
259 259 else
260 260 ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
261 261 ''
262 262 end
263 263 output << "</small></div>"
264 264
265 265 output
266 266 when :image
267 267
268 268 options[:image].fill('black')
269 269 options[:image].stroke('transparent')
270 270 options[:image].stroke_width(1)
271 271 options[:image].text(options[:indent], options[:top] + 2, project.name)
272 272 when :pdf
273 273 options[:pdf].SetY(options[:top])
274 274 options[:pdf].SetX(15)
275 275
276 276 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
277 277 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
278 278
279 279 options[:pdf].SetY(options[:top])
280 280 options[:pdf].SetX(options[:subject_width])
281 281 options[:pdf].Cell(options[:g_width], 5, "", "LR")
282 282 end
283 283 end
284 284
285 285 def line_for_project(project, options)
286 # Skip versions that don't have a start_date
287 if project.is_a?(Project) && project.start_date
286 # Skip versions that don't have a start_date or due date
287 if project.is_a?(Project) && project.start_date && project.due_date
288 288 options[:zoom] ||= 1
289 289 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
290 290
291 291
292 292 case options[:format]
293 293 when :html
294 294 output = ''
295 295 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
296 296
297 297 start_date = project.start_date
298 298 start_date ||= self.date_from
299 299 start_left = ((start_date - self.date_from)*options[:zoom]).floor
300 300
301 301 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
302 302 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
303 303 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
304 304 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
305 305
306 306 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
307 307 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
308 308
309 309 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
310 310 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
311 311 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
312 312
313 313 # Bar graphic
314 314
315 315 # Make sure that negative i_left and i_width don't
316 316 # overflow the subject
317 317 if i_end > 0 && i_left <= options[:g_width]
318 318 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
319 319 end
320 320
321 321 if l_width > 0 && i_left <= options[:g_width]
322 322 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
323 323 end
324 324 if d_width > 0 && i_left <= options[:g_width]
325 325 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
326 326 end
327 327
328 328
329 329 # Starting diamond
330 330 if start_left <= options[:g_width] && start_left > 0
331 331 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
332 332 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
333 333 output << "</div>"
334 334 end
335 335
336 336 # Ending diamond
337 337 # Don't show items too far ahead
338 338 if i_end <= options[:g_width] && i_end > 0
339 339 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
340 340 end
341 341
342 342 # DIsplay the Project name and %
343 343 if i_end <= options[:g_width]
344 344 # Display the status even if it's floated off to the left
345 345 status_px = i_end + 12 # 12px for the diamond
346 346 status_px = 0 if status_px <= 0
347 347
348 348 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
349 349 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
350 350 output << "</div>"
351 351 end
352 352
353 353 output
354 354 when :image
355 355 options[:image].stroke('transparent')
356 356 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
357 357
358 358 # Make sure negative i_left doesn't overflow the subject
359 359 if i_left > options[:subject_width]
360 360 options[:image].fill('blue')
361 361 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
362 362 options[:image].fill('black')
363 363 options[:image].text(i_left + 11, options[:top] + 1, project.name)
364 364 end
365 365 when :pdf
366 366 options[:pdf].SetY(options[:top]+1.5)
367 367 i_left = ((project.due_date - @date_from)*options[:zoom])
368 368
369 369 # Make sure negative i_left doesn't overflow the subject
370 370 if i_left > 0
371 371 options[:pdf].SetX(options[:subject_width] + i_left)
372 372 options[:pdf].SetFillColor(50,50,200)
373 373 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
374 374
375 375 options[:pdf].SetY(options[:top]+1.5)
376 376 options[:pdf].SetX(options[:subject_width] + i_left + 3)
377 377 options[:pdf].Cell(30, 2, "#{project.name}")
378 378 end
379 379 end
380 380 else
381 381 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
382 382 ''
383 383 end
384 384 end
385 385
386 386 def subject_for_version(version, options)
387 387 case options[:format]
388 388 when :html
389 389 output = ''
390 390 output << "<div class='version-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
391 391 if version.is_a? Version
392 392 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
393 393 output << view.link_to_version(version)
394 394 output << '</span>'
395 395 else
396 396 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
397 397 ''
398 398 end
399 399 output << "</small></div>"
400 400
401 401 output
402 402 when :image
403 403 options[:image].fill('black')
404 404 options[:image].stroke('transparent')
405 405 options[:image].stroke_width(1)
406 406 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project)
407 407 when :pdf
408 408 options[:pdf].SetY(options[:top])
409 409 options[:pdf].SetX(15)
410 410
411 411 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
412 412 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
413 413
414 414 options[:pdf].SetY(options[:top])
415 415 options[:pdf].SetX(options[:subject_width])
416 416 options[:pdf].Cell(options[:g_width], 5, "", "LR")
417 417 end
418 418 end
419 419
420 420 def line_for_version(version, options)
421 421 # Skip versions that don't have a start_date
422 if version.is_a?(Version) && version.start_date
422 if version.is_a?(Version) && version.start_date && version.due_date
423 423 options[:zoom] ||= 1
424 424 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
425 425
426 426 case options[:format]
427 427 when :html
428 428 output = ''
429 429 i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
430 430 # TODO: or version.fixed_issues.collect(&:start_date).min
431 431 start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
432 432 start_date ||= self.date_from
433 433 start_left = ((start_date - self.date_from)*options[:zoom]).floor
434 434
435 435 i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
436 436 i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
437 437 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
438 438 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
439 439
440 440 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
441 441
442 442 i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
443 443 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
444 444 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
445 445
446 446 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
447 447
448 448 # Bar graphic
449 449
450 450 # Make sure that negative i_left and i_width don't
451 451 # overflow the subject
452 452 if i_width > 0 && i_left <= options[:g_width]
453 453 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
454 454 end
455 455 if l_width > 0 && i_left <= options[:g_width]
456 456 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
457 457 end
458 458 if d_width > 0 && i_left <= options[:g_width]
459 459 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
460 460 end
461 461
462 462
463 463 # Starting diamond
464 464 if start_left <= options[:g_width] && start_left > 0
465 465 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
466 466 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
467 467 output << "</div>"
468 468 end
469 469
470 470 # Ending diamond
471 471 # Don't show items too far ahead
472 472 if i_left <= options[:g_width] && i_end > 0
473 473 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
474 474 end
475 475
476 476 # Display the Version name and %
477 477 if i_end <= options[:g_width]
478 478 # Display the status even if it's floated off to the left
479 479 status_px = i_end + 12 # 12px for the diamond
480 480 status_px = 0 if status_px <= 0
481 481
482 482 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
483 483 output << h("#{version.project} -") unless @project && @project == version.project
484 484 output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
485 485 output << "</div>"
486 486 end
487 487
488 488 output
489 489 when :image
490 490 options[:image].stroke('transparent')
491 491 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
492 492
493 493 # Make sure negative i_left doesn't overflow the subject
494 494 if i_left > options[:subject_width]
495 495 options[:image].fill('green')
496 496 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
497 497 options[:image].fill('black')
498 498 options[:image].text(i_left + 11, options[:top] + 1, version.name)
499 499 end
500 500 when :pdf
501 501 options[:pdf].SetY(options[:top]+1.5)
502 502 i_left = ((version.start_date - @date_from)*options[:zoom])
503 503
504 504 # Make sure negative i_left doesn't overflow the subject
505 505 if i_left > 0
506 506 options[:pdf].SetX(options[:subject_width] + i_left)
507 507 options[:pdf].SetFillColor(50,200,50)
508 508 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
509 509
510 510 options[:pdf].SetY(options[:top]+1.5)
511 511 options[:pdf].SetX(options[:subject_width] + i_left + 3)
512 512 options[:pdf].Cell(30, 2, "#{version.name}")
513 513 end
514 514 end
515 515 else
516 516 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
517 517 ''
518 518 end
519 519 end
520 520
521 521 def subject_for_issue(issue, options)
522 522 case options[:format]
523 523 when :html
524 524 output = ''
525 525 output << "<div class='tooltip'>"
526 526 output << "<div class='issue-subject' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
527 527 if issue.is_a? Issue
528 528 css_classes = []
529 529 css_classes << 'issue-overdue' if issue.overdue?
530 530 css_classes << 'issue-behind-schedule' if issue.behind_schedule?
531 531 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
532 532
533 533 if issue.assigned_to.present?
534 534 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
535 535 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
536 536 end
537 537 output << "<span class='#{css_classes.join(' ')}'>"
538 538 output << view.link_to_issue(issue)
539 539 output << ":"
540 540 output << h(issue.subject)
541 541 output << '</span>'
542 542 else
543 543 ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
544 544 ''
545 545 end
546 546 output << "</small></div>"
547 547
548 548 # Tooltip
549 549 if issue.is_a? Issue
550 550 output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
551 551 output << view.render_issue_tooltip(issue)
552 552 output << "</span>"
553 553 end
554 554
555 555 output << "</div>"
556 556 output
557 557 when :image
558 558 options[:image].fill('black')
559 559 options[:image].stroke('transparent')
560 560 options[:image].stroke_width(1)
561 561 options[:image].text(options[:indent], options[:top] + 2, issue.subject)
562 562 when :pdf
563 563 options[:pdf].SetY(options[:top])
564 564 options[:pdf].SetX(15)
565 565
566 566 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
567 567 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
568 568
569 569 options[:pdf].SetY(options[:top])
570 570 options[:pdf].SetX(options[:subject_width])
571 571 options[:pdf].Cell(options[:g_width], 5, "", "LR")
572 572 end
573 573 end
574 574
575 575 def line_for_issue(issue, options)
576 576 # Skip issues that don't have a due_before (due_date or version's due_date)
577 577 if issue.is_a?(Issue) && issue.due_before
578 578 case options[:format]
579 579 when :html
580 580 output = ''
581 581 # Handle nil start_dates, rare but can happen.
582 582 i_start_date = if issue.start_date && issue.start_date >= self.date_from
583 583 issue.start_date
584 584 else
585 585 self.date_from
586 586 end
587 587
588 588 i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to )
589 589 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
590 590 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
591 591 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
592 592
593 593 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
594 594
595 595 i_left = ((i_start_date - self.date_from)*options[:zoom]).floor
596 596 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders)
597 597 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width
598 598 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
599 599 css = "task " + (issue.leaf? ? 'leaf' : 'parent')
600 600
601 601 # Make sure that negative i_left and i_width don't
602 602 # overflow the subject
603 603 if i_width > 0
604 604 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'>&nbsp;</div>"
605 605 end
606 606 if l_width > 0
607 607 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'>&nbsp;</div>"
608 608 end
609 609 if d_width > 0
610 610 output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'>&nbsp;</div>"
611 611 end
612 612
613 613 # Display the status even if it's floated off to the left
614 614 status_px = i_left + i_width + 5
615 615 status_px = 5 if status_px <= 0
616 616
617 617 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>"
618 618 output << issue.status.name
619 619 output << ' '
620 620 output << (issue.done_ratio).to_i.to_s
621 621 output << "%"
622 622 output << "</div>"
623 623
624 624 output << "<div class='tooltip' style='position: absolute;top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;height:12px;'>"
625 625 output << '<span class="tip">'
626 626 output << view.render_issue_tooltip(issue)
627 627 output << "</span></div>"
628 628 output
629 629
630 630 when :image
631 631 # Handle nil start_dates, rare but can happen.
632 632 i_start_date = if issue.start_date && issue.start_date >= @date_from
633 633 issue.start_date
634 634 else
635 635 @date_from
636 636 end
637 637
638 638 i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
639 639 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
640 640 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
641 641 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
642 642 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
643 643
644 644 i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
645 645 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
646 646 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
647 647 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
648 648
649 649
650 650 # Make sure that negative i_left and i_width don't
651 651 # overflow the subject
652 652 if i_width > 0
653 653 options[:image].fill('grey')
654 654 options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
655 655 options[:image].fill('red')
656 656 options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
657 657 options[:image].fill('blue')
658 658 options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
659 659 end
660 660
661 661 # Show the status and % done next to the subject if it overflows
662 662 options[:image].fill('black')
663 663 if i_width > 0
664 664 options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
665 665 else
666 666 options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
667 667 end
668 668
669 669 when :pdf
670 670 options[:pdf].SetY(options[:top]+1.5)
671 671 # Handle nil start_dates, rare but can happen.
672 672 i_start_date = if issue.start_date && issue.start_date >= @date_from
673 673 issue.start_date
674 674 else
675 675 @date_from
676 676 end
677 677
678 678 i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
679 679
680 680 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
681 681 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
682 682 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
683 683
684 684 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
685 685
686 686 i_left = ((i_start_date - @date_from)*options[:zoom])
687 687 i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
688 688 d_width = ((i_done_date - i_start_date)*options[:zoom])
689 689 l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
690 690 l_width ||= 0
691 691
692 692 # Make sure that negative i_left and i_width don't
693 693 # overflow the subject
694 694 if i_width > 0
695 695 options[:pdf].SetX(options[:subject_width] + i_left)
696 696 options[:pdf].SetFillColor(200,200,200)
697 697 options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
698 698 end
699 699
700 700 if l_width > 0
701 701 options[:pdf].SetY(options[:top]+1.5)
702 702 options[:pdf].SetX(options[:subject_width] + i_left)
703 703 options[:pdf].SetFillColor(255,100,100)
704 704 options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
705 705 end
706 706 if d_width > 0
707 707 options[:pdf].SetY(options[:top]+1.5)
708 708 options[:pdf].SetX(options[:subject_width] + i_left)
709 709 options[:pdf].SetFillColor(100,100,255)
710 710 options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
711 711 end
712 712
713 713 options[:pdf].SetY(options[:top]+1.5)
714 714
715 715 # Make sure that negative i_left and i_width don't
716 716 # overflow the subject
717 717 if (i_left + i_width) >= 0
718 718 options[:pdf].SetX(options[:subject_width] + i_left + i_width)
719 719 else
720 720 options[:pdf].SetX(options[:subject_width])
721 721 end
722 722 options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
723 723 end
724 724 else
725 725 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
726 726 ''
727 727 end
728 728 end
729 729
730 730 # Generates a gantt image
731 731 # Only defined if RMagick is avalaible
732 732 def to_image(format='PNG')
733 733 date_to = (@date_from >> @months)-1
734 734 show_weeks = @zoom > 1
735 735 show_days = @zoom > 2
736 736
737 737 subject_width = 400
738 738 header_heigth = 18
739 739 # width of one day in pixels
740 740 zoom = @zoom*2
741 741 g_width = (@date_to - @date_from + 1)*zoom
742 742 g_height = 20 * number_of_rows + 30
743 743 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
744 744 height = g_height + headers_heigth
745 745
746 746 imgl = Magick::ImageList.new
747 747 imgl.new_image(subject_width+g_width+1, height)
748 748 gc = Magick::Draw.new
749 749
750 750 # Subjects
751 751 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
752 752
753 753 # Months headers
754 754 month_f = @date_from
755 755 left = subject_width
756 756 @months.times do
757 757 width = ((month_f >> 1) - month_f) * zoom
758 758 gc.fill('white')
759 759 gc.stroke('grey')
760 760 gc.stroke_width(1)
761 761 gc.rectangle(left, 0, left + width, height)
762 762 gc.fill('black')
763 763 gc.stroke('transparent')
764 764 gc.stroke_width(1)
765 765 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
766 766 left = left + width
767 767 month_f = month_f >> 1
768 768 end
769 769
770 770 # Weeks headers
771 771 if show_weeks
772 772 left = subject_width
773 773 height = header_heigth
774 774 if @date_from.cwday == 1
775 775 # date_from is monday
776 776 week_f = date_from
777 777 else
778 778 # find next monday after date_from
779 779 week_f = @date_from + (7 - @date_from.cwday + 1)
780 780 width = (7 - @date_from.cwday + 1) * zoom
781 781 gc.fill('white')
782 782 gc.stroke('grey')
783 783 gc.stroke_width(1)
784 784 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
785 785 left = left + width
786 786 end
787 787 while week_f <= date_to
788 788 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
789 789 gc.fill('white')
790 790 gc.stroke('grey')
791 791 gc.stroke_width(1)
792 792 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
793 793 gc.fill('black')
794 794 gc.stroke('transparent')
795 795 gc.stroke_width(1)
796 796 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
797 797 left = left + width
798 798 week_f = week_f+7
799 799 end
800 800 end
801 801
802 802 # Days details (week-end in grey)
803 803 if show_days
804 804 left = subject_width
805 805 height = g_height + header_heigth - 1
806 806 wday = @date_from.cwday
807 807 (date_to - @date_from + 1).to_i.times do
808 808 width = zoom
809 809 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
810 810 gc.stroke('grey')
811 811 gc.stroke_width(1)
812 812 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
813 813 left = left + width
814 814 wday = wday + 1
815 815 wday = 1 if wday > 7
816 816 end
817 817 end
818 818
819 819 # border
820 820 gc.fill('transparent')
821 821 gc.stroke('grey')
822 822 gc.stroke_width(1)
823 823 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
824 824 gc.stroke('black')
825 825 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
826 826
827 827 # content
828 828 top = headers_heigth + 20
829 829
830 830 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
831 831
832 832 # today red line
833 833 if Date.today >= @date_from and Date.today <= date_to
834 834 gc.stroke('red')
835 835 x = (Date.today-@date_from+1)*zoom + subject_width
836 836 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
837 837 end
838 838
839 839 gc.draw(imgl)
840 840 imgl.format = format
841 841 imgl.to_blob
842 842 end if Object.const_defined?(:Magick)
843 843
844 844 def to_pdf
845 845 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
846 846 pdf.SetTitle("#{l(:label_gantt)} #{project}")
847 847 pdf.AliasNbPages
848 848 pdf.footer_date = format_date(Date.today)
849 849 pdf.AddPage("L")
850 850 pdf.SetFontStyle('B',12)
851 851 pdf.SetX(15)
852 852 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
853 853 pdf.Ln
854 854 pdf.SetFontStyle('B',9)
855 855
856 856 subject_width = PDF::LeftPaneWidth
857 857 header_heigth = 5
858 858
859 859 headers_heigth = header_heigth
860 860 show_weeks = false
861 861 show_days = false
862 862
863 863 if self.months < 7
864 864 show_weeks = true
865 865 headers_heigth = 2*header_heigth
866 866 if self.months < 3
867 867 show_days = true
868 868 headers_heigth = 3*header_heigth
869 869 end
870 870 end
871 871
872 872 g_width = PDF.right_pane_width
873 873 zoom = (g_width) / (self.date_to - self.date_from + 1)
874 874 g_height = 120
875 875 t_height = g_height + headers_heigth
876 876
877 877 y_start = pdf.GetY
878 878
879 879 # Months headers
880 880 month_f = self.date_from
881 881 left = subject_width
882 882 height = header_heigth
883 883 self.months.times do
884 884 width = ((month_f >> 1) - month_f) * zoom
885 885 pdf.SetY(y_start)
886 886 pdf.SetX(left)
887 887 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
888 888 left = left + width
889 889 month_f = month_f >> 1
890 890 end
891 891
892 892 # Weeks headers
893 893 if show_weeks
894 894 left = subject_width
895 895 height = header_heigth
896 896 if self.date_from.cwday == 1
897 897 # self.date_from is monday
898 898 week_f = self.date_from
899 899 else
900 900 # find next monday after self.date_from
901 901 week_f = self.date_from + (7 - self.date_from.cwday + 1)
902 902 width = (7 - self.date_from.cwday + 1) * zoom-1
903 903 pdf.SetY(y_start + header_heigth)
904 904 pdf.SetX(left)
905 905 pdf.Cell(width + 1, height, "", "LTR")
906 906 left = left + width+1
907 907 end
908 908 while week_f <= self.date_to
909 909 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
910 910 pdf.SetY(y_start + header_heigth)
911 911 pdf.SetX(left)
912 912 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
913 913 left = left + width
914 914 week_f = week_f+7
915 915 end
916 916 end
917 917
918 918 # Days headers
919 919 if show_days
920 920 left = subject_width
921 921 height = header_heigth
922 922 wday = self.date_from.cwday
923 923 pdf.SetFontStyle('B',7)
924 924 (self.date_to - self.date_from + 1).to_i.times do
925 925 width = zoom
926 926 pdf.SetY(y_start + 2 * header_heigth)
927 927 pdf.SetX(left)
928 928 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
929 929 left = left + width
930 930 wday = wday + 1
931 931 wday = 1 if wday > 7
932 932 end
933 933 end
934 934
935 935 pdf.SetY(y_start)
936 936 pdf.SetX(15)
937 937 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
938 938
939 939 # Tasks
940 940 top = headers_heigth + y_start
941 941 pdf_subjects_and_lines(pdf, {
942 942 :top => top,
943 943 :zoom => zoom,
944 944 :subject_width => subject_width,
945 945 :g_width => g_width
946 946 })
947 947
948 948
949 949 pdf.Line(15, top, subject_width+g_width, top)
950 950 pdf.Output
951 951
952 952
953 953 end
954 954
955 955 private
956 956
957 957 # Renders both the subjects and lines of the Gantt chart for the
958 958 # PDF format
959 959 def pdf_subjects_and_lines(pdf, options = {})
960 960 subject_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :subject, :format => :pdf, :pdf => pdf}.merge(options)
961 961 line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options)
962 962
963 963 if @project
964 964 render_project(@project, subject_options)
965 965 render_project(@project, line_options)
966 966 else
967 967 Project.roots.each do |project|
968 968 render_project(project, subject_options)
969 969 render_project(project, line_options)
970 970 end
971 971 end
972 972 end
973 973
974 974 end
975 975 end
976 976 end
@@ -1,65 +1,75
1 1 require File.dirname(__FILE__) + '/../test_helper'
2 2
3 3 class GanttsControllerTest < ActionController::TestCase
4 4 fixtures :all
5 5
6 6 context "#gantt" do
7 7 should "work" do
8 8 i2 = Issue.find(2)
9 9 i2.update_attribute(:due_date, 1.month.from_now)
10 10
11 11 get :show, :project_id => 1
12 12 assert_response :success
13 13 assert_template 'show.html.erb'
14 14 assert_not_nil assigns(:gantt)
15 15 # Issue with start and due dates
16 16 i = Issue.find(1)
17 17 assert_not_nil i.due_date
18 18 assert_select "div a.issue", /##{i.id}/
19 19 # Issue with on a targeted version should not be in the events but loaded in the html
20 20 i = Issue.find(2)
21 21 assert_select "div a.issue", /##{i.id}/
22 22 end
23 23
24 should_eventually "work without issue due dates" do
24 should "work without issue due dates" do
25 25 Issue.update_all("due_date = NULL")
26 26
27 27 get :show, :project_id => 1
28 28 assert_response :success
29 29 assert_template 'show.html.erb'
30 30 assert_not_nil assigns(:gantt)
31 31 end
32
33 should "work without issue and version due dates" do
34 Issue.update_all("due_date = NULL")
35 Version.update_all("effective_date = NULL")
36
37 get :show, :project_id => 1
38 assert_response :success
39 assert_template 'show.html.erb'
40 assert_not_nil assigns(:gantt)
41 end
32 42
33 43 should "work cross project" do
34 44 get :show
35 45 assert_response :success
36 46 assert_template 'show.html.erb'
37 47 assert_not_nil assigns(:gantt)
38 48 assert_not_nil assigns(:gantt).query
39 49 assert_nil assigns(:gantt).project
40 50 end
41 51
42 52 should "export to pdf" do
43 53 get :show, :project_id => 1, :format => 'pdf'
44 54 assert_response :success
45 55 assert_equal 'application/pdf', @response.content_type
46 56 assert @response.body.starts_with?('%PDF')
47 57 assert_not_nil assigns(:gantt)
48 58 end
49 59
50 60 should "export to pdf cross project" do
51 61 get :show, :format => 'pdf'
52 62 assert_response :success
53 63 assert_equal 'application/pdf', @response.content_type
54 64 assert @response.body.starts_with?('%PDF')
55 65 assert_not_nil assigns(:gantt)
56 66 end
57 67
58 68 should "export to png" do
59 69 get :show, :project_id => 1, :format => 'png'
60 70 assert_response :success
61 71 assert_equal 'image/png', @response.content_type
62 72 end if Object.const_defined?(:Magick)
63 73
64 74 end
65 75 end
General Comments 0
You need to be logged in to leave comments. Login now