Redmine ガントチャート用パッチをVer3.3.2へ適用する

ファーエンドテクノロジーさんのRedmine ガントチャート用パッチを入れようと思いました。

こちらの記事より。
・Redmineを改善するパッチを書いて、OSSへの貢献もする仕事
　https://www.farend.co.jp/blog/2017/08/redmine-development/

##３項目のパッチ
①Feature #20481: ガントチャートの表示領域の幅をドラッグでリサイズ
・http://www.redmine.org/issues/20481
②Feature #10485: ガントチャートでコンテキストメニューを表示
・http://www.redmine.org/issues/10485#note-10
③Feature #26409: ガントチャートに担当者を表示
・http://www.redmine.org/issues/26409#note-2

**しかし、バージョン3.4.0では大丈夫でしたが、バージョン3.3.2でパッチがうまく動作しなかったので。**

## パッチを修正しました。

パッチはこちらです。
https://www.dropbox.com/s/1buqigmcnaxl0rp/gantt_patch_for_ver332.zip?dl=0

```patch:gantt_for_ver332.patch
diff -u -r redmine-3.3.2/app/models/issue_query.rb redmine-3.3.2_fix/app/models/issue_query.rb
--- redmine-3.3.2/app/models/issue_query.rb	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/app/models/issue_query.rb	2018-05-02 16:44:49.000000000 +0900
@@ -120,10 +120,20 @@
     options[:draw_progress_line] = (arg == '1' ? '1' : nil)
   end
 
+  def draw_assigned_to_names
+    r = options[:draw_assigned_to_names]
+    r == '1'
+  end
+
+  def draw_assigned_to_names=(arg)
+    options[:draw_assigned_to_names] = (arg == '1' ? '1' : nil)
+  end
+
   def build_from_params(params)
     super
     self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
     self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
+    self.draw_assigned_to_names = params[:draw_assigned_to_names] || (params[:query] && params[:query][:draw_assigned_to_names])
     self
   end
 
diff -u -r redmine-3.3.2/app/views/gantts/show.html.erb redmine-3.3.2_fix/app/views/gantts/show.html.erb
--- redmine-3.3.2/app/views/gantts/show.html.erb	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/app/views/gantts/show.html.erb	2018-05-02 17:32:34.000000000 +0900
@@ -49,6 +49,15 @@
             </label>
           </fieldset>
         </td>
+        <td>
+          <fieldset>
+            <legend><%= l(:field_assigned_to) %></legend>
+            <label for="draw_assigned_to_names">
+              <%= check_box 'query', 'draw_assigned_to_names', :id => 'draw_assigned_to_names' %>
+              <%= l(:label_display) %>
+            </label>
+          </fieldset>
+        </td>
       </tr>
     </table>
   </div>
@@ -86,6 +95,7 @@
 
   subject_width = 330
   header_height = 18
+  assigned_to_width = 100
 
   headers_height = header_height
   show_weeks = false
@@ -119,19 +129,18 @@
   <p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
 <% end %>
 
-<table style="width:100%; border:0; border-collapse: collapse;">
+<table style="width:100%; border:0; border-collapse: collapse;", class="gantt_container">
 <tr>
-<td style="width:<%= subject_width %>px; padding:0px;">
+<td style="width:<%= subject_width %>px; padding:0px;" class="gantt_subjects_column">
   <%
     style  = ""
     style += "position:relative;"
     style += "height: #{t_height + 24}px;"
     style += "width: #{subject_width + 1}px;"
   %>
-  <%= content_tag(:div, :style => style) do %>
+  <%= content_tag(:div, :style => style, :class => "gantt_subjects_container") do %>
     <%
       style  = ""
-      style += "right:-2px;"
       style += "width: #{subject_width}px;"
       style += "height: #{headers_height}px;"
       style += 'background: #eee;'
@@ -139,7 +148,6 @@
     <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
     <%
       style  = ""
-      style += "right:-2px;"
       style += "width: #{subject_width}px;"
       style += "height: #{t_height}px;"
       style += 'border-left: 1px solid #c0c0c0;'
@@ -147,7 +155,38 @@
     %>
     <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
     <%= content_tag(:div, :class => "gantt_subjects") do %>
-      <%= @gantt.subjects.html_safe %>
+      <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
+        <%= @gantt.subjects.html_safe %>
+      <% end %>    
+    <% end %>
+  <% end %>
+</td>
+
+<td style="width:<%= assigned_to_width %>px; padding:0px; display:none;" class="gantt_assigned_to_names_column">
+  <%
+    style  = ""
+    style += "position:relative;"
+    style += "height: #{t_height + 24}px;"
+    style += "width: #{assigned_to_width + 1}px;"
+  %>
+  <%= content_tag(:div, :style => style, :class => "gantt_assigned_to_names_container") do %>
+    <%
+      style  = ""
+      style += "width: #{assigned_to_width}px;"
+      style += "height: #{headers_height}px;"
+      style += 'background: #eee;'
+    %>
+    <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
+    <%
+      style  = ""
+      style += "width: #{assigned_to_width}px;"
+      style += "height: #{t_height}px;"
+      style += 'border-left: 1px solid #c0c0c0;'
+      style += 'overflow: hidden;'
+    %>
+    <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
+    <%= content_tag(:div, :class => "gantt_assigned_to_names") do %>
+      <%= @gantt.assigned_to_names.html_safe %>
     <% end %>
   <% end %>
 </td>
@@ -299,7 +338,9 @@
   <% end %>
 <% end %>
 
-<%= @gantt.lines.html_safe %>
+<%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
+  <%= @gantt.lines.html_safe %>
+<% end %>
 
 <% ###### Today red line (excluded from cache) ###### %>
 <% if User.current.today >= @gantt.date_from and User.current.today <= @gantt.date_to %>
@@ -363,10 +404,14 @@
 
 <%= javascript_tag do %>
   var issue_relation_type = <%= raw Redmine::Helpers::Gantt::DRAW_TYPES.to_json %>;
-  $(document).ready(drawGanttHandler);
-  $(window).resize(drawGanttHandler);
   $(function() {
-    $("#draw_relations").change(drawGanttHandler);
-    $("#draw_progress_line").change(drawGanttHandler);
+    drawGanttHandler();
+    resizableSubjectColumn();
+    $("#draw_relations, #draw_progress_line, #draw_assigned_to_names").change(drawGanttHandler);
+  });
+  $(window).resize(function() {
+    drawGanttHandler();
+    resizableSubjectColumn();
   });
 <% end %>
+<%= context_menu issues_context_menu_path %>
diff -u -r redmine-3.3.2/app/views/queries/_form.html.erb redmine-3.3.2_fix/app/views/queries/_form.html.erb
--- redmine-3.3.2/app/views/queries/_form.html.erb	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/app/views/queries/_form.html.erb	2018-05-02 16:51:17.000000000 +0900
@@ -42,6 +42,7 @@
   <p><label><%= l(:button_show) %></label>
   <label class="inline"><%= check_box_tag "query[draw_relations]", "1", @query.draw_relations %> <%= l(:label_related_issues) %></label>
   <label class="inline"><%= check_box_tag "query[draw_progress_line]", "1", @query.draw_progress_line %> <%= l(:label_gantt_progress_line) %></label>
+  <label class="inline"><%= check_box_tag "query[draw_assigned_to_names]", "1", @query.draw_assigned_to_names %> <%= l(:label_gantt_assigned_to_names) %></label>
   </p>
 </fieldset>
 <% end %>
diff -u -r redmine-3.3.2/config/locales/ja.yml redmine-3.3.2_fix/config/locales/ja.yml
--- redmine-3.3.2/config/locales/ja.yml	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/config/locales/ja.yml	2018-05-02 17:58:57.000000000 +0900
@@ -820,6 +820,7 @@
   label_parent_revision: 親
   label_child_revision: 子
   label_gantt_progress_line: イナズマ線
+  label_gantt_assigned_to_names: 担当者
 
   button_login: ログイン
   button_submit: 送信
diff -u -r redmine-3.3.2/lib/redmine/helpers/gantt.rb redmine-3.3.2_fix/lib/redmine/helpers/gantt.rb
--- redmine-3.3.2/lib/redmine/helpers/gantt.rb	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/lib/redmine/helpers/gantt.rb	2018-05-02 16:55:51.000000000 +0900
@@ -76,6 +76,7 @@
         @date_to = (@date_from >> @months) - 1
         @subjects = ''
         @lines = ''
+        @assigned_to_names = ''
         @number_of_rows = nil
         @truncated = false
         if options.has_key?(:max_rows)
@@ -135,6 +136,12 @@
         @lines
       end
 
+      # Renders the assigned_to_names of the Gantt chart, the right side of subjects.
+      def assigned_to_names(options={})
+        render(options.merge(:only => :assigned_to_names)) unless @assigned_to_names_rendered
+        @assigned_to_names
+      end
+
       # Returns issues that will be rendered
       def issues
         @issues ||= @query.issues(
@@ -196,8 +203,9 @@
                    :indent_increment => 20, :render => :subject,
                    :format => :html}.merge(options)
         indent = options[:indent] || 4
-        @subjects = '' unless options[:only] == :lines
-        @lines = '' unless options[:only] == :subjects
+        @subjects = '' unless options[:only] == :lines || options[:only] == :assigned_to_names
+        @lines = '' unless options[:only] == :subjects || options[:only] == :assigned_to_names
+        @assigned_to_names = '' unless options[:only] == :lines || options[:only] == :subjects
         @number_of_rows = 0
         begin
           Project.project_tree(projects) do |project, level|
@@ -207,8 +215,9 @@
         rescue MaxLinesLimitReached
           @truncated = true
         end
-        @subjects_rendered = true unless options[:only] == :lines
-        @lines_rendered = true unless options[:only] == :subjects
+        @subjects_rendered = true unless options[:only] == :lines || options[:only] == :assigned_to_names
+        @lines_rendered = true unless options[:only] == :subjects || options[:only] == :assigned_to_names
+        @assigned_to_names_rendered = true unless options[:only] == :lines || options[:only] == :subjects
         render_end(options)
       end
 
@@ -254,8 +263,9 @@
 
       def render_object_row(object, options)
         class_name = object.class.name.downcase
-        send("subject_for_#{class_name}", object, options) unless options[:only] == :lines
-        send("line_for_#{class_name}", object, options) unless options[:only] == :subjects
+        send("subject_for_#{class_name}", object, options) unless options[:only] == :lines || options[:only] == :assigned_to_names
+        send("line_for_#{class_name}", object, options) unless options[:only] == :subjects || options[:only] == :assigned_to_names
+        assigned_to_name(object, options) unless options[:only] == :lines || options[:only] == :subjects
         options[:top] += options[:top_increment]
         @number_of_rows += 1
         if @max_rows && @number_of_rows >= @max_rows
@@ -320,6 +330,18 @@
         end
       end
 
+      def assigned_to_name(issue, options)
+        if issue.is_a?(Issue) && options[:format] == :html && issue.assigned_to.present?
+          style = "position: absolute;top: #{options[:top]}px;"
+          content = view.avatar(issue.assigned_to,
+                                 :class => 'gravatar icon-gravatar',
+                                 :size => 13) + view.link_to_user(issue.assigned_to)
+          assigned_to_name = view.content_tag(:div, content.html_safe, :style => style, :class => 'issue-assigned-name')
+          @assigned_to_names << assigned_to_name
+          assigned_to_name
+        end
+      end
+
       def subject(label, options, object=nil)
         send "#{options[:format]}_subject", options, label, object
       end
@@ -667,6 +689,7 @@
                              :title => assigned_string).to_s.html_safe
           end
           s << view.link_to_issue(issue).html_safe
+          s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => issue.id, :style => 'display:none;', :class => 'toggle-selection')
           view.content_tag(:span, s, :class => css_classes).html_safe
         when Version
           version = object
@@ -701,7 +724,7 @@
         case object
         when Issue
           tag_options[:id] = "issue-#{object.id}"
-          tag_options[:class] = "issue-subject"
+          tag_options[:class] = "issue-subject hascontextmenu"
           tag_options[:title] = object.subject
         when Version
           tag_options[:id] = "version-#{object.id}"
@@ -838,6 +861,7 @@
           s = view.content_tag(:span,
                                view.render_issue_tooltip(object).html_safe,
                                :class => "tip")
+          s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection')          
           style = ""
           style << "position: absolute;"
           style << "top:#{params[:top]}px;"
@@ -846,7 +870,7 @@
           style << "height:12px;"
           output << view.content_tag(:div, s.html_safe,
                                      :style => style,
-                                     :class => "tooltip")
+                                     :class => "tooltip hascontextmenu")
         end
         @lines << output
         output
diff -u -r redmine-3.3.2/public/javascripts/context_menu.js redmine-3.3.2_fix/public/javascripts/context_menu.js
--- redmine-3.3.2/public/javascripts/context_menu.js	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/public/javascripts/context_menu.js	2018-05-02 18:00:28.000000000 +0900
@@ -7,8 +7,8 @@
 function contextMenuRightClick(event) {
   var target = $(event.target);
   if (target.is('a')) {return;}
-  var tr = target.parents('tr').first();
-  if (!tr.hasClass('hascontextmenu')) {return;}
+  var tr = target.closest('.hascontextmenu').first();
+  if (tr.length < 1) {return;}
   event.preventDefault();
   if (!contextMenuIsSelected(tr)) {
     contextMenuUnselectAll();
diff -u -r redmine-3.3.2/public/javascripts/gantt.js redmine-3.3.2_fix/public/javascripts/gantt.js
--- redmine-3.3.2/public/javascripts/gantt.js	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/public/javascripts/gantt.js	2018-05-02 16:58:22.000000000 +0900
@@ -161,6 +161,36 @@
   }
 }
 
+function drawAssignedToNames(){
+  if ($("#draw_assigned_to_names").prop('checked')){
+    $('td.gantt_assigned_to_names_column').show();
+    $('td.gantt_assigned_to_names_column').resizable({
+      alsoResize: ".gantt_assigned_to_names_container, .gantt_assigned_to_names_container>.gantt_hdr, .issue-assigned-name",
+      minWidth: 20,
+      handles: "e",
+      containment: "#content",
+      create: function( event, ui ) {
+        $(".ui-resizable-e").css("cursor","ew-resize");
+      }
+    }).on('resize', function (e) {
+        e.stopPropagation();
+    });
+    if(isMobile()) {
+      var width = Math.round($('.gantt_container').width()*0.15)
+      $('.gantt_assigned_to_names_container>.gantt_hdr').width(width-2);
+      $('.gantt_assigned_to_names_container, .gantt_assigned_to_names_column').width(width);
+      $('.issue-assigned-name').each(function(){
+        $(this).width($(".gantt_assigned_to_names_column").width());
+      });
+      $('td.gantt_assigned_to_names_column').resizable('disable');
+    }else{
+      $('td.gantt_assigned_to_names_column').resizable('enable');
+    };
+  }else{
+    $('td.gantt_assigned_to_names_column').hide();
+  }
+}
+
 function drawGanttHandler() {
   var folder = document.getElementById('gantt_draw_area');
   if(draw_gantt != null)
@@ -168,8 +198,31 @@
   else
     draw_gantt = Raphael(folder);
   setDrawArea();
+  drawAssignedToNames();
   if ($("#draw_progress_line").prop('checked'))
     drawGanttProgressLines();
   if ($("#draw_relations").prop('checked'))
     drawRelations();
 }
+
+function resizableSubjectColumn(){
+  $('.issue-subject, .project-name, .version-name').each(function(){
+    $(this).width($(".gantt_subjects_column").width()-$(this).position().left);
+  });
+  $('td.gantt_subjects_column').resizable({
+    alsoResize: '.gantt_subjects_container, .gantt_subjects_container>.gantt_hdr, .project-name, .issue-subject, .version-name',
+    minWidth: 100,
+    handles: 'e',
+    containment: '#content',
+    create: function( event, ui ) {
+      $('.ui-resizable-e').css('cursor','ew-resize');
+    }
+  }).on('resize', function (e) {
+      e.stopPropagation();
+  });
+  if(isMobile()) {
+    $('td.gantt_subjects_column').resizable('disable');
+  }else{
+    $('td.gantt_subjects_column').resizable('enable');
+  };
+}
diff -u -r redmine-3.3.2/public/stylesheets/application.css redmine-3.3.2_fix/public/stylesheets/application.css
--- redmine-3.3.2/public/stylesheets/application.css	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/public/stylesheets/application.css	2018-05-02 17:00:50.000000000 +0900
@@ -1082,8 +1082,13 @@
 
 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
 
-.gantt_subjects { font-size: 0.8em; }
-.gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
+.gantt_subjects, .issue-assigned-name { font-size: 0.8em; }
+.gantt_subjects div, .issue-assigned-name { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
+.gantt_subjects div.issue-subject:hover { background-color:#ffffdd; }
+.issue-assigned-name { padding: 0px 3px; width: 100px; }
+.gantt_subjects .issue-subject img.icon-gravatar, .issue-assigned-name img.icon-gravatar {
+  margin: 2px 5px 0px 2px;
+}
 
 .task {
   position: absolute;
diff -u -r redmine-3.3.2/public/stylesheets/context_menu.css redmine-3.3.2_fix/public/stylesheets/context_menu.css
--- redmine-3.3.2/public/stylesheets/context_menu.css	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/public/stylesheets/context_menu.css	2018-05-02 16:41:57.000000000 +0900
@@ -54,3 +54,6 @@
 .context-menu-selection { background-color:#507AAA !important; color:#f8f8f8 !important; }
 .context-menu-selection a, .context-menu-selection a:hover { color:#f8f8f8 !important; }
 .context-menu-selection:hover { background-color:#507AAA !important; color:#f8f8f8  !important; }
+div#gantt_area .context-menu-selection { background-color: rgba(80, 122, 170, 0.48) !important; }
+div#gantt_area .context-menu-selection:hover { background-color: rgba(80, 122, 170, 0.48) !important; }
+div#gantt_area .context-menu-selection a { color: #169 !important; }
diff -u -r redmine-3.3.2/public/stylesheets/responsive.css redmine-3.3.2_fix/public/stylesheets/responsive.css
--- redmine-3.3.2/public/stylesheets/responsive.css	2017-01-07 00:42:18.000000000 +0900
+++ redmine-3.3.2_fix/public/stylesheets/responsive.css	2018-05-02 17:06:16.000000000 +0900
@@ -676,6 +676,33 @@
     padding-top: 1em;
   }
 
+  /* Gantt charts */
+  /*
+   * [1] override inline styles with important
+   * [2] keep border between subjects and gantt area
+   * [3] remove whitespace between subjects and gantt area
+   * [4] maintain width due to [3]
+   */
+  .gantt_subjects_column {
+    width: 50% !important; /* [1] */
+  }
+  
+  .gantt_subjects_container {
+    width: 100% !important;
+    overflow: hidden;
+  }
+  
+  .gantt_subjects_column .gantt_hdr {
+    width: 100% !important;
+    border-right: 1px solid #c0c0c0; /* [2] */
+    right: 0 !important; /* [2] */
+  }
+  
+  #gantt_area {
+    left: -2px; /* [3] */
+    margin-right: -2px; /* [4] */
+  }  
+
   /*----------------------------------------*\
     G) FORMS
   \*----------------------------------------*/
```
## パッチの当て方(Linux)

※ パッチかけられない方は、変更前後のソースも添付していますので、参考にしてください。

１）redmineルートディレクトリに、gantt_for_ver332.patch を保存

```bash:
CONTRIBUTING.md  Rakefile      config      extra                  log       test
Gemfile          app           config.ru  files                   plugins   tmp
Gemfile.lock     appveyor.yml  db         gantt_for_ver332.patch  public
README.rdoc      bin           doc        lib                     script
```
２）$ sudo patch -p1 < gantt_for_ver332.patch を実行
 
```bash:
patching file app/models/issue_query.rb
patching file app/views/gantts/show.html.erb
patching file app/views/queries/_form.html.erb
patching file config/locales/ja.yml
patching file lib/redmine/helpers/gantt.rb
patching file public/javascripts/context_menu.js
patching file public/javascripts/gantt.js
patching file public/stylesheets/application.css
patching file public/stylesheets/context_menu.css
patching file public/stylesheets/responsive.css
```

３）$ sudo rm gantt_for_ver332.patch でパッチを削除

## 修正したところ概略

※ 全体的に編集行のズレを修正しました。その他は、下記になります。

### ①ガントチャートをドラッグでリサイズ
■下記の様に、ガントチャート要素にクラスを追加。(Ver3.4.0を参考に修正)

```erb:/app/views/gantts/show.html.erb
<%= content_tag(:div, :style => style, :class => "gantt_subjects_container") do %>

<table style="width:100%; border:0; border-collapse: collapse;", class="gantt_container">

<td style="width:<%= subject_width %>px; padding:0px;" class="gantt_subjects_column">
```

###②ガントチャートでコンテキストメニューを表示
■Ver3.3.2では、context_menuに引数が必要。（チケット一覧などと同じ表記に）

```erb:/app/views/gantts/show.html.erb
<%= context_menu issues_context_menu_path %>
```

■右クリックした時に、hascontextmenuクラスが見つからずリターンされる。(Ver3.4.0を参考に修正)

```js:public/javascripts/context_menu.js
  var tr = target.parents('tr').first();
  if (!tr.hasClass('hascontextmenu')) {return;}

  ⬇ ⬇ ⬇

  var tr = target.closest('.hascontextmenu').first();
  if (tr.length < 1) {return;}
```

### ③ガントチャートに担当者を表示
■下記ガントcssの記述が元々なかったので、追記。
(Ver3.4.0を参考に修正)

```css:/public/stylesheets/responsive.css
679   /* Gantt charts */
680   /*
681    * [1] override inline styles with important
682    * [2] keep border between subjects and gantt area
683    * [3] remove whitespace between subjects and gantt area
684    * [4] maintain width due to [3]
685    */
686   .gantt_subjects_column {
687     width: 50% !important; /* [1] */
688   }
689 
690   .gantt_subjects_container {
691     width: 100% !important;
692     overflow: hidden;
693   }
694 
695   .gantt_subjects_column .gantt_hdr {
696     width: 100% !important;
697     border-right: 1px solid #c0c0c0; /* [2] */
698     right: 0 !important; /* [2] */
699   }
700 
701   #gantt_area {
702     left: -2px; /* [3] */
703     margin-right: -2px; /* [4] */
704   }
705 
706   /*----------------------------------------*\
707     G) FORMS
708   \*----------------------------------------*/
```

■カスタムクエリの日本語がないので、登録

```yml:/redmine_co/config/locales/ja.yml
label_gantt_assigned_to_names: 担当者
```