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