From 601148c5b11dd53a834e4dc738d33668970f9193 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 5 Jan 2013 12:28:34 +0000 Subject: [PATCH] Show precedes/follows and blocks/blocked relations on the Gantt diagram (#3436). Based on Toshi MARUYAMA's patch. git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@11118 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/gantts/show.html.erb | 23 ++++- lib/redmine/helpers/gantt.rb | 45 ++++++++- public/javascripts/gantt.js | 114 ++++++++++++++++++++++ test/functional/gantts_controller_test.rb | 16 +++ 4 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 public/javascripts/gantt.js diff --git a/app/views/gantts/show.html.erb b/app/views/gantts/show.html.erb index 0cebd0a65..caec4c3c2 100644 --- a/app/views/gantts/show.html.erb +++ b/app/views/gantts/show.html.erb @@ -102,7 +102,7 @@ -
+
<% style = "" style += "width: #{g_width - 1}px;" @@ -231,7 +231,15 @@ %> <%= content_tag(:div, ' '.html_safe, :style => style) %> <% end %> - +<% + style = "" + style += "position: absolute;" + style += "height: #{g_height}px;" + style += "top: #{headers_height + 1}px;" + style += "left: 0px;" + style += "width: #{g_width - 1}px;" +%> +<%= content_tag(:div, '', :style => style, :id => "gantt_draw_area") %>
@@ -261,3 +269,14 @@ <% end %> <% html_title(l(:label_gantt)) -%> + +<% content_for :header_tags do %> + <%= javascript_include_tag 'raphael' %> + <%= javascript_include_tag 'gantt' %> +<% end %> + +<%= javascript_tag do %> + var issue_relation_type = <%= raw Redmine::Helpers::Gantt::DRAW_TYPES.to_json %>; + $(document).ready(drawGanttHandler); + $(window).resize(drawGanttHandler); +<% end %> diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index db88b7902..5bf0a1e98 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -23,6 +23,12 @@ module Redmine include Redmine::I18n include Redmine::Utils::DateCalculation + # Relation types that are rendered + DRAW_TYPES = { + IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' }, + IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' } + }.freeze + # :nodoc: # Some utility methods for the PDF export class PDF @@ -136,6 +142,20 @@ module Redmine ) end + # Returns a hash of the relations between the issues that are present on the gantt + # and that should be displayed, grouped by issue ids. + def relations + return @relations if @relations + if issues.any? + issue_ids = issues.map(&:id) + @relations = IssueRelation. + where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys). + group_by(&:issue_from_id) + else + @relations = {} + end + end + # Return all the project nodes that will be displayed def projects return @projects if @projects @@ -705,6 +725,16 @@ module Redmine params[:image].text(params[:indent], params[:top] + 2, subject) end + def issue_relations(issue) + rels = {} + if relations[issue.id] + relations[issue.id].each do |relation| + (rels[relation.relation_type] ||= []) << relation.issue_to_id + end + end + rels + end + def html_task(params, coords, options={}) output = '' # Renders the task bar, with progress and late @@ -714,9 +744,18 @@ module Redmine style << "top:#{params[:top]}px;" style << "left:#{coords[:bar_start]}px;" style << "width:#{width}px;" - output << view.content_tag(:div, ' '.html_safe, - :style => style, - :class => "#{options[:css]} task_todo") + html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue] + content_opt = {:style => style, + :class => "#{options[:css]} task_todo", + :id => html_id} + if options[:issue] + rels_hash = {} + issue_relations(options[:issue]).each do |k, v| + rels_hash[k] = v.join(',') + end + content_opt[:data] = {"rels" => rels_hash} + end + output << view.content_tag(:div, ' '.html_safe, content_opt) if coords[:bar_late_end] width = coords[:bar_late_end] - coords[:bar_start] - 2 style = "" diff --git a/public/javascripts/gantt.js b/public/javascripts/gantt.js new file mode 100644 index 000000000..e16f4028f --- /dev/null +++ b/public/javascripts/gantt.js @@ -0,0 +1,114 @@ +var draw_gantt = null; +var draw_top; +var draw_right; +var draw_left; + +var rels_stroke_width = 2; + +function setDrawArea() { + draw_top = $("#gantt_draw_area").position().top; + draw_right = $("#gantt_draw_area").width(); + draw_left = $("#gantt_area").scrollLeft(); +} + +function getRelationsArray() { + var arr = new Array(); + $.each($('div.task_todo'), function(index_div, element) { + var element_id = $(element).attr("id"); + if (element_id != null) { + var issue_id = element_id.replace("task-todo-issue-", ""); + var data_rels = $(element).data("rels"); + if (data_rels != null) { + for (rel_type_key in issue_relation_type) { + if (rel_type_key in data_rels) { + var issue_arr = data_rels[rel_type_key].toString().split(","); + $.each(issue_arr, function(index_issue, element_issue) { + arr.push({issue_from: issue_id, issue_to: element_issue, + rel_type: rel_type_key}); + }); + } + } + } + } + }); + return arr; +} + +function drawRelations() { + var arr = getRelationsArray(); + $.each(arr, function(index_issue, element_issue) { + var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]); + var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]); + if (issue_from.size() == 0 || issue_to.size() == 0) { + return; + } + var issue_height = issue_from.height(); + var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top; + var issue_from_right = issue_from.position().left + issue_from.width(); + var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top; + var issue_to_left = issue_to.position().left; + var color = issue_relation_type[element_issue["rel_type"]]["color"]; + var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"]; + var issue_from_right_rel = issue_from_right + landscape_margin; + var issue_to_left_rel = issue_to_left - landscape_margin; + draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top, + "L", issue_from_right_rel + draw_left, issue_from_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + if (issue_from_right_rel < issue_to_left_rel) { + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, + "L", issue_from_right_rel + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top, + "L", issue_to_left + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + } else { + var issue_middle_top = issue_to_top + + (issue_height * + ((issue_from_top > issue_to_top) ? 1 : -1)); + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, + "L", issue_from_right_rel + draw_left, issue_middle_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top, + "L", issue_to_left_rel + draw_left, issue_middle_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top, + "L", issue_to_left_rel + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top, + "L", issue_to_left + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + } + draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, + "l", -4 * rels_stroke_width, -2 * rels_stroke_width, + "l", 0, 4 * rels_stroke_width, "z"]) + .attr({stroke: "none", + fill: color, + "stroke-linecap": "butt", + "stroke-linejoin": "miter", + }); + }); +} + +function drawGanttHandler() { + var folder = document.getElementById('gantt_draw_area'); + if(draw_gantt != null) + draw_gantt.clear(); + else + draw_gantt = Raphael(folder); + setDrawArea(); + drawRelations(); +} diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb index 902eaff26..41b71d773 100644 --- a/test/functional/gantts_controller_test.rb +++ b/test/functional/gantts_controller_test.rb @@ -81,6 +81,22 @@ class GanttsControllerTest < ActionController::TestCase assert_no_tag 'a', :content => /Private child of eCookbook/ end + def test_gantt_should_display_relations + IssueRelation.delete_all + issue1 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now) + issue2 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now) + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => 'precedes') + + get :show + assert_response :success + + relations = assigns(:gantt).relations + assert_kind_of Hash, relations + assert relations.present? + assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s + assert_select 'div.task_todo[id=?][data-rels=?]', "task-todo-issue-#{issue2.id}", '{}' + end + def test_gantt_should_export_to_pdf get :show, :project_id => 1, :format => 'pdf' assert_response :success