</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 += "width: #{g_width - 1}px;"
%>
<%= 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") %>
</div>
</td>
</tr>
<% 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 %>
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
)
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
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
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 = ""
--- /dev/null
+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();
+}
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