##// END OF EJS Templates
Fixed: new gantt chart discloses all private projects names (#6276)....
Jean-Philippe Lang -
r4311:dfc76ce642ee
parent child
Show More
@@ -1,987 +1,987
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 Project.roots.inject(0) do |total, project|
94 Project.roots.visible.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 project.children.each do |subproject|
122 project.children.visible.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 Project.roots.each do |project|
137 Project.roots.visible.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 Project.roots.each do |project|
153 Project.roots.visible.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 sort_issues!(issues)
182 182 if issues
183 183 issue_rendering = render_issues(issues, options)
184 184 output << issue_rendering if options[:format] == :html
185 185 end
186 186
187 187 # Third, Versions
188 188 project.versions.sort.each do |version|
189 189 version_rendering = render_version(version, options)
190 190 output << version_rendering if options[:format] == :html
191 191 end
192 192
193 193 # Fourth, subprojects
194 project.children.each do |project|
194 project.children.visible.each do |project|
195 195 subproject_rendering = render_project(project, options)
196 196 output << subproject_rendering if options[:format] == :html
197 197 end
198 198
199 199 # Remove indent to hit the next sibling
200 200 options[:indent] -= options[:indent_increment]
201 201
202 202 output
203 203 end
204 204
205 205 def render_issues(issues, options={})
206 206 output = ''
207 207 issues.each do |i|
208 208 issue_rendering = if options[:render] == :subject
209 209 subject_for_issue(i, options)
210 210 else
211 211 # :line
212 212 line_for_issue(i, options)
213 213 end
214 214 output << issue_rendering if options[:format] == :html
215 215 options[:top] += options[:top_increment]
216 216 end
217 217 output
218 218 end
219 219
220 220 def render_version(version, options={})
221 221 output = ''
222 222 # Version header
223 223 version_rendering = if options[:render] == :subject
224 224 subject_for_version(version, options)
225 225 else
226 226 # :line
227 227 line_for_version(version, options)
228 228 end
229 229
230 230 output << version_rendering if options[:format] == :html
231 231
232 232 options[:top] += options[:top_increment]
233 233
234 234 # Remove the project requirement for Versions because it will
235 235 # restrict issues to only be on the current project. This
236 236 # ends up missing issues which are assigned to shared versions.
237 237 @query.project = nil if @query.project
238 238
239 239 issues = version.fixed_issues.for_gantt.with_query(@query)
240 240 if issues
241 241 sort_issues!(issues)
242 242 # Indent issues
243 243 options[:indent] += options[:indent_increment]
244 244 output << render_issues(issues, options)
245 245 options[:indent] -= options[:indent_increment]
246 246 end
247 247
248 248 output
249 249 end
250 250
251 251 def subject_for_project(project, options)
252 252 case options[:format]
253 253 when :html
254 254 output = ''
255 255
256 256 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> "
257 257 if project.is_a? Project
258 258 output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
259 259 output << view.link_to_project(project)
260 260 output << '</span>'
261 261 else
262 262 ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
263 263 ''
264 264 end
265 265 output << "</small></div>"
266 266
267 267 output
268 268 when :image
269 269
270 270 options[:image].fill('black')
271 271 options[:image].stroke('transparent')
272 272 options[:image].stroke_width(1)
273 273 options[:image].text(options[:indent], options[:top] + 2, project.name)
274 274 when :pdf
275 275 options[:pdf].SetY(options[:top])
276 276 options[:pdf].SetX(15)
277 277
278 278 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
279 279 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
280 280
281 281 options[:pdf].SetY(options[:top])
282 282 options[:pdf].SetX(options[:subject_width])
283 283 options[:pdf].Cell(options[:g_width], 5, "", "LR")
284 284 end
285 285 end
286 286
287 287 def line_for_project(project, options)
288 288 # Skip versions that don't have a start_date or due date
289 289 if project.is_a?(Project) && project.start_date && project.due_date
290 290 options[:zoom] ||= 1
291 291 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
292 292
293 293
294 294 case options[:format]
295 295 when :html
296 296 output = ''
297 297 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
298 298
299 299 start_date = project.start_date
300 300 start_date ||= self.date_from
301 301 start_left = ((start_date - self.date_from)*options[:zoom]).floor
302 302
303 303 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
304 304 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
305 305 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
306 306 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
307 307
308 308 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
309 309 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
310 310
311 311 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
312 312 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
313 313 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
314 314
315 315 # Bar graphic
316 316
317 317 # Make sure that negative i_left and i_width don't
318 318 # overflow the subject
319 319 if i_end > 0 && i_left <= options[:g_width]
320 320 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
321 321 end
322 322
323 323 if l_width > 0 && i_left <= options[:g_width]
324 324 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
325 325 end
326 326 if d_width > 0 && i_left <= options[:g_width]
327 327 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
328 328 end
329 329
330 330
331 331 # Starting diamond
332 332 if start_left <= options[:g_width] && start_left > 0
333 333 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
334 334 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
335 335 output << "</div>"
336 336 end
337 337
338 338 # Ending diamond
339 339 # Don't show items too far ahead
340 340 if i_end <= options[:g_width] && i_end > 0
341 341 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
342 342 end
343 343
344 344 # DIsplay the Project name and %
345 345 if i_end <= options[:g_width]
346 346 # Display the status even if it's floated off to the left
347 347 status_px = i_end + 12 # 12px for the diamond
348 348 status_px = 0 if status_px <= 0
349 349
350 350 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
351 351 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
352 352 output << "</div>"
353 353 end
354 354
355 355 output
356 356 when :image
357 357 options[:image].stroke('transparent')
358 358 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
359 359
360 360 # Make sure negative i_left doesn't overflow the subject
361 361 if i_left > options[:subject_width]
362 362 options[:image].fill('blue')
363 363 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
364 364 options[:image].fill('black')
365 365 options[:image].text(i_left + 11, options[:top] + 1, project.name)
366 366 end
367 367 when :pdf
368 368 options[:pdf].SetY(options[:top]+1.5)
369 369 i_left = ((project.due_date - @date_from)*options[:zoom])
370 370
371 371 # Make sure negative i_left doesn't overflow the subject
372 372 if i_left > 0
373 373 options[:pdf].SetX(options[:subject_width] + i_left)
374 374 options[:pdf].SetFillColor(50,50,200)
375 375 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
376 376
377 377 options[:pdf].SetY(options[:top]+1.5)
378 378 options[:pdf].SetX(options[:subject_width] + i_left + 3)
379 379 options[:pdf].Cell(30, 2, "#{project.name}")
380 380 end
381 381 end
382 382 else
383 383 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
384 384 ''
385 385 end
386 386 end
387 387
388 388 def subject_for_version(version, options)
389 389 case options[:format]
390 390 when :html
391 391 output = ''
392 392 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> "
393 393 if version.is_a? Version
394 394 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
395 395 output << view.link_to_version(version)
396 396 output << '</span>'
397 397 else
398 398 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
399 399 ''
400 400 end
401 401 output << "</small></div>"
402 402
403 403 output
404 404 when :image
405 405 options[:image].fill('black')
406 406 options[:image].stroke('transparent')
407 407 options[:image].stroke_width(1)
408 408 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project)
409 409 when :pdf
410 410 options[:pdf].SetY(options[:top])
411 411 options[:pdf].SetX(15)
412 412
413 413 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
414 414 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
415 415
416 416 options[:pdf].SetY(options[:top])
417 417 options[:pdf].SetX(options[:subject_width])
418 418 options[:pdf].Cell(options[:g_width], 5, "", "LR")
419 419 end
420 420 end
421 421
422 422 def line_for_version(version, options)
423 423 # Skip versions that don't have a start_date
424 424 if version.is_a?(Version) && version.start_date && version.due_date
425 425 options[:zoom] ||= 1
426 426 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
427 427
428 428 case options[:format]
429 429 when :html
430 430 output = ''
431 431 i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
432 432 # TODO: or version.fixed_issues.collect(&:start_date).min
433 433 start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
434 434 start_date ||= self.date_from
435 435 start_left = ((start_date - self.date_from)*options[:zoom]).floor
436 436
437 437 i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
438 438 i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
439 439 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
440 440 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
441 441
442 442 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
443 443
444 444 i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
445 445 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
446 446 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
447 447
448 448 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
449 449
450 450 # Bar graphic
451 451
452 452 # Make sure that negative i_left and i_width don't
453 453 # overflow the subject
454 454 if i_width > 0 && i_left <= options[:g_width]
455 455 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
456 456 end
457 457 if l_width > 0 && i_left <= options[:g_width]
458 458 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
459 459 end
460 460 if d_width > 0 && i_left <= options[:g_width]
461 461 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
462 462 end
463 463
464 464
465 465 # Starting diamond
466 466 if start_left <= options[:g_width] && start_left > 0
467 467 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
468 468 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
469 469 output << "</div>"
470 470 end
471 471
472 472 # Ending diamond
473 473 # Don't show items too far ahead
474 474 if i_left <= options[:g_width] && i_end > 0
475 475 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
476 476 end
477 477
478 478 # Display the Version name and %
479 479 if i_end <= options[:g_width]
480 480 # Display the status even if it's floated off to the left
481 481 status_px = i_end + 12 # 12px for the diamond
482 482 status_px = 0 if status_px <= 0
483 483
484 484 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
485 485 output << h("#{version.project} -") unless @project && @project == version.project
486 486 output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
487 487 output << "</div>"
488 488 end
489 489
490 490 output
491 491 when :image
492 492 options[:image].stroke('transparent')
493 493 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
494 494
495 495 # Make sure negative i_left doesn't overflow the subject
496 496 if i_left > options[:subject_width]
497 497 options[:image].fill('green')
498 498 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
499 499 options[:image].fill('black')
500 500 options[:image].text(i_left + 11, options[:top] + 1, version.name)
501 501 end
502 502 when :pdf
503 503 options[:pdf].SetY(options[:top]+1.5)
504 504 i_left = ((version.start_date - @date_from)*options[:zoom])
505 505
506 506 # Make sure negative i_left doesn't overflow the subject
507 507 if i_left > 0
508 508 options[:pdf].SetX(options[:subject_width] + i_left)
509 509 options[:pdf].SetFillColor(50,200,50)
510 510 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
511 511
512 512 options[:pdf].SetY(options[:top]+1.5)
513 513 options[:pdf].SetX(options[:subject_width] + i_left + 3)
514 514 options[:pdf].Cell(30, 2, "#{version.name}")
515 515 end
516 516 end
517 517 else
518 518 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
519 519 ''
520 520 end
521 521 end
522 522
523 523 def subject_for_issue(issue, options)
524 524 case options[:format]
525 525 when :html
526 526 output = ''
527 527 output << "<div class='tooltip'>"
528 528 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> "
529 529 if issue.is_a? Issue
530 530 css_classes = []
531 531 css_classes << 'issue-overdue' if issue.overdue?
532 532 css_classes << 'issue-behind-schedule' if issue.behind_schedule?
533 533 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
534 534
535 535 if issue.assigned_to.present?
536 536 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
537 537 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
538 538 end
539 539 output << "<span class='#{css_classes.join(' ')}'>"
540 540 output << view.link_to_issue(issue)
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 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
958 958 def sort_issues!(issues)
959 959 issues.sort! do |a, b|
960 960 cmp = 0
961 961 cmp = (a.start_date <=> b.start_date) if a.start_date? && b.start_date?
962 962 cmp = (a.due_date <=> b.due_date) if cmp == 0 && a.due_date? && b.due_date?
963 963 cmp = (a.id <=> b.id) if cmp == 0
964 964 cmp
965 965 end
966 966 end
967 967
968 968 # Renders both the subjects and lines of the Gantt chart for the
969 969 # PDF format
970 970 def pdf_subjects_and_lines(pdf, options = {})
971 971 subject_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :subject, :format => :pdf, :pdf => pdf}.merge(options)
972 972 line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options)
973 973
974 974 if @project
975 975 render_project(@project, subject_options)
976 976 render_project(@project, line_options)
977 977 else
978 978 Project.roots.each do |project|
979 979 render_project(project, subject_options)
980 980 render_project(project, line_options)
981 981 end
982 982 end
983 983 end
984 984
985 985 end
986 986 end
987 987 end
@@ -1,75 +1,87
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 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 32
33 33 should "work without issue and version due dates" do
34 34 Issue.update_all("due_date = NULL")
35 35 Version.update_all("effective_date = NULL")
36 36
37 37 get :show, :project_id => 1
38 38 assert_response :success
39 39 assert_template 'show.html.erb'
40 40 assert_not_nil assigns(:gantt)
41 41 end
42 42
43 43 should "work cross project" do
44 44 get :show
45 45 assert_response :success
46 46 assert_template 'show.html.erb'
47 47 assert_not_nil assigns(:gantt)
48 48 assert_not_nil assigns(:gantt).query
49 49 assert_nil assigns(:gantt).project
50 50 end
51 51
52 should "not disclose private projects" do
53 get :show
54 assert_response :success
55 assert_template 'show.html.erb'
56
57 assert_tag 'a', :content => /eCookbook/
58 # Root private project
59 assert_no_tag 'a', {:content => /OnlineStore/}
60 # Private children of a public project
61 assert_no_tag 'a', :content => /Private child of eCookbook/
62 end
63
52 64 should "export to pdf" do
53 65 get :show, :project_id => 1, :format => 'pdf'
54 66 assert_response :success
55 67 assert_equal 'application/pdf', @response.content_type
56 68 assert @response.body.starts_with?('%PDF')
57 69 assert_not_nil assigns(:gantt)
58 70 end
59 71
60 72 should "export to pdf cross project" do
61 73 get :show, :format => 'pdf'
62 74 assert_response :success
63 75 assert_equal 'application/pdf', @response.content_type
64 76 assert @response.body.starts_with?('%PDF')
65 77 assert_not_nil assigns(:gantt)
66 78 end
67 79
68 80 should "export to png" do
69 81 get :show, :project_id => 1, :format => 'png'
70 82 assert_response :success
71 83 assert_equal 'image/png', @response.content_type
72 84 end if Object.const_defined?(:Magick)
73 85
74 86 end
75 87 end
General Comments 0
You need to be logged in to leave comments. Login now