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
This commit is contained in:
Jean-Philippe Lang 2013-01-05 12:28:34 +00:00
parent 9adb0c61a9
commit 601148c5b1
4 changed files with 193 additions and 5 deletions

View File

@ -102,7 +102,7 @@
</td> </td>
<td> <td>
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;"> <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
<% <%
style = "" style = ""
style += "width: #{g_width - 1}px;" style += "width: #{g_width - 1}px;"
@ -231,7 +231,15 @@
%> %>
<%= content_tag(:div, '&nbsp;'.html_safe, :style => style) %> <%= content_tag(:div, '&nbsp;'.html_safe, :style => style) %>
<% end %> <% 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") %>
</div> </div>
</td> </td>
</tr> </tr>
@ -261,3 +269,14 @@
<% end %> <% end %>
<% html_title(l(:label_gantt)) -%> <% 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 %>

View File

@ -23,6 +23,12 @@ module Redmine
include Redmine::I18n include Redmine::I18n
include Redmine::Utils::DateCalculation 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: # :nodoc:
# Some utility methods for the PDF export # Some utility methods for the PDF export
class PDF class PDF
@ -136,6 +142,20 @@ module Redmine
) )
end 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 # Return all the project nodes that will be displayed
def projects def projects
return @projects if @projects return @projects if @projects
@ -705,6 +725,16 @@ module Redmine
params[:image].text(params[:indent], params[:top] + 2, subject) params[:image].text(params[:indent], params[:top] + 2, subject)
end 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={}) def html_task(params, coords, options={})
output = '' output = ''
# Renders the task bar, with progress and late # Renders the task bar, with progress and late
@ -714,9 +744,18 @@ module Redmine
style << "top:#{params[:top]}px;" style << "top:#{params[:top]}px;"
style << "left:#{coords[:bar_start]}px;" style << "left:#{coords[:bar_start]}px;"
style << "width:#{width}px;" style << "width:#{width}px;"
output << view.content_tag(:div, '&nbsp;'.html_safe, html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
:style => style, content_opt = {:style => style,
:class => "#{options[:css]} task_todo") :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, '&nbsp;'.html_safe, content_opt)
if coords[:bar_late_end] if coords[:bar_late_end]
width = coords[:bar_late_end] - coords[:bar_start] - 2 width = coords[:bar_late_end] - coords[:bar_start] - 2
style = "" style = ""

114
public/javascripts/gantt.js Normal file
View File

@ -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();
}

View File

@ -81,6 +81,22 @@ class GanttsControllerTest < ActionController::TestCase
assert_no_tag 'a', :content => /Private child of eCookbook/ assert_no_tag 'a', :content => /Private child of eCookbook/
end 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 def test_gantt_should_export_to_pdf
get :show, :project_id => 1, :format => 'pdf' get :show, :project_id => 1, :format => 'pdf'
assert_response :success assert_response :success