##// END OF EJS Templates
Show precedes/follows and blocks/blocked relations on the Gantt diagram (#3436)....
Jean-Philippe Lang -
r10888:601148c5b11d
parent child
Show More
@@ -0,0 +1,114
1 var draw_gantt = null;
2 var draw_top;
3 var draw_right;
4 var draw_left;
5
6 var rels_stroke_width = 2;
7
8 function setDrawArea() {
9 draw_top = $("#gantt_draw_area").position().top;
10 draw_right = $("#gantt_draw_area").width();
11 draw_left = $("#gantt_area").scrollLeft();
12 }
13
14 function getRelationsArray() {
15 var arr = new Array();
16 $.each($('div.task_todo'), function(index_div, element) {
17 var element_id = $(element).attr("id");
18 if (element_id != null) {
19 var issue_id = element_id.replace("task-todo-issue-", "");
20 var data_rels = $(element).data("rels");
21 if (data_rels != null) {
22 for (rel_type_key in issue_relation_type) {
23 if (rel_type_key in data_rels) {
24 var issue_arr = data_rels[rel_type_key].toString().split(",");
25 $.each(issue_arr, function(index_issue, element_issue) {
26 arr.push({issue_from: issue_id, issue_to: element_issue,
27 rel_type: rel_type_key});
28 });
29 }
30 }
31 }
32 }
33 });
34 return arr;
35 }
36
37 function drawRelations() {
38 var arr = getRelationsArray();
39 $.each(arr, function(index_issue, element_issue) {
40 var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]);
41 var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]);
42 if (issue_from.size() == 0 || issue_to.size() == 0) {
43 return;
44 }
45 var issue_height = issue_from.height();
46 var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top;
47 var issue_from_right = issue_from.position().left + issue_from.width();
48 var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top;
49 var issue_to_left = issue_to.position().left;
50 var color = issue_relation_type[element_issue["rel_type"]]["color"];
51 var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"];
52 var issue_from_right_rel = issue_from_right + landscape_margin;
53 var issue_to_left_rel = issue_to_left - landscape_margin;
54 draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top,
55 "L", issue_from_right_rel + draw_left, issue_from_top])
56 .attr({stroke: color,
57 "stroke-width": rels_stroke_width
58 });
59 if (issue_from_right_rel < issue_to_left_rel) {
60 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
61 "L", issue_from_right_rel + draw_left, issue_to_top])
62 .attr({stroke: color,
63 "stroke-width": rels_stroke_width
64 });
65 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top,
66 "L", issue_to_left + draw_left, issue_to_top])
67 .attr({stroke: color,
68 "stroke-width": rels_stroke_width
69 });
70 } else {
71 var issue_middle_top = issue_to_top +
72 (issue_height *
73 ((issue_from_top > issue_to_top) ? 1 : -1));
74 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
75 "L", issue_from_right_rel + draw_left, issue_middle_top])
76 .attr({stroke: color,
77 "stroke-width": rels_stroke_width
78 });
79 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top,
80 "L", issue_to_left_rel + draw_left, issue_middle_top])
81 .attr({stroke: color,
82 "stroke-width": rels_stroke_width
83 });
84 draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top,
85 "L", issue_to_left_rel + draw_left, issue_to_top])
86 .attr({stroke: color,
87 "stroke-width": rels_stroke_width
88 });
89 draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top,
90 "L", issue_to_left + draw_left, issue_to_top])
91 .attr({stroke: color,
92 "stroke-width": rels_stroke_width
93 });
94 }
95 draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top,
96 "l", -4 * rels_stroke_width, -2 * rels_stroke_width,
97 "l", 0, 4 * rels_stroke_width, "z"])
98 .attr({stroke: "none",
99 fill: color,
100 "stroke-linecap": "butt",
101 "stroke-linejoin": "miter",
102 });
103 });
104 }
105
106 function drawGanttHandler() {
107 var folder = document.getElementById('gantt_draw_area');
108 if(draw_gantt != null)
109 draw_gantt.clear();
110 else
111 draw_gantt = Raphael(folder);
112 setDrawArea();
113 drawRelations();
114 }
@@ -102,7 +102,7
102 </td>
102 </td>
103
103
104 <td>
104 <td>
105 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
105 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
106 <%
106 <%
107 style = ""
107 style = ""
108 style += "width: #{g_width - 1}px;"
108 style += "width: #{g_width - 1}px;"
@@ -231,7 +231,15
231 %>
231 %>
232 <%= content_tag(:div, '&nbsp;'.html_safe, :style => style) %>
232 <%= content_tag(:div, '&nbsp;'.html_safe, :style => style) %>
233 <% end %>
233 <% end %>
234
234 <%
235 style = ""
236 style += "position: absolute;"
237 style += "height: #{g_height}px;"
238 style += "top: #{headers_height + 1}px;"
239 style += "left: 0px;"
240 style += "width: #{g_width - 1}px;"
241 %>
242 <%= content_tag(:div, '', :style => style, :id => "gantt_draw_area") %>
235 </div>
243 </div>
236 </td>
244 </td>
237 </tr>
245 </tr>
@@ -261,3 +269,14
261 <% end %>
269 <% end %>
262
270
263 <% html_title(l(:label_gantt)) -%>
271 <% html_title(l(:label_gantt)) -%>
272
273 <% content_for :header_tags do %>
274 <%= javascript_include_tag 'raphael' %>
275 <%= javascript_include_tag 'gantt' %>
276 <% end %>
277
278 <%= javascript_tag do %>
279 var issue_relation_type = <%= raw Redmine::Helpers::Gantt::DRAW_TYPES.to_json %>;
280 $(document).ready(drawGanttHandler);
281 $(window).resize(drawGanttHandler);
282 <% end %>
@@ -23,6 +23,12 module Redmine
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
@@ -136,6 +142,20 module Redmine
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
@@ -705,6 +725,16 module Redmine
705 params[:image].text(params[:indent], params[:top] + 2, subject)
725 params[:image].text(params[:indent], params[:top] + 2, subject)
706 end
726 end
707
727
728 def issue_relations(issue)
729 rels = {}
730 if relations[issue.id]
731 relations[issue.id].each do |relation|
732 (rels[relation.relation_type] ||= []) << relation.issue_to_id
733 end
734 end
735 rels
736 end
737
708 def html_task(params, coords, options={})
738 def html_task(params, coords, options={})
709 output = ''
739 output = ''
710 # Renders the task bar, with progress and late
740 # Renders the task bar, with progress and late
@@ -714,9 +744,18 module Redmine
714 style << "top:#{params[:top]}px;"
744 style << "top:#{params[:top]}px;"
715 style << "left:#{coords[:bar_start]}px;"
745 style << "left:#{coords[:bar_start]}px;"
716 style << "width:#{width}px;"
746 style << "width:#{width}px;"
717 output << view.content_tag(:div, '&nbsp;'.html_safe,
747 html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
718 :style => style,
748 content_opt = {:style => style,
719 :class => "#{options[:css]} task_todo")
749 :class => "#{options[:css]} task_todo",
750 :id => html_id}
751 if options[:issue]
752 rels_hash = {}
753 issue_relations(options[:issue]).each do |k, v|
754 rels_hash[k] = v.join(',')
755 end
756 content_opt[:data] = {"rels" => rels_hash}
757 end
758 output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
720 if coords[:bar_late_end]
759 if coords[:bar_late_end]
721 width = coords[:bar_late_end] - coords[:bar_start] - 2
760 width = coords[:bar_late_end] - coords[:bar_start] - 2
722 style = ""
761 style = ""
@@ -81,6 +81,22 class GanttsControllerTest < ActionController::TestCase
81 assert_no_tag 'a', :content => /Private child of eCookbook/
81 assert_no_tag 'a', :content => /Private child of eCookbook/
82 end
82 end
83
83
84 def test_gantt_should_display_relations
85 IssueRelation.delete_all
86 issue1 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now)
87 issue2 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now)
88 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => 'precedes')
89
90 get :show
91 assert_response :success
92
93 relations = assigns(:gantt).relations
94 assert_kind_of Hash, relations
95 assert relations.present?
96 assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s
97 assert_select 'div.task_todo[id=?][data-rels=?]', "task-todo-issue-#{issue2.id}", '{}'
98 end
99
84 def test_gantt_should_export_to_pdf
100 def test_gantt_should_export_to_pdf
85 get :show, :project_id => 1, :format => 'pdf'
101 get :show, :project_id => 1, :format => 'pdf'
86 assert_response :success
102 assert_response :success
General Comments 0
You need to be logged in to leave comments. Login now