comparison lib/redmine/helpers/gantt.rb @ 1464:261b3d9a4903 redmine-2.4

Update to Redmine 2.4 branch rev 12663
author Chris Cannam
date Tue, 14 Jan 2014 14:37:42 +0000
parents 433d4f72a19b
children e248c7af89ec
comparison
equal deleted inserted replaced
1296:038ba2d95de8 1464:261b3d9a4903
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 # 3 #
4 # This program is free software; you can redistribute it and/or 4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License 5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2 6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version. 7 # of the License, or (at your option) any later version.
21 class Gantt 21 class Gantt
22 include ERB::Util 22 include ERB::Util
23 include Redmine::I18n 23 include Redmine::I18n
24 include Redmine::Utils::DateCalculation 24 include Redmine::Utils::DateCalculation
25 25
26 # Relation types that are rendered
27 DRAW_TYPES = {
28 IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' },
29 IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
30 }.freeze
31
26 # :nodoc: 32 # :nodoc:
27 # Some utility methods for the PDF export 33 # Some utility methods for the PDF export
28 class PDF 34 class PDF
29 MaxCharactorsForSubject = 45 35 MaxCharactorsForSubject = 45
30 TotalWidth = 280 36 TotalWidth = 280
134 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC", 140 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
135 :limit => @max_rows 141 :limit => @max_rows
136 ) 142 )
137 end 143 end
138 144
145 # Returns a hash of the relations between the issues that are present on the gantt
146 # and that should be displayed, grouped by issue ids.
147 def relations
148 return @relations if @relations
149 if issues.any?
150 issue_ids = issues.map(&:id)
151 @relations = IssueRelation.
152 where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys).
153 group_by(&:issue_from_id)
154 else
155 @relations = {}
156 end
157 end
158
139 # Return all the project nodes that will be displayed 159 # Return all the project nodes that will be displayed
140 def projects 160 def projects
141 return @projects if @projects 161 return @projects if @projects
142 ids = issues.collect(&:project).uniq.collect(&:id) 162 ids = issues.collect(&:project).uniq.collect(&:id)
143 if ids.any? 163 if ids.any?
144 # All issues projects and their visible ancestors 164 # All issues projects and their visible ancestors
145 @projects = Project.visible.all( 165 @projects = Project.visible.
146 :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt", 166 joins("LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt").
147 :conditions => ["child.id IN (?)", ids], 167 where("child.id IN (?)", ids).
148 :order => "#{Project.table_name}.lft ASC" 168 order("#{Project.table_name}.lft ASC").
149 ).uniq 169 uniq.
170 all
150 else 171 else
151 @projects = [] 172 @projects = []
152 end 173 end
153 end 174 end
154 175
192 options[:top] += options[:top_increment] 213 options[:top] += options[:top_increment]
193 options[:indent] += options[:indent_increment] 214 options[:indent] += options[:indent_increment]
194 @number_of_rows += 1 215 @number_of_rows += 1
195 return if abort? 216 return if abort?
196 issues = project_issues(project).select {|i| i.fixed_version.nil?} 217 issues = project_issues(project).select {|i| i.fixed_version.nil?}
197 sort_issues!(issues) 218 self.class.sort_issues!(issues)
198 if issues 219 if issues
199 render_issues(issues, options) 220 render_issues(issues, options)
200 return if abort? 221 return if abort?
201 end 222 end
202 versions = project_versions(project) 223 versions = project_versions(project)
224 self.class.sort_versions!(versions)
203 versions.each do |version| 225 versions.each do |version|
204 render_version(project, version, options) 226 render_version(project, version, options)
205 end 227 end
206 # Remove indent to hit the next sibling 228 # Remove indent to hit the next sibling
207 options[:indent] -= options[:indent_increment] 229 options[:indent] -= options[:indent_increment]
226 options[:top] += options[:top_increment] 248 options[:top] += options[:top_increment]
227 @number_of_rows += 1 249 @number_of_rows += 1
228 return if abort? 250 return if abort?
229 issues = version_issues(project, version) 251 issues = version_issues(project, version)
230 if issues 252 if issues
231 sort_issues!(issues) 253 self.class.sort_issues!(issues)
232 # Indent issues 254 # Indent issues
233 options[:indent] += options[:indent_increment] 255 options[:indent] += options[:indent_increment]
234 render_issues(issues, options) 256 render_issues(issues, options)
235 options[:indent] -= options[:indent_increment] 257 options[:indent] -= options[:indent_increment]
236 end 258 end
275 image_task(options, coords, :label => label, :markers => true, :height => 3) 297 image_task(options, coords, :label => label, :markers => true, :height => 3)
276 when :pdf 298 when :pdf
277 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) 299 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
278 end 300 end
279 else 301 else
280 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
281 '' 302 ''
282 end 303 end
283 end 304 end
284 305
285 def subject_for_version(version, options) 306 def subject_for_version(version, options)
287 when :html 308 when :html
288 html_class = "" 309 html_class = ""
289 html_class << 'icon icon-package ' 310 html_class << 'icon icon-package '
290 html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " 311 html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
291 html_class << (version.overdue? ? 'version-overdue' : '') 312 html_class << (version.overdue? ? 'version-overdue' : '')
313 html_class << ' version-closed' unless version.open?
314 if version.start_date && version.due_date && version.completed_pourcent
315 progress_date = calc_progress_date(version.start_date,
316 version.due_date, version.completed_pourcent)
317 html_class << ' behind-start-date' if progress_date < self.date_from
318 html_class << ' over-end-date' if progress_date > self.date_to
319 end
292 s = view.link_to_version(version).html_safe 320 s = view.link_to_version(version).html_safe
293 subject = view.content_tag(:span, s, 321 subject = view.content_tag(:span, s,
294 :class => html_class).html_safe 322 :class => html_class).html_safe
295 html_subject(options, subject, :css => "version-name") 323 html_subject(options, subject, :css => "version-name",
324 :id => "version-#{version.id}")
296 when :image 325 when :image
297 image_subject(options, version.to_s_with_project) 326 image_subject(options, version.to_s_with_project)
298 when :pdf 327 when :pdf
299 pdf_new_page?(options) 328 pdf_new_page?(options)
300 pdf_subject(options, version.to_s_with_project) 329 pdf_subject(options, version.to_s_with_project)
301 end 330 end
302 end 331 end
303 332
304 def line_for_version(version, options) 333 def line_for_version(version, options)
305 # Skip versions that don't have a start_date 334 # Skip versions that don't have a start_date
306 if version.is_a?(Version) && version.start_date && version.due_date 335 if version.is_a?(Version) && version.due_date && version.start_date
307 options[:zoom] ||= 1 336 options[:zoom] ||= 1
308 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] 337 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
309 coords = coordinates(version.start_date, 338 coords = coordinates(version.start_date,
310 version.due_date, version.completed_pourcent, 339 version.due_date, version.completed_percent,
311 options[:zoom]) 340 options[:zoom])
312 label = "#{h version} #{h version.completed_pourcent.to_i.to_s}%" 341 label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
313 label = h("#{version.project} -") + label unless @project && @project == version.project 342 label = h("#{version.project} -") + label unless @project && @project == version.project
314 case options[:format] 343 case options[:format]
315 when :html 344 when :html
316 html_task(options, coords, :css => "version task", :label => label, :markers => true) 345 html_task(options, coords, :css => "version task",
346 :label => label, :markers => true, :version => version)
317 when :image 347 when :image
318 image_task(options, coords, :label => label, :markers => true, :height => 3) 348 image_task(options, coords, :label => label, :markers => true, :height => 3)
319 when :pdf 349 when :pdf
320 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) 350 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
321 end 351 end
322 else 352 else
323 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
324 '' 353 ''
325 end 354 end
326 end 355 end
327 356
328 def subject_for_issue(issue, options) 357 def subject_for_issue(issue, options)
334 when :html 363 when :html
335 css_classes = '' 364 css_classes = ''
336 css_classes << ' issue-overdue' if issue.overdue? 365 css_classes << ' issue-overdue' if issue.overdue?
337 css_classes << ' issue-behind-schedule' if issue.behind_schedule? 366 css_classes << ' issue-behind-schedule' if issue.behind_schedule?
338 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to 367 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
368 css_classes << ' issue-closed' if issue.closed?
369 if issue.start_date && issue.due_before && issue.done_ratio
370 progress_date = calc_progress_date(issue.start_date,
371 issue.due_before, issue.done_ratio)
372 css_classes << ' behind-start-date' if progress_date < self.date_from
373 css_classes << ' over-end-date' if progress_date > self.date_to
374 end
339 s = "".html_safe 375 s = "".html_safe
340 if issue.assigned_to.present? 376 if issue.assigned_to.present?
341 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name 377 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
342 s << view.avatar(issue.assigned_to, 378 s << view.avatar(issue.assigned_to,
343 :class => 'gravatar icon-gravatar', 379 :class => 'gravatar icon-gravatar',
345 :title => assigned_string).to_s.html_safe 381 :title => assigned_string).to_s.html_safe
346 end 382 end
347 s << view.link_to_issue(issue).html_safe 383 s << view.link_to_issue(issue).html_safe
348 subject = view.content_tag(:span, s, :class => css_classes).html_safe 384 subject = view.content_tag(:span, s, :class => css_classes).html_safe
349 html_subject(options, subject, :css => "issue-subject", 385 html_subject(options, subject, :css => "issue-subject",
350 :title => issue.subject) + "\n" 386 :title => issue.subject, :id => "issue-#{issue.id}") + "\n"
351 when :image 387 when :image
352 image_subject(options, issue.subject) 388 image_subject(options, issue.subject)
353 when :pdf 389 when :pdf
354 pdf_new_page?(options) 390 pdf_new_page?(options)
355 pdf_subject(options, issue.subject) 391 pdf_subject(options, issue.subject)
376 image_task(options, coords, :label => label) 412 image_task(options, coords, :label => label)
377 when :pdf 413 when :pdf
378 pdf_task(options, coords, :label => label) 414 pdf_task(options, coords, :label => label)
379 end 415 end
380 else 416 else
381 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
382 '' 417 ''
383 end 418 end
384 end 419 end
385 420
386 # Generates a gantt image 421 # Generates a gantt image
609 coords[:bar_end] = end_date - self.date_from + 1 644 coords[:bar_end] = end_date - self.date_from + 1
610 else 645 else
611 coords[:bar_end] = self.date_to - self.date_from + 1 646 coords[:bar_end] = self.date_to - self.date_from + 1
612 end 647 end
613 if progress 648 if progress
614 progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0) 649 progress_date = calc_progress_date(start_date, end_date, progress)
615 if progress_date > self.date_from && progress_date > start_date 650 if progress_date > self.date_from && progress_date > start_date
616 if progress_date < self.date_to 651 if progress_date < self.date_to
617 coords[:bar_progress_end] = progress_date - self.date_from 652 coords[:bar_progress_end] = progress_date - self.date_from
618 else 653 else
619 coords[:bar_progress_end] = self.date_to - self.date_from + 1 654 coords[:bar_progress_end] = self.date_to - self.date_from + 1
636 coords[key] = (coords[key] * zoom).floor 671 coords[key] = (coords[key] * zoom).floor
637 end 672 end
638 coords 673 coords
639 end 674 end
640 675
641 # Sorts a collection of issues by start_date, due_date, id for gantt rendering 676 def calc_progress_date(start_date, end_date, progress)
642 def sort_issues!(issues) 677 start_date + (end_date - start_date + 1) * (progress / 100.0)
643 issues.sort! { |a, b| gantt_issue_compare(a, b) } 678 end
644 end 679
645 680 def self.sort_issues!(issues)
646 # TODO: top level issues should be sorted by start date 681 issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)}
647 def gantt_issue_compare(x, y) 682 end
648 if x.root_id == y.root_id 683
649 x.lft <=> y.lft 684 def self.sort_issue_logic(issue)
650 else 685 julian_date = Date.new()
651 x.root_id <=> y.root_id 686 ancesters_start_date = []
652 end 687 current_issue = issue
688 begin
689 ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
690 current_issue = current_issue.parent
691 end while (current_issue)
692 ancesters_start_date
693 end
694
695 def self.sort_versions!(versions)
696 versions.sort!
653 end 697 end
654 698
655 def current_limit 699 def current_limit
656 if @max_rows 700 if @max_rows
657 @max_rows - @number_of_rows 701 @max_rows - @number_of_rows
676 end 720 end
677 721
678 def html_subject(params, subject, options={}) 722 def html_subject(params, subject, options={})
679 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;" 723 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
680 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width] 724 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
681 output = view.content_tag('div', subject, 725 output = view.content_tag(:div, subject,
682 :class => options[:css], :style => style, 726 :class => options[:css], :style => style,
683 :title => options[:title]) 727 :title => options[:title],
728 :id => options[:id])
684 @subjects << output 729 @subjects << output
685 output 730 output
686 end 731 end
687 732
688 def pdf_subject(params, subject, options={}) 733 def pdf_subject(params, subject, options={})
703 params[:image].stroke('transparent') 748 params[:image].stroke('transparent')
704 params[:image].stroke_width(1) 749 params[:image].stroke_width(1)
705 params[:image].text(params[:indent], params[:top] + 2, subject) 750 params[:image].text(params[:indent], params[:top] + 2, subject)
706 end 751 end
707 752
753 def issue_relations(issue)
754 rels = {}
755 if relations[issue.id]
756 relations[issue.id].each do |relation|
757 (rels[relation.relation_type] ||= []) << relation.issue_to_id
758 end
759 end
760 rels
761 end
762
708 def html_task(params, coords, options={}) 763 def html_task(params, coords, options={})
709 output = '' 764 output = ''
710 # Renders the task bar, with progress and late 765 # Renders the task bar, with progress and late
711 if coords[:bar_start] && coords[:bar_end] 766 if coords[:bar_start] && coords[:bar_end]
712 width = coords[:bar_end] - coords[:bar_start] - 2 767 width = coords[:bar_end] - coords[:bar_start] - 2
713 style = "" 768 style = ""
714 style << "top:#{params[:top]}px;" 769 style << "top:#{params[:top]}px;"
715 style << "left:#{coords[:bar_start]}px;" 770 style << "left:#{coords[:bar_start]}px;"
716 style << "width:#{width}px;" 771 style << "width:#{width}px;"
717 output << view.content_tag(:div, '&nbsp;'.html_safe, 772 html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
718 :style => style, 773 html_id = "task-todo-version-#{options[:version].id}" if options[:version]
719 :class => "#{options[:css]} task_todo") 774 content_opt = {:style => style,
775 :class => "#{options[:css]} task_todo",
776 :id => html_id}
777 if options[:issue]
778 rels = issue_relations(options[:issue])
779 if rels.present?
780 content_opt[:data] = {"rels" => rels.to_json}
781 end
782 end
783 output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
720 if coords[:bar_late_end] 784 if coords[:bar_late_end]
721 width = coords[:bar_late_end] - coords[:bar_start] - 2 785 width = coords[:bar_late_end] - coords[:bar_start] - 2
722 style = "" 786 style = ""
723 style << "top:#{params[:top]}px;" 787 style << "top:#{params[:top]}px;"
724 style << "left:#{coords[:bar_start]}px;" 788 style << "left:#{coords[:bar_start]}px;"
731 width = coords[:bar_progress_end] - coords[:bar_start] - 2 795 width = coords[:bar_progress_end] - coords[:bar_start] - 2
732 style = "" 796 style = ""
733 style << "top:#{params[:top]}px;" 797 style << "top:#{params[:top]}px;"
734 style << "left:#{coords[:bar_start]}px;" 798 style << "left:#{coords[:bar_start]}px;"
735 style << "width:#{width}px;" 799 style << "width:#{width}px;"
800 html_id = "task-done-issue-#{options[:issue].id}" if options[:issue]
801 html_id = "task-done-version-#{options[:version].id}" if options[:version]
736 output << view.content_tag(:div, '&nbsp;'.html_safe, 802 output << view.content_tag(:div, '&nbsp;'.html_safe,
737 :style => style, 803 :style => style,
738 :class => "#{options[:css]} task_done") 804 :class => "#{options[:css]} task_done",
805 :id => html_id)
739 end 806 end
740 end 807 end
741 # Renders the markers 808 # Renders the markers
742 if options[:markers] 809 if options[:markers]
743 if coords[:start] 810 if coords[:start]