2024-moving-gantt-bar.patch

Yuichi HARADA, 2020-11-27 07:20

Download (39 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 = "#task-todo-#{klass_name}-#{obj.id}"
49
  css_subject = 'span.' <<
50
    case obj
51
    when Project
52
      'icon-projects'
53
    when Version
54
      'icon-package'
55
    when Issue
56
      'icon-issue'
57
    end
58
  elm_subject = "##{klass_name}-#{obj.id} #{css_subject}"
59

  
60
  subject_content = Nokogiri::HTML.parse(gantt.__send__(:html_subject_content, obj))
61
  subject_content = subject_content.css(css_subject).to_s.tr("\n",'').gsub(/'/, "\\\\'")
62
-%>
63
if($('<%= elm_subject %>').length){
64
  $('<%= elm_todo %>').parent().html('<%= raw(todo_content) %>');
65
  $('<%= elm_subject %>').replaceWith('<%= raw(subject_content) %>');
66
<%
67
  case obj
68
  when Issue
69
    @query.columns.each do |column|
70
-%>
71
  elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>');
72
  if(elm.length){
73
    elm.html('<%= escape_javascript(column_content(column, obj)) %>');
74
  }
75
<%
76
    end
77
  end
78
-%>
79
}
80
<%
81
end
82
-%>
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
750 750
          tag_options[:class] = "version-name"
751 751
          has_children = object.fixed_issues.exists?
752 752
        when Project
753
          tag_options[:id] = "project-#{object.id}"
753 754
          tag_options[:class] = "project-name"
754 755
          has_children = object.issues.exists? || object.versions.exists?
755 756
        end
......
828 829
          end
829 830
        # Renders the task bar, with progress and late
830 831
        if coords[:bar_start] && coords[:bar_end]
831
          width = coords[:bar_end] - coords[:bar_start] - 2
832
          width = coords[:bar_end] - coords[:bar_start]
832 833
          style = +""
833
          style << "top:#{params[:top]}px;"
834 834
          style << "left:#{coords[:bar_start]}px;"
835 835
          style << "width:#{width}px;"
836
          html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue)
837
          html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
836
          html_id =
837
            case object
838
            when Project
839
              "task-todo-project-#{object.id}"
840
            when Version
841
              "task-todo-version-#{object.id}"
842
            when Issue
843
              "task-todo-issue-#{object.id}"
844
            end
838 845
          content_opt = {:style => style,
839
                         :class => "#{css} task_todo",
846
                         :class => "task_todo",
840 847
                         :id => html_id,
841 848
                         :data => {}}
842 849
          if object.is_a?(Issue)
......
844 851
            if rels.present?
845 852
              content_opt[:data] = {"rels" => rels.to_json}
846 853
            end
854
            content_opt[:data].merge!({
855
              :url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path(
856
                object
857
              ),
858
              :object => {
859
                :start_date => object.start_date,
860
                :due_date => object.due_date,
861
                :lock_version => object.lock_version,
862
              }.to_json,
863
            })
847 864
          end
848 865
          content_opt[:data].merge!(data_options)
849
          output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
866
          bar_contents = []
850 867
          if coords[:bar_late_end]
851
            width = coords[:bar_late_end] - coords[:bar_start] - 2
868
            width = coords[:bar_late_end] - coords[:bar_start]
852 869
            style = +""
853
            style << "top:#{params[:top]}px;"
854
            style << "left:#{coords[:bar_start]}px;"
855 870
            style << "width:#{width}px;"
856
            output << view.content_tag(:div, '&nbsp;'.html_safe,
857
                                       :style => style,
858
                                       :class => "#{css} task_late",
859
                                       :data => data_options)
871
            bar_contents << view.content_tag(:div, '&nbsp;'.html_safe,
872
                                             :style => style,
873
                                             :class => "task_late",
874
                                             :data => data_options)
860 875
          end
861 876
          if coords[:bar_progress_end]
862
            width = coords[:bar_progress_end] - coords[:bar_start] - 2
877
            width = coords[:bar_progress_end] - coords[:bar_start]
863 878
            style = +""
864
            style << "top:#{params[:top]}px;"
865
            style << "left:#{coords[:bar_start]}px;"
866 879
            style << "width:#{width}px;"
867 880
            html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue)
868 881
            html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
869
            output << view.content_tag(:div, '&nbsp;'.html_safe,
882
            bar_contents << view.content_tag(:div, '&nbsp;'.html_safe,
883
                                             :style => style,
884
                                             :class => "task_done",
885
                                             :id => html_id,
886
                                             :data => data_options)
887
          end
888

  
889
          # Renders the tooltip
890
          if object.is_a?(Issue)
891
            s = view.content_tag(:span,
892
                               view.render_issue_tooltip(object).html_safe,
893
                               :class => "tip")
894
            s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection')
895
            style = +""
896
            style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
897
            style << "height:12px;"
898
            bar_contents << view.content_tag(:div, s.html_safe,
899
                                             :style => style,
900
                                             :class => "tooltip hascontextmenu",
901
                                             :data => data_options)
902
          end
903

  
904
          # Renders the label on the right
905
          if label
906
            style = +""
907
            style << "top:0px;"
908
            style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;"
909
            bar_contents << view.content_tag(:div, label,
910
                                             :style => style,
911
                                             :class => "label",
912
                                             :data => data_options)
913
          end
914

  
915
          bar_contents = bar_contents.join.presence
916
          output << view.content_tag(:div, (bar_contents || '&nbsp;').html_safe, content_opt)
917
        else
918
          # Renders the label on the right
919
          if label
920
            style = +""
921
            style << "top:1px;"
922
            style << "left:#{(coords[:bar_end] || 0) + 8}px;"
923
            output << view.content_tag(:div, label,
870 924
                                       :style => style,
871
                                       :class => "#{css} task_done",
872
                                       :id => html_id,
925
                                       :class => "label",
873 926
                                       :data => data_options)
874 927
          end
875 928
        end
......
877 930
        if markers
878 931
          if coords[:start]
879 932
            style = +""
880
            style << "top:#{params[:top]}px;"
881 933
            style << "left:#{coords[:start]}px;"
882
            style << "width:15px;"
883 934
            output << view.content_tag(:div, '&nbsp;'.html_safe,
884 935
                                       :style => style,
885
                                       :class => "#{css} marker starting",
936
                                       :class => "marker starting",
886 937
                                       :data => data_options)
887 938
          end
888 939
          if coords[:end]
889 940
            style = +""
890
            style << "top:#{params[:top]}px;"
891 941
            style << "left:#{coords[:end]}px;"
892
            style << "width:15px;"
893 942
            output << view.content_tag(:div, '&nbsp;'.html_safe,
894 943
                                       :style => style,
895
                                       :class => "#{css} marker ending",
944
                                       :class => "marker ending",
896 945
                                       :data => data_options)
897 946
          end
898 947
        end
899
        # Renders the label on the right
900
        if label
901
          style = +""
902
          style << "top:#{params[:top]}px;"
903
          style << "left:#{(coords[:bar_end] || 0) + 8}px;"
904
          style << "width:15px;"
905
          output << view.content_tag(:div, label,
906
                                     :style => style,
907
                                     :class => "#{css} label",
908
                                     :data => data_options)
909
        end
910
        # Renders the tooltip
911
        if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
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 << "position: absolute;"
918
          style << "top:#{params[:top]}px;"
919
          style << "left:#{coords[:bar_start]}px;"
920
          style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
921
          style << "height:12px;"
922
          output << view.content_tag(:div, s.html_safe,
923
                                     :style => style,
924
                                     :class => "tooltip hascontextmenu",
925
                                     :data => data_options)
926
        end
948
        output = view.content_tag(:div, output.html_safe,
949
          :class => "#{css} line",
950
          :style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;",
951
          :data => data_options
952
        )
927 953
        @lines << output
928 954
        output
929 955
      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
1401 1401

  
1402 1402
.task {
1403 1403
  position: absolute;
1404
  height:8px;
1404
  height:10px;
1405 1405
  font-size:0.8em;
1406 1406
  color:#888;
1407 1407
  padding:0;
......
1410 1410
  white-space:nowrap;
1411 1411
}
1412 1412

  
1413
.task.label {width:100%;}
1414
.task.label.project, .task.label.version { font-weight: bold; }
1413
.task.line { left: 0; }
1414
.task div.tooltip:hover span.tip { font-size: inherit; }
1415
.task .task_todo .label { font-size: inherit; }
1416
.task.project .task_todo .label { margin-top: -4px; }
1417
.task.version .task_todo .label { margin-top: -3px; }
1415 1418

  
1416
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1417
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1418
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1419
.task .label { position: absolute; width: auto; }
1420
.task.project .label, .task.version .label { font-weight: bold; }
1419 1421

  
1420
.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1421
.task_late.parent, .task_done.parent { height: 3px;}
1422
.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;}
1423
.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;}
1422
.task_late { position: absolute; height: inherit; background:#f66; }
1423
.task_done { position: absolute; height: inherit; background:#00c600; }
1424
.task_todo { position: absolute; height: inherit; background:#aaa; }
1424 1425

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

  
1449
.version .task_late { background:#f66; height: 4px; }
1450
.version .task_done { background:#00c600; height: 4px; }
1451
.version .task_todo { background:#aaa; height: 4px; margin-top: 3px; }
1452
.version .marker {
1453
  width: 0;
1454
  height: 0;
1455
  border: 5px solid transparent;
1456
  border-bottom-color: black;
1457
  position: absolute;
1458
  margin-top: -5px;
1459
  margin-left: -6px;
1460
}
1461
.version .marker:after {
1462
  content: '';
1463
  position: absolute;
1464
  left: -5px;
1465
  top: 5px;
1466
  width: 0;
1467
  height: 0;
1468
  border: 5px solid transparent;
1469
  border-top-color: black;
1470
}
1429 1471

  
1430
.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1431
.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1432
.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1433
.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1472
.project .task_late { background:#f66; height: 2px; }
1473
.project .task_done { background:#00c600; height: 2px; }
1474
.project .task_todo { background:#aaa; height: 2px; margin-top: 4px; }
1475
.project .marker {
1476
  width: 0;
1477
  height: 0;
1478
  border: 5px solid transparent;
1479
  border-bottom-color: blue;
1480
  position: absolute;
1481
  margin-top: -5px;
1482
  margin-left: -6px;
1483
}
1484
.project .marker:after {
1485
  content: '';
1486
  position: absolute;
1487
  left: -5px;
1488
  top: 5px;
1489
  width: 0;
1490
  height: 0;
1491
  border: 5px solid transparent;
1492
  border-top-color: blue;
1493
}
1434 1494

  
1435 1495
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1436 1496
.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