@@ -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, ' '.html_safe, :style => style) %> |
|
232 | <%= content_tag(:div, ' '.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, ' '.html_safe, |
|
747 | html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue] | |
718 | :style => style, |
|
748 | content_opt = {:style => style, | |
719 |
|
|
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, ' '.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