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