Project

General

Profile

Feature #2024 » 0001-refs-2024-integrate-gantt-change_duration.patch

Taro Matsuzawa, 2024-03-26 07:49

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 = duration_params
68
      unless @obj.save
69
        render_403(:message => @obj.errors.full_messages.join)
70
        raise ActiveRecord::Rollback
71
      end
72
      retrieve_query
73
    rescue ActiveRecord::StaleObjectError
74
      render_403(:message => :notice_issue_update_conflict)
75
    rescue ActiveRecord::RecordNotFound
76
      render_404
77
    end
78
  end
79

  
80
  private
81

  
82
  def duration_params
83
    params.require(:change_duration).permit(:start_date, :due_date, :lock_version)
84
  end
58 85
end
app/views/gantts/change_duration.js.erb
1
var elm;
2
<%
3
gantt = Redmine::Helpers::Gantt.new(params)
4
durations = gantt.duration(@obj, self, @query)
5
durations.each do |duration|
6
  elm_subject = duration[:elm_subject]
7
  elm_todo = duration[:elm_todo]
8
  todo_context = duration[:todo_context]
9
  subject_content = duration[:subject_content]
10
  columns = duration[:columns]
11
  obj = duration[:obj]
12
%>
13
if ($('<%= raw(elm_subject) %>').length) {
14
  $('<%= elm_todo %>').each(function (_, task) {
15
    var el_parent = $(task).parent();
16
    el_parent.html('<%= raw(todo_context) %>');
17
    var number_of_rows = el_parent.attr('data-number-of-rows');
18
    if (number_of_rows) {
19
      el_parent.find('div[data-number-of-rows]').attr('data-number-of-rows', number_of_rows);
20
    }
21
  });
22
  $('<%= raw(elm_subject) %>').replaceWith('<%= raw(subject_content) %>');
23
  <% columns.each do |column| -%>
24
  elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>');
25
  if(elm.length) {
26
    elm.html('<%= escape_javascript(column_content(column, obj)) %>');
27
  }
28
  <% end -%>
29
}
30
<% end -%>
config/routes.rb
63 63

  
64 64
  get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
65 65
  get '/issues/gantt', :to => 'gantts#show'
66
  put '/issues/gantt/:id/change_duration', :to => 'gantts#change_duration', :as => 'gantt_change_duration'
66 67

  
67 68
  get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
68 69
  get '/issues/calendar', :to => 'calendars#show'
lib/redmine/helpers/gantt.rb
629 629
        pdf.Output
630 630
      end
631 631

  
632
      def duration(issue, view, query)
633
        @view = view
634
        @query = query
635
        draw_obj = []
636

  
637
        select_precedes = ->(issue) do
638
          issue.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to).each do |follows|
639
            next if draw_obj.include?(follows)
640

  
641
            while follows
642
              draw_obj.push(follows, follows.fixed_version, follows.project)
643
              select_precedes.call(follows)
644
              follows.children.each do |child|
645
                draw_obj.push(child, child.fixed_version, child.project)
646
                select_precedes.call(child)
647
              end
648
              follows = follows.parent
649
            end
650
          end
651
        end
652

  
653
        while issue
654
          draw_obj.push(issue, issue.fixed_version, issue.project)
655
          select_precedes.call(issue)
656
          issue = issue.parent
657
        end
658
        draw_objs = draw_obj.compact.uniq
659
        draw_objs.reject!{|obj| ![Project, Version, Issue].include?(obj.class)}
660

  
661
        return_values = []
662
        draw_objs.each do |obj|
663
          @number_of_rows = 0
664
          @lines = +''
665
          render_object_row(obj, {format: :html, only: :lines, zoom: 2 ** @zoom, top: 0, top_increment: 20})
666
          todo_content = Nokogiri::HTML.parse(@lines)
667
          todo_context = todo_content.xpath(
668
            "//div[contains(@class, 'task') and contains(@class, 'line')]/*"
669
          ).to_s.tr("\n", '').gsub("'", "\\\\'")
670

  
671
          klass_name = obj.class.name.underscore
672
          elm_todo = "[id=task-todo-#{klass_name}-#{obj.id}]"
673
          css_subject = 'span:not(.expander)'
674
          elm_subject = "[id=#{klass_name}-#{obj.id}] > #{css_subject}"
675

  
676
          subject_content = Nokogiri::HTML.parse(html_subject_content(obj))
677
          subject_content = subject_content.css(css_subject).to_s.tr("\n", '').gsub("'", "\\\\'")
678

  
679
          columns = []
680
          case obj
681
          when Issue
682
            columns = query.columns
683
          end
684
          return_values << {:elm_subject => elm_subject,
685
                            :elm_todo => elm_todo,
686
                            :todo_context => todo_context,
687
                            :subject_content => subject_content,
688
                            :columns => columns,
689
                            :obj => obj}
690
        end
691
        return_values
692
      end
693

  
632 694
      private
633 695

  
634 696
      def coordinates(start_date, end_date, progress, zoom=nil)
......
774 836
          tag_options[:class] = "version-name"
775 837
          has_children = object.fixed_issues.exists?
776 838
        when Project
839
          tag_options[:id] = "project-#{object.id}"
777 840
          tag_options[:class] = "project-name"
778 841
          has_children = object.issues.exists? || object.versions.exists?
779 842
        end
......
856 919
          end
857 920
        # Renders the task bar, with progress and late
858 921
        if coords[:bar_start] && coords[:bar_end]
859
          width = coords[:bar_end] - coords[:bar_start] - 2
922
          width = coords[:bar_end] - coords[:bar_start]
860 923
          style = +""
861
          style << "top:#{params[:top]}px;"
862 924
          style << "left:#{coords[:bar_start]}px;"
863 925
          style << "width:#{width}px;"
864
          html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue)
865
          html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
926
          html_id =
927
            case object
928
            when Project
929
              "task-todo-project-#{object.id}"
930
            when Version
931
              "task-todo-version-#{object.id}"
932
            when Issue
933
              "task-todo-issue-#{object.id}"
934
            end
866 935
          content_opt = {:style => style,
867
                         :class => "#{css} task_todo",
936
                         :class => "task_todo",
868 937
                         :id => html_id,
869 938
                         :data => {}}
870 939
          if object.is_a?(Issue)
......
872 941
            if rels.present?
873 942
              content_opt[:data] = {"rels" => rels.to_json}
874 943
            end
944
            content_opt[:data].merge!({
945
                                        :url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path(
946
                                          object
947
                                        ),
948
                                        :object => {
949
                                          :start_date => object.start_date,
950
                                          :due_date => object.due_date,
951
                                          :lock_version => object.lock_version,
952
                                        }.to_json,
953
                                      })
875 954
          end
876 955
          content_opt[:data].merge!(data_options)
877
          output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
956
          bar_contents = []
878 957
          if coords[:bar_late_end]
879
            width = coords[:bar_late_end] - coords[:bar_start] - 2
958
            width = coords[:bar_late_end] - coords[:bar_start]
880 959
            style = +""
881
            style << "top:#{params[:top]}px;"
882
            style << "left:#{coords[:bar_start]}px;"
883 960
            style << "width:#{width}px;"
884
            output << view.content_tag(:div, '&nbsp;'.html_safe,
885
                                       :style => style,
886
                                       :class => "#{css} task_late",
887
                                       :data => data_options)
961
            bar_contents << view.content_tag(:div, '&nbsp;'.html_safe,
962
                                             :style => style,
963
                                             :class => "#{css} task_late",
964
                                             :data => data_options)
888 965
          end
889 966
          if coords[:bar_progress_end]
890
            width = coords[:bar_progress_end] - coords[:bar_start] - 2
967
            width = coords[:bar_progress_end] - coords[:bar_start]
891 968
            style = +""
892
            style << "top:#{params[:top]}px;"
893
            style << "left:#{coords[:bar_start]}px;"
894 969
            style << "width:#{width}px;"
895 970
            html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue)
896 971
            html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
897
            output << view.content_tag(:div, '&nbsp;'.html_safe,
972
            bar_contents << view.content_tag(:div, '&nbsp;'.html_safe,
973
                                             :style => style,
974
                                             :class => "task_done",
975
                                             :id => html_id,
976
                                             :data => data_options)
977
          end
978

  
979
          # Renders the tooltip
980
          if object.is_a?(Issue)
981
            s = view.content_tag(:span,
982
                                 view.render_issue_tooltip(object).html_safe,
983
                                 :class => "tip")
984
            s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection')
985
            style = +""
986
            style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
987
            style << "height:12px;"
988
            bar_contents << view.content_tag(:div, s.html_safe,
989
                                             :style => style,
990
                                             :class => "tooltip hascontextmenu",
991
                                             :data => data_options)
992
          end
993

  
994
          # Renders the label on the right
995
          if label
996
            style = +""
997
            style << "top:0px;"
998
            style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;"
999
            style << "height:12px;"
1000
            bar_contents << view.content_tag(:div, label,
1001
                                             :style => style,
1002
                                             :class => "label",
1003
                                             :data => data_options)
1004
          end
1005

  
1006
          bar_contents = bar_contents.join.presence
1007
          output << view.content_tag(:div, (bar_contents || '&nbsp;').html_safe, content_opt)
1008
        else
1009
          # Renders the label on the right
1010
          if label
1011
            style = +""
1012
            style << "top:1px;"
1013
            style << "left:#{(coords[:bar_end] || 0) + 8}px;"
1014
            output << view.content_tag(:div, label,
898 1015
                                       :style => style,
899
                                       :class => "#{css} task_done",
900 1016
                                       :id => html_id,
1017
                                       :class => "label",
901 1018
                                       :data => data_options)
902 1019
          end
903 1020
        end
......
905 1022
        if markers
906 1023
          if coords[:start]
907 1024
            style = +""
908
            style << "top:#{params[:top]}px;"
909 1025
            style << "left:#{coords[:start]}px;"
910
            style << "width:15px;"
911 1026
            output << view.content_tag(:div, '&nbsp;'.html_safe,
912 1027
                                       :style => style,
913
                                       :class => "#{css} marker starting",
1028
                                       :class => "marker starting",
914 1029
                                       :data => data_options)
915 1030
          end
916 1031
          if coords[:end]
917 1032
            style = +""
918
            style << "top:#{params[:top]}px;"
919 1033
            style << "left:#{coords[:end]}px;"
920
            style << "width:15px;"
921 1034
            output << view.content_tag(:div, '&nbsp;'.html_safe,
922 1035
                                       :style => style,
923
                                       :class => "#{css} marker ending",
1036
                                       :class => "marker ending",
924 1037
                                       :data => data_options)
925 1038
          end
926 1039
        end
927
        # Renders the label on the right
928
        if label
929
          style = +""
930
          style << "top:#{params[:top]}px;"
931
          style << "left:#{(coords[:bar_end] || 0) + 8}px;"
932
          style << "width:15px;"
933
          output << view.content_tag(:div, label,
934
                                     :style => style,
935
                                     :class => "#{css} label",
936
                                     :data => data_options)
937
        end
938
        # Renders the tooltip
939
        if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
940
          s = view.content_tag(:span,
941
                               view.render_issue_tooltip(object).html_safe,
942
                               :class => "tip")
943
          s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
944
                                :value => object.id, :style => 'display:none;',
945
                                :class => 'toggle-selection')
946
          style = +""
947
          style << "position: absolute;"
948
          style << "top:#{params[:top]}px;"
949
          style << "left:#{coords[:bar_start]}px;"
950
          style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
951
          style << "height:12px;"
952
          output << view.content_tag(:div, s.html_safe,
953
                                     :style => style,
954
                                     :class => "tooltip hascontextmenu",
955
                                     :data => data_options)
956
        end
1040
        output = view.content_tag(:div, output.html_safe,
1041
          :class => "#{css} line",
1042
          :style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;",
1043
          :data => data_options
1044
        )
957 1045
        @lines << output
958 1046
        output
959 1047
      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_done.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});
......
303 305
    $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true);
304 306
  });
305 307
}
308

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

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

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

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

  
414
$(document).ready(initGanttDnD);
public/stylesheets/application.css
1496 1496

  
1497 1497
.task {
1498 1498
  position: absolute;
1499
  height:8px;
1499
  height:10px;
1500 1500
  font-size:0.8em;
1501 1501
  color:#888;
1502 1502
  padding:0;
......
1505 1505
  white-space:nowrap;
1506 1506
}
1507 1507

  
1508
.task.label {width:100%;}
1509
.task.label.project, .task.label.version { font-weight: bold; }
1508
.task.line { left: 0; }
1509
.task div.tooltip:hover span.tip { font-size: inherit; }
1510
.task .task_todo .label { font-size: inherit; }
1511
.task.project .task_todo .label { margin-top: -4px; }
1512
.task.version .task_todo .label { margin-top: -3px; }
1510 1513

  
1511
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1512
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1513
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1514
.task .label { position: absolute; width: auto; }
1515
.task.project .label, .task.version .label { font-weight: bold; }
1514 1516

  
1515
.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1516
.task_late.parent, .task_done.parent { height: 3px;}
1517
.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;}
1518
.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;}
1517
.task_late { position: absolute; height: inherit; background:#f66; }
1518
.task_done { position: absolute; height: inherit; background:#00c600; }
1519
.task_todo { position: absolute; height: inherit; background:#aaa; }
1519 1520

  
1520
.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1521
.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1522
.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1523
.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1521
.parent .task_todo { background: #888; height: 5px; }
1522
.parent .task_late, .parent .task_done { height: 5px; }
1523
.parent .marker {
1524
  background: #888;
1525
  display: inline-block;
1526
  position: absolute;
1527
  width: 8px;
1528
  height: 6px;
1529
  margin-left: -5px;
1530
  margin-bottom: -4px;
1531
}
1532

  
1533
.version .task_late { background:#f66; height: 4px; }
1534
.version .task_done { background:#00c600; height: 4px; }
1535
.version .task_todo { background:#aaa; height: 4px; margin-top: 3px; }
1536
.verison .marker {
1537
  width: 0;
1538
  height: 0;
1539
  border: 5px solid trasnparent;
1540
  border-bottom-color: black;
1541
  position: absolute;
1542
  margin-top: -5px;
1543
  margin-left: -6px;
1544
}
1545
.version .marker:after {
1546
  content: '';
1547
  position: absolute;
1548
  left: -5px;
1549
  top: 5px;
1550
  width: 0;
1551
  height: 0;
1552
  border: 5px solid transparent;
1553
  border-top-color: black;
1554
}
1524 1555

  
1525
.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1526
.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1527
.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1528
.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1556
.project .task_late { background:#f66; height: 2px; }
1557
.project .task_done { background:#00c600; height: 2px; }
1558
.project .task_todo { background:#aaa; height: 2px; margin-top: 4px; }
1559
.project .marker {
1560
  width: 0;
1561
  height: 0;
1562
  border: 5px solid transparent;
1563
  border-bottom-color: blue;
1564
  position: absolute;
1565
  margin-top: -5px;
1566
  margin-left: -6px;
1567
}
1568
.project .marker:after {
1569
  content: '';
1570
  position: absolute;
1571
  left: -5px;
1572
  top: 5px;
1573
  width: 0;
1574
  height: 0;
1575
  border: 5px solid transparent;
1576
  border-top-color: blue;
1577
}
1529 1578

  
1530 1579
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1531 1580
.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 /issues/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", :text => /#{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
(35-35/35)