Mercurial > hg > soundsoftware-site
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, ' '.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, ' '.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, ' '.html_safe, | 802 output << view.content_tag(:div, ' '.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] |