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:
parent
9adb0c61a9
commit
601148c5b1
|
@ -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, ' '.html_safe, :style => style) %>
|
<%= content_tag(:div, ' '.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 %>
|
||||||
|
|
|
@ -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, ' '.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, ' '.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 = ""
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue