]> source.dussan.org Git - redmine.git/commitdiff
Show precedes/follows and blocks/blocked relations on the Gantt diagram (#3436).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 5 Jan 2013 12:28:34 +0000 (12:28 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 5 Jan 2013 12:28:34 +0000 (12:28 +0000)
Based on Toshi MARUYAMA's patch.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@11118 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/views/gantts/show.html.erb
lib/redmine/helpers/gantt.rb
public/javascripts/gantt.js [new file with mode: 0644]
test/functional/gantts_controller_test.rb

index 0cebd0a652807a369edc94a64883d09ea454d44e..caec4c3c2ca04b88b6eeb18943874cb12aa8ca78 100644 (file)
 </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, '&nbsp;'.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 %>
index db88b790295cd5ca3cdc026498c0698ea55ad81c..5bf0a1e98d6a7ead847d466219208bdd6d5d313a 100644 (file)
@@ -23,6 +23,12 @@ module Redmine
       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
@@ -136,6 +142,20 @@ module Redmine
         )
       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
@@ -705,6 +725,16 @@ module Redmine
         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
@@ -714,9 +744,18 @@ module Redmine
           style << "top:#{params[:top]}px;"
           style << "left:#{coords[:bar_start]}px;"
           style << "width:#{width}px;"
-          output << view.content_tag(:div, '&nbsp;'.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, '&nbsp;'.html_safe, content_opt)
           if coords[:bar_late_end]
             width = coords[:bar_late_end] - coords[:bar_start] - 2
             style = ""
diff --git a/public/javascripts/gantt.js b/public/javascripts/gantt.js
new file mode 100644 (file)
index 0000000..e16f402
--- /dev/null
@@ -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();
+}
index 902eaff26ac82a54f1d49f8d2c73e7e35f1c6c56..41b71d773bcc7f9ecea4267208feed67bff0e010 100644 (file)
@@ -81,6 +81,22 @@ class GanttsControllerTest < ActionController::TestCase
     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