0001-Moving-gantt-bar.patch

Yuichi HARADA, 2021-02-18 02:41

Download (38 KB)

View differences:

app/controllers/gantts_controller.rb
19 19

  
20 20
class GanttsController < ApplicationController
21 21
  menu_item :gantt
22
  before_action :find_optional_project
22
  before_action :find_optional_project, :only => [:show]
23 23

  
24 24
  rescue_from Query::StatementInvalid, :with => :query_statement_invalid
25 25

  
......
55 55
      end
56 56
    end
57 57
  end
58

  
59
  def change_duration
60
    return render_error(:status => :unprocessable_entity) unless request.xhr?
61

  
62
    @obj = Issue.find(params[:id])
63
    raise Unauthorized unless @obj.visible?
64

  
65
    ActiveRecord::Base.transaction do
66
      @obj.init_journal(User.current)
67
      @obj.safe_attributes = params[:change_duration]
68
      if !@obj.save
69
        render_403(:message => @obj.errors.full_messages.join)
70
        raise ActiveRecord::Rollback
71
      end
72
    end
73
    retrieve_query
74
  rescue ActiveRecord::StaleObjectError
75
    render_403(:message => :notice_issue_update_conflict)
76
  rescue ActiveRecord::RecordNotFound
77
    render_404
78
  end
58 79
end
app/views/gantts/change_duration.js.erb
1
<%
2
@draw_objs = []
3

  
4
def select_precedes(issue)
5
  issue.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to).each do |follows|
6
    next if @draw_objs.include?(follows)
7

  
8
    while follows do
9
      @draw_objs.concat [follows, follows.fixed_version, follows.project]
10
      select_precedes(follows)
11
      follows.children.each do |child|
12
        @draw_objs.concat [child, child.fixed_version, child.project]
13
        select_precedes(child)
14
      end
15
      follows = follows.parent
16
    end
17
  end
18
end
19

  
20
issue = @obj
21
while issue do
22
  @draw_objs.concat [issue, issue.fixed_version, issue.project]
23
  select_precedes(issue)
24
  issue = issue.parent
25
end
26
@draw_objs = @draw_objs.compact.uniq
27
@draw_objs.reject!{|obj| ![Project, Version, Issue].include?(obj.class)}
28
-%>
29
var elm;
30
<%
31
gantt = Redmine::Helpers::Gantt.new(params)
32
gantt.view = self
33
gantt.query = @query
34

  
35
@draw_objs.each do |obj|
36
  gantt.instance_variable_set(:@number_of_rows, 0)
37
  gantt.instance_variable_set(:@lines, '')
38
  gantt.render_object_row(
39
    obj,
40
    {format: :html, only: :lines, zoom: 2 ** gantt.zoom, top: 0, top_increment: 20}
41
  )
42
  todo_content = Nokogiri::HTML.parse(gantt.instance_variable_get(:@lines))
43
  todo_content = todo_content.xpath(
44
    "//div[contains(@class,'task') and contains(@class,'line')]/*"
45
  ).to_s.tr("\n",'').gsub(/'/, "\\\\'")
46

  
47
  klass_name = obj.class.name.underscore
48
  elm_todo = "[id=task-todo-#{klass_name}-#{obj.id}]"
49
  css_subject = 'span:not(.expander)'
50
  elm_subject = raw("[id=#{klass_name}-#{obj.id}] > #{css_subject}")
51

  
52
  subject_content = Nokogiri::HTML.parse(gantt.__send__(:html_subject_content, obj))
53
  subject_content = subject_content.css(css_subject).to_s.tr("\n",'').gsub(/'/, "\\\\'")
54
-%>
55
if($('<%= elm_subject %>').length){
56
  $('<%= elm_todo %>').parent().html('<%= raw(todo_content) %>');
57
  $('<%= elm_subject %>').replaceWith('<%= raw(subject_content) %>');
58
<%
59
  case obj
60
  when Issue
61
    @query.columns.each do |column|
62
-%>
63
  elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>');
64
  if(elm.length){
65
    elm.html('<%= escape_javascript(column_content(column, obj)) %>');
66
  }
67
<%
68
    end
69
  end
70
-%>
71
}
72
<%
73
end
74
-%>
config/routes.rb
60 60

  
61 61
  get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
62 62
  get '/issues/gantt', :to => 'gantts#show'
63
  put '/gantt/:id/change_duration', :to => 'gantts#change_duration', :as => 'gantt_change_duration'
63 64

  
64 65
  get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
65 66
  get '/issues/calendar', :to => 'calendars#show'
lib/redmine/helpers/gantt.rb
771 771
          tag_options[:class] = "version-name"
772 772
          has_children = object.fixed_issues.exists?
773 773
        when Project
774
          tag_options[:id] = "project-#{object.id}"
774 775
          tag_options[:class] = "project-name"
775 776
          has_children = object.issues.exists? || object.versions.exists?
776 777
        end
......
849 850
          end
850 851
        # Renders the task bar, with progress and late
851 852
        if coords[:bar_start] && coords[:bar_end]
852
          width = coords[:bar_end] - coords[:bar_start] - 2
853
          width = coords[:bar_end] - coords[:bar_start]
853 854
          style = +""
854
          style << "top:#{params[:top]}px;"
855 855
          style << "left:#{coords[:bar_start]}px;"
856 856
          style << "width:#{width}px;"
857
          html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue)
858
          html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
857
          html_id =
858
            case object
859
            when Project
860
              "task-todo-project-#{object.id}"
861
            when Version
862
              "task-todo-version-#{object.id}"
863
            when Issue
864
              "task-todo-issue-#{object.id}"
865
            end
859 866
          content_opt = {:style => style,
860
                         :class => "#{css} task_todo",
867
                         :class => "task_todo",
861 868
                         :id => html_id,
862 869
                         :data => {}}
863 870
          if object.is_a?(Issue)
......
865 872
            if rels.present?
866 873
              content_opt[:data] = {"rels" => rels.to_json}
867 874
            end
875
            content_opt[:data].merge!({
876
              :url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path(
877
                object
878
              ),
879
              :object => {
880
                :start_date => object.start_date,
881
                :due_date => object.due_date,
882
                :lock_version => object.lock_version,
883
              }.to_json,
884
            })
868 885
          end
869 886
          content_opt[:data].merge!(data_options)
870
          output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
887
          bar_contents = []
871 888
          if coords[:bar_late_end]
872
            width = coords[:bar_late_end] - coords[:bar_start] - 2
889
            width = coords[:bar_late_end] - coords[:bar_start]
873 890
            style = +""
874
            style << "top:#{params[:top]}px;"
875
            style << "left:#{coords[:bar_start]}px;"
876 891
            style << "width:#{width}px;"
877
            output << view.content_tag(:div, '&nbsp;'.html_safe,
878
                                       :style => style,
879
                                       :class => "#{css} task_late",
880
                                       :data => data_options)
892
            bar_contents << view.content_tag(:div, '&nbsp;'.html_safe,
893
                                             :style => style,
894
                                             :class => "task_late",
895
                                             :data => data_options)
881 896
          end
882 897
          if coords[:bar_progress_end]
883
            width = coords[:bar_progress_end] - coords[:bar_start] - 2
898
            width = coords[:bar_progress_end] - coords[:bar_start]
884 899
            style = +""
885
            style << "top:#{params[:top]}px;"
886
            style << "left:#{coords[:bar_start]}px;"
887 900
            style << "width:#{width}px;"
888 901
            html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue)
889 902
            html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
890
            output << view.content_tag(:div, '&nbsp;'.html_safe,
903
            bar_contents << view.content_tag(:div, '&nbsp;'.html_safe,
904
                                             :style => style,
905
                                             :class => "task_done",
906
                                             :id => html_id,
907
                                             :data => data_options)
908
          end
909

  
910
          # Renders the tooltip
911
          if object.is_a?(Issue)
912
            s = view.content_tag(:span,
913
                               view.render_issue_tooltip(object).html_safe,
914
                               :class => "tip")
915
            s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection')
916
            style = +""
917
            style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
918
            style << "height:12px;"
919
            bar_contents << view.content_tag(:div, s.html_safe,
920
                                             :style => style,
921
                                             :class => "tooltip hascontextmenu",
922
                                             :data => data_options)
923
          end
924

  
925
          # Renders the label on the right
926
          if label
927
            style = +""
928
            style << "top:0px;"
929
            style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;"
930
            bar_contents << view.content_tag(:div, label,
931
                                             :style => style,
932
                                             :class => "label",
933
                                             :data => data_options)
934
          end
935

  
936
          bar_contents = bar_contents.join.presence
937
          output << view.content_tag(:div, (bar_contents || '&nbsp;').html_safe, content_opt)
938
        else
939
          # Renders the label on the right
940
          if label
941
            style = +""
942
            style << "top:1px;"
943
            style << "left:#{(coords[:bar_end] || 0) + 8}px;"
944
            output << view.content_tag(:div, label,
891 945
                                       :style => style,
892
                                       :class => "#{css} task_done",
893
                                       :id => html_id,
946
                                       :class => "label",
894 947
                                       :data => data_options)
895 948
          end
896 949
        end
......
898 951
        if markers
899 952
          if coords[:start]
900 953
            style = +""
901
            style << "top:#{params[:top]}px;"
902 954
            style << "left:#{coords[:start]}px;"
903
            style << "width:15px;"
904 955
            output << view.content_tag(:div, '&nbsp;'.html_safe,
905 956
                                       :style => style,
906
                                       :class => "#{css} marker starting",
957
                                       :class => "marker starting",
907 958
                                       :data => data_options)
908 959
          end
909 960
          if coords[:end]
910 961
            style = +""
911
            style << "top:#{params[:top]}px;"
912 962
            style << "left:#{coords[:end]}px;"
913
            style << "width:15px;"
914 963
            output << view.content_tag(:div, '&nbsp;'.html_safe,
915 964
                                       :style => style,
916
                                       :class => "#{css} marker ending",
965
                                       :class => "marker ending",
917 966
                                       :data => data_options)
918 967
          end
919 968
        end
920
        # Renders the label on the right
921
        if label
922
          style = +""
923
          style << "top:#{params[:top]}px;"
924
          style << "left:#{(coords[:bar_end] || 0) + 8}px;"
925
          style << "width:15px;"
926
          output << view.content_tag(:div, label,
927
                                     :style => style,
928
                                     :class => "#{css} label",
929
                                     :data => data_options)
930
        end
931
        # Renders the tooltip
932
        if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
933
          s = view.content_tag(:span,
934
                               view.render_issue_tooltip(object).html_safe,
935
                               :class => "tip")
936
          s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
937
                                :value => object.id, :style => 'display:none;',
938
                                :class => 'toggle-selection')
939
          style = +""
940
          style << "position: absolute;"
941
          style << "top:#{params[:top]}px;"
942
          style << "left:#{coords[:bar_start]}px;"
943
          style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
944
          style << "height:12px;"
945
          output << view.content_tag(:div, s.html_safe,
946
                                     :style => style,
947
                                     :class => "tooltip hascontextmenu",
948
                                     :data => data_options)
949
        end
969
        output = view.content_tag(:div, output.html_safe,
970
          :class => "#{css} line",
971
          :style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;",
972
          :data => data_options
973
        )
950 974
        @lines << output
951 975
        output
952 976
      end
public/javascripts/gantt.js
4 4
var draw_gantt = null;
5 5
var draw_top;
6 6
var draw_right;
7
var draw_left;
8 7

  
9 8
var rels_stroke_width = 2;
10 9

  
11 10
function setDrawArea() {
12
  draw_top   = $("#gantt_draw_area").position().top;
11
  draw_top   = $("#gantt_draw_area").offset().top;
13 12
  draw_right = $("#gantt_draw_area").width();
14
  draw_left  = $("#gantt_area").scrollLeft();
15 13
}
16 14

  
17 15
function getRelationsArray() {
......
42 40
      return;
43 41
    }
44 42
    var issue_height = issue_from.height();
45
    var issue_from_top   = issue_from.position().top  + (issue_height / 2) - draw_top;
43
    var issue_from_top   = issue_from.offset().top  + (issue_height / 2) - draw_top;
46 44
    var issue_from_right = issue_from.position().left + issue_from.width();
47
    var issue_to_top   = issue_to.position().top  + (issue_height / 2) - draw_top;
45
    var issue_to_top   = issue_to.offset().top  + (issue_height / 2) - draw_top;
48 46
    var issue_to_left  = issue_to.position().left;
49 47
    var color = issue_relation_type[element_issue["rel_type"]]["color"];
50 48
    var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"];
51 49
    var issue_from_right_rel = issue_from_right + landscape_margin;
52 50
    var issue_to_left_rel    = issue_to_left    - landscape_margin;
53
    draw_gantt.path(["M", issue_from_right + draw_left,     issue_from_top,
54
                     "L", issue_from_right_rel + draw_left, issue_from_top])
51
    draw_gantt.path(["M", issue_from_right,     issue_from_top,
52
                     "L", issue_from_right_rel, issue_from_top])
55 53
                   .attr({stroke: color,
56 54
                          "stroke-width": rels_stroke_width
57 55
                          });
58 56
    if (issue_from_right_rel < issue_to_left_rel) {
59
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
60
                       "L", issue_from_right_rel + draw_left, issue_to_top])
57
      draw_gantt.path(["M", issue_from_right_rel, issue_from_top,
58
                       "L", issue_from_right_rel, issue_to_top])
61 59
                     .attr({stroke: color,
62 60
                          "stroke-width": rels_stroke_width
63 61
                          });
64
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top,
65
                       "L", issue_to_left + draw_left,        issue_to_top])
62
      draw_gantt.path(["M", issue_from_right_rel, issue_to_top,
63
                       "L", issue_to_left,        issue_to_top])
66 64
                     .attr({stroke: color,
67 65
                          "stroke-width": rels_stroke_width
68 66
                          });
......
70 68
      var issue_middle_top = issue_to_top +
71 69
                                (issue_height *
72 70
                                   ((issue_from_top > issue_to_top) ? 1 : -1));
73
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
74
                       "L", issue_from_right_rel + draw_left, issue_middle_top])
71
      draw_gantt.path(["M", issue_from_right_rel, issue_from_top,
72
                       "L", issue_from_right_rel, issue_middle_top])
75 73
                     .attr({stroke: color,
76 74
                          "stroke-width": rels_stroke_width
77 75
                          });
78
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top,
79
                       "L", issue_to_left_rel + draw_left,    issue_middle_top])
76
      draw_gantt.path(["M", issue_from_right_rel, issue_middle_top,
77
                       "L", issue_to_left_rel,    issue_middle_top])
80 78
                     .attr({stroke: color,
81 79
                          "stroke-width": rels_stroke_width
82 80
                          });
83
      draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top,
84
                       "L", issue_to_left_rel + draw_left, issue_to_top])
81
      draw_gantt.path(["M", issue_to_left_rel, issue_middle_top,
82
                       "L", issue_to_left_rel, issue_to_top])
85 83
                     .attr({stroke: color,
86 84
                          "stroke-width": rels_stroke_width
87 85
                          });
88
      draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top,
89
                       "L", issue_to_left + draw_left,     issue_to_top])
86
      draw_gantt.path(["M", issue_to_left_rel, issue_to_top,
87
                       "L", issue_to_left,     issue_to_top])
90 88
                     .attr({stroke: color,
91 89
                          "stroke-width": rels_stroke_width
92 90
                          });
93 91
    }
94
    draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top,
92
    draw_gantt.path(["M", issue_to_left, issue_to_top,
95 93
                     "l", -4 * rels_stroke_width, -2 * rels_stroke_width,
96 94
                     "l", 0, 4 * rels_stroke_width, "z"])
97 95
                   .attr({stroke: "none",
......
104 102

  
105 103
function getProgressLinesArray() {
106 104
  var arr = new Array();
107
  var today_left = $('#today_line').position().left;
105
  var today_left = $('#today_line').position().left + $("#gantt_area").scrollLeft();
108 106
  arr.push({left: today_left, top: 0});
109 107
  $.each($('div.issue-subject, div.version-name'), function(index, element) {
110 108
    if(!$(element).is(':visible')) return true;
111
    var t = $(element).position().top - draw_top ;
109
    var t = $(element).offset().top - draw_top ;
112 110
    var h = ($(element).height() / 9);
113 111
    var element_top_upper  = t - h;
114 112
    var element_top_center = t + (h * 3);
......
125 123
        arr.push({left: draw_right, top: element_top_upper, is_right_edge: true});
126 124
        arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true});
127 125
      } else if (issue_done.length > 0) {
128
        var done_left = issue_done.first().position().left +
129
                           issue_done.first().width();
126
        var done_left = today_left;
127
        var issue_todo = $("#task-todo-" + $(element).attr("id"));
128
        if (issue_todo.length > 0){
129
          done_left = issue_todo.first().position().left;
130
        }
130 131
        arr.push({left: done_left, top: element_top_center});
131 132
      } else if (is_behind_start) {
132 133
        arr.push({left: 0 , top: element_top_upper, is_left_edge: true});
......
145 146
}
146 147

  
147 148
function drawGanttProgressLines() {
149
  if(!$("#today_line").length) return;
148 150
  var arr = getProgressLinesArray();
149 151
  var color = $("#today_line")
150 152
                    .css("border-left-color");
......
154 156
        (!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) &&
155 157
         !("is_left_edge"  in arr[i - 1] && "is_left_edge"  in arr[i]))
156 158
        ) {
157
      var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left;
158
      var x2 = (arr[i].left == 0)     ? 0 : arr[i].left     + draw_left;
159
      var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left;
160
      var x2 = (arr[i].left == 0)     ? 0 : arr[i].left;
159 161
      draw_gantt.path(["M", x1, arr[i - 1].top,
160 162
                       "L", x2, arr[i].top])
161 163
                   .attr({stroke: color, "stroke-width": 2});
......
301 303
    $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true);
302 304
  });
303 305
}
306

  
307
initGanttDnD = function(){
308
  var grid_x = 0;
309
  if($('#zoom').length){
310
    switch(parseInt($('#zoom').val())){
311
    case 4:
312
      grid_x = 16;
313
      break;
314
    case 3:
315
      grid_x = 8;
316
      break;
317
    }
318
  }
319
  if(grid_x > 0){
320
    $('.leaf .task_todo').draggable({
321
      containment: 'parent',
322
      axis: 'x',
323
      grid: [grid_x, 0],
324
      opacity: 0.5,
325
      cursor: 'move',
326
      revertDuration: 100,
327
      start: function (event, ui) {
328
        var helper = ui.helper[0];
329
        helper.startLeft = ui.position.left;
330
      },
331
    });
332

  
333
    $('.task.line').droppable({
334
      accept: '.leaf .task_todo',
335
      drop: function (event, ui) {
336
        var target = $(ui.draggable);
337
        var url = target.attr('data-url-change-duration');
338
        var object = JSON.parse(target.attr('data-object'));
339
        var startLeft = target[0].startLeft;
340
        var relative_days = Math.floor((ui.position.left - startLeft) / grid_x);
341
        if(relative_days == 0){
342
          return;
343
        }
344
        var start_date = new Date(object.start_date);
345
        start_date.setDate(start_date.getDate() + relative_days);
346
        start_date =
347
          [
348
            start_date.getFullYear(),
349
            ('0' + (start_date.getMonth() + 1)).slice(-2),
350
            ('0' + start_date.getDate()).slice(-2),
351
          ].join('-');
352
        var due_date = null;
353
        if(object.due_date != null){
354
          due_date = new Date(object.due_date);
355
          due_date.setDate(due_date.getDate() + relative_days);
356
          due_date =
357
            [
358
              due_date.getFullYear(),
359
              ('0' + (due_date.getMonth() + 1)).slice(-2),
360
              ('0' + due_date.getDate()).slice(-2),
361
            ].join('-');
362
        }
363

  
364
        $('#selected_c option:not(:disabled)').prop('selected', true);
365
        var form = $('#query_form').serializeArray();
366
        var json_param = {};
367
        form.forEach(function(data){
368
          var key = data.name;
369
          var value = data.value;
370
          if(/\[\]$/.test(key)){
371
            if(!json_param.hasOwnProperty(key)){
372
              json_param[key] = [];
373
            }
374
            json_param[key].push(value);
375
          }
376
          else{
377
            json_param[key] = value;
378
          }
379
        });
380
        $('#selected_c option:not(:disabled)').prop('selected', false);
381
        Object.assign(json_param, {
382
          change_duration: {
383
            start_date: start_date,
384
            due_date: due_date,
385
            lock_version: object.lock_version,
386
          },
387
        });
388

  
389
        $.ajax({
390
          type: 'PUT',
391
          url: url,
392
          data: json_param,
393
        }).done(function(data){
394
          drawGanttHandler();
395
          initGanttDnD();
396
        }).fail(function(jqXHR){
397
          var contents = $('<div>' + jqXHR.responseText + '</div>');
398
          var error_message = contents.find('p#errorExplanation');
399
          if(error_message.length){
400
            $('div#content h2:first-of-type').after(error_message);
401
            $('p#errorExplanation').hide('fade', {}, 3000, function(){
402
              $(this).remove();
403
            });
404
          }
405
          ui.draggable.animate({'left': ui.draggable[0].startLeft}, 'fast');
406
        });
407
      }
408
    });
409
  }
410
};
411

  
412
$(document).ready(initGanttDnD);
public/stylesheets/application.css
1410 1410

  
1411 1411
.task {
1412 1412
  position: absolute;
1413
  height:8px;
1413
  height:10px;
1414 1414
  font-size:0.8em;
1415 1415
  color:#888;
1416 1416
  padding:0;
......
1419 1419
  white-space:nowrap;
1420 1420
}
1421 1421

  
1422
.task.label {width:100%;}
1423
.task.label.project, .task.label.version { font-weight: bold; }
1422
.task.line { left: 0; }
1423
.task div.tooltip:hover span.tip { font-size: inherit; }
1424
.task .task_todo .label { font-size: inherit; }
1425
.task.project .task_todo .label { margin-top: -4px; }
1426
.task.version .task_todo .label { margin-top: -3px; }
1424 1427

  
1425
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1426
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1427
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1428
.task .label { position: absolute; width: auto; }
1429
.task.project .label, .task.version .label { font-weight: bold; }
1428 1430

  
1429
.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1430
.task_late.parent, .task_done.parent { height: 3px;}
1431
.task.parent.marker.starting  { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
1432
.task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
1431
.task_late { position: absolute; height: inherit; background:#f66; }
1432
.task_done { position: absolute; height: inherit; background:#00c600; }
1433
.task_todo { position: absolute; height: inherit; background:#aaa; }
1433 1434

  
1434
.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1435
.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1436
.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1437
.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1435
.parent .task_todo { background: #888; height: 5px; }
1436
.parent .task_late, .parent .task_done { height: 5px; }
1437
.parent .marker {
1438
  background: #888;
1439
  display: inline-block;
1440
  position: absolute;
1441
  width: 8px;
1442
  height: 6px;
1443
  margin-left: -5px;
1444
  margin-bottom: -4px;
1445
}
1446
.parent .marker:after {
1447
  border-top: 3px solid #888;
1448
  border-left: 4px solid transparent;
1449
  border-right: 4px solid transparent;
1450
  content: '';
1451
  height: 0;
1452
  left: 0;
1453
  position: absolute;
1454
  bottom: -3px;
1455
  width: 0;
1456
}
1457

  
1458
.version .task_late { background:#f66; height: 4px; }
1459
.version .task_done { background:#00c600; height: 4px; }
1460
.version .task_todo { background:#aaa; height: 4px; margin-top: 3px; }
1461
.version .marker {
1462
  width: 0;
1463
  height: 0;
1464
  border: 5px solid transparent;
1465
  border-bottom-color: black;
1466
  position: absolute;
1467
  margin-top: -5px;
1468
  margin-left: -6px;
1469
}
1470
.version .marker:after {
1471
  content: '';
1472
  position: absolute;
1473
  left: -5px;
1474
  top: 5px;
1475
  width: 0;
1476
  height: 0;
1477
  border: 5px solid transparent;
1478
  border-top-color: black;
1479
}
1438 1480

  
1439
.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1440
.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1441
.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1442
.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1481
.project .task_late { background:#f66; height: 2px; }
1482
.project .task_done { background:#00c600; height: 2px; }
1483
.project .task_todo { background:#aaa; height: 2px; margin-top: 4px; }
1484
.project .marker {
1485
  width: 0;
1486
  height: 0;
1487
  border: 5px solid transparent;
1488
  border-bottom-color: blue;
1489
  position: absolute;
1490
  margin-top: -5px;
1491
  margin-left: -6px;
1492
}
1493
.project .marker:after {
1494
  content: '';
1495
  position: absolute;
1496
  left: -5px;
1497
  top: 5px;
1498
  width: 0;
1499
  height: 0;
1500
  border: 5px solid transparent;
1501
  border-top-color: blue;
1502
}
1443 1503

  
1444 1504
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1445 1505
.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
test/integration/routing/gantts_test.rb
26 26

  
27 27
    should_route 'GET /projects/foo/issues/gantt' => 'gantts#show', :project_id => 'foo'
28 28
    should_route 'GET /projects/foo/issues/gantt.pdf' => 'gantts#show', :project_id => 'foo', :format => 'pdf'
29

  
30
    should_route 'PUT /gantt/123/change_duration' => 'gantts#change_duration', :id => '123'
29 31
  end
30 32
end
test/unit/lib/redmine/helpers/gantt_test.rb
235 235
    @project.issues << @issue
236 236
    @output_buffer = @gantt.lines
237 237

  
238
    assert_select "div.project.task_todo"
239
    assert_select "div.project.starting"
240
    assert_select "div.project.ending"
241
    assert_select "div.label.project", /#{@project.name}/
238
    assert_select "div.task.project" do
239
      assert_select "> div.task_todo" do
240
        assert_select "> div.label", /#{@project.name}/
241
      end
242
      assert_select "> div.starting"
243
      assert_select "> div.ending"
244
    end
242 245

  
243
    assert_select "div.version.task_todo"
244
    assert_select "div.version.starting"
245
    assert_select "div.version.ending"
246
    assert_select "div.label.version", /#{@version.name}/
246
    assert_select "div.task.version" do
247
      assert_select "> div.task_todo" do
248
        assert_select "div.label", /#{@version.name}/
249
      end
250
      assert_select "> div.starting"
251
      assert_select "> div.ending"
252
    end
247 253

  
248
    assert_select "div.task_todo"
249
    assert_select "div.task.label", /#{@issue.done_ratio}/
250
    assert_select "div.tooltip", /#{@issue.subject}/
254
    assert_select "div.task" do
255
      assert_select "> div.task_todo" do
256
        assert_select "> div.label", /#{@issue.done_ratio}/
257
        assert_select "> div.tooltip", /#{@issue.subject}/
258
      end
259
    end
251 260
  end
252 261

  
253 262
  test "#selected_column_content" do
......
331 340
    @project.stubs(:start_date).returns(today - 7)
332 341
    @project.stubs(:due_date).returns(today + 7)
333 342
    @output_buffer = @gantt.line_for_project(@project, :format => :html)
334
    assert_select "div.project.label", :text => @project.name
343
    assert_select "div.task.project > div.task_todo" do
344
      assert_select "> div.label", :text => @project.name
345
    end
335 346
  end
336 347

  
337 348
  test "#line_for_version" do
......
341 352
    version.stubs(:due_date).returns(today + 7)
342 353
    version.stubs(:visible_fixed_issues => stub(:completed_percent => 30))
343 354
    @output_buffer = @gantt.line_for_version(version, :format => :html)
344
    assert_select "div.version.label", :text => /Foo/
345
    assert_select "div.version.label", :text => /30%/
355
    assert_select "div.task.version > div.task_todo" do
356
      assert_select "> div.label", :text => 'Foo 30%'
357
    end
346 358
  end
347 359

  
348 360
  test "#line_for_issue" do
349 361
    create_gantt
350 362
    issue = Issue.generate!(:project => @project, :start_date => today - 7, :due_date => today + 7, :done_ratio => 30)
351 363
    @output_buffer = @gantt.line_for_issue(issue, :format => :html)
352
    assert_select "div.task.label", :text => /#{issue.status.name}/
353
    assert_select "div.task.label", :text => /30%/
354
    assert_select "div.tooltip", /#{issue.subject}/
364
    assert_select "div.task_todo" do
365
      assert_select "> div.label", :text => "#{issue.status.name} 30%"
366
      assert_select "> div.tooltip", /#{issue.subject}/
367
    end
355 368
  end
356 369

  
357 370
  test "#line todo line should start from the starting point on the left" do
......
365 378
    [gantt_start - 1, gantt_start].each do |start_date|
366 379
      @output_buffer = @gantt.line(start_date, gantt_start, 30, false, 'line', :format => :html, :zoom => 4)
367 380
      # the leftmost date (Date.today - 14 days)
368
      assert_select 'div.task_todo[style*="left:0px"]', 1, @output_buffer
369
      assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer
381
      assert_select 'div.task_todo[style*="left:0px"][style*="width:4px"]', 1, @output_buffer
370 382
    end
371 383
  end
372 384

  
......
375 387
    [gantt_end, gantt_end + 1].each do |end_date|
376 388
      @output_buffer = @gantt.line(gantt_end, end_date, 30, false, 'line', :format => :html, :zoom => 4)
377 389
      # the rightmost date (Date.today + 14 days)
378
      assert_select 'div.task_todo[style*="left:112px"]', 1, @output_buffer
379
      assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer
390
      assert_select 'div.task_todo[style*="left:112px"][style*="width:4px"]', 1, @output_buffer
380 391
    end
381 392
  end
382 393

  
383 394
  test "#line todo line should be the total width" do
384 395
    create_gantt
385 396
    @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4)
386
    assert_select 'div.task_todo[style*="width:58px"]', 1
397
    assert_select 'div.task_todo[style*="width:60px"]', 1
387 398
  end
388 399

  
389 400
  test "#line late line should start from the starting point on the left" do
390 401
    create_gantt
391 402
    @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4)
392
    assert_select 'div.task_late[style*="left:28px"]', 1
403
    assert_select 'div.task_todo[style*="left:28px"]' do
404
      assert_select '> div.task_late', 1
405
    end
393 406
  end
394 407

  
395 408
  test "#line late line should be the total delayed width" do
396 409
    create_gantt
397 410
    @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4)
398
    assert_select 'div.task_late[style*="width:30px"]', 1
411
    assert_select 'div.task_late[style*="width:32px"]', 1
399 412
  end
400 413

  
401 414
  test "#line late line should be the same width as task_todo if start date and end date are the same day" do
402 415
    create_gantt
403 416
    @output_buffer = @gantt.line(today - 7, today - 7, 0, false, 'line', :format => :html, :zoom => 4)
404
    assert_select 'div.task_late[style*="width:2px"]', 1
405
    assert_select 'div.task_todo[style*="width:2px"]', 1
417
    assert_select 'div.task_todo[style*="width:4px"]' do
418
      assert_select '> div.task_late[style*="width:4px"]', 1
419
    end
406 420
  end
407 421

  
408 422
  test "#line late line should be the same width as task_todo if start date and today are the same day" do
409 423
    create_gantt
410 424
    @output_buffer = @gantt.line(today, today, 0, false, 'line', :format => :html, :zoom => 4)
411
    assert_select 'div.task_late[style*="width:2px"]', 1
412
    assert_select 'div.task_todo[style*="width:2px"]', 1
425
    assert_select 'div.task_todo[style*="width:4px"]' do
426
      assert_select '> div.task_late[style*="width:4px"]', 1
427
    end
413 428
  end
414 429

  
415 430
  test "#line done line should start from the starting point on the left" do
416 431
    create_gantt
417 432
    @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4)
418
    assert_select 'div.task_done[style*="left:28px"]', 1
433
    assert_select 'div.task_todo[style*="left:28px"]' do
434
      assert_select '> div.task_done', 1
435
    end
419 436
  end
420 437

  
421 438
  test "#line done line should be the width for the done ratio" do
422 439
    create_gantt
423 440
    @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4)
424
    # 15 days * 4 px * 30% - 2 px for borders = 16 px
425
    assert_select 'div.task_done[style*="width:16px"]', 1
441
    # 15 days * 4 px * 30% = 18 px
442
    assert_select 'div.task_done[style*="width:18px"]', 1
426 443
  end
427 444

  
428 445
  test "#line done line should be the total width for 100% done ratio" do
429 446
    create_gantt
430 447
    @output_buffer = @gantt.line(today - 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4)
431
    # 15 days * 4 px - 2 px for borders = 58 px
432
    assert_select 'div.task_done[style*="width:58px"]', 1
448
    # 15 days * 4 px = 60 px
449
    assert_select 'div.task_done[style*="width:60px"]', 1
433 450
  end
434 451

  
435 452
  test "#line done line should be the total width for 100% done ratio with same start and end dates" do
436 453
    create_gantt
437 454
    @output_buffer = @gantt.line(today + 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4)
438
    assert_select 'div.task_done[style*="width:2px"]', 1
455
    assert_select 'div.task_done[style*="width:4px"]', 1
439 456
  end
440 457

  
441 458
  test "#line done line should not be the total done width if the gantt starts after start date" do
442 459
    create_gantt
443 460
    @output_buffer = @gantt.line(today - 16, today - 2, 30, false, 'line', :format => :html, :zoom => 4)
444
    assert_select 'div.task_done[style*="left:0px"]', 1
445
    assert_select 'div.task_done[style*="width:8px"]', 1
461
    assert_select 'div.task_todo[style*="left:0px"]' do
462
      assert_select '> div.task_done[style*="width:10px"]', 1
463
    end
446 464
  end
447 465

  
448 466
  test "#line starting marker should appear at the start date" do
449
-