Redmine: チェックボックスのカスタムフィールドを横並びで表示する (View customize plugin)

Redmine Users (japanese) - Google グループで、チェックボックスを横並びにしたいといった要望が出ていたので、View customize でやってみます。

なお、段組みを行う方法は以前書きましたが、段組みだと、コンテンツは上から下に流れるため、並び順としては横に並ばない形になります。

横に並べる方法としては、CSS3でflexboxという仕組みがあるので、これを使います。

flexboxを使うと、コンテンツを横並びに隙間なく埋めることが出来ます。(昔はfloatで頑張らなければならなかったので、便利になりましたね!)

設定内容

Path pattern

チケット編集系の画面を対象とします。

/issues

Code

Type:StyleSheetとして下記を設定します。

span.enumeration_cf.check_box_group {
  display: flex;
  display: -webkit-flex;
  flex-wrap: wrap; 
  -webkit-flex-wrap: wrap; 
}

項目のサイズを統一したい場合には、下記も追記します。(下記の例だと幅を60pxに)

span.enumeration_cf.check_box_group label {
  width: 60px;
}

結果

下記のように横並びで表示されるようになります。

f:id:onozaty:20161023004832p:plain

サイズを統一した場合は、下記のようになります。

f:id:onozaty:20161023005014p:plain

Redmine: チケット一覧のコンテキストメニューに、サーバにリクエストを送る項目を追加する (View customize plugin)

チケット一覧のコンテキストメニューに項目を追加して、その項目をクリックしたらサーバ側に選択しているチケットの番号を送って、サーバ側で処理させることができないかという質問をいただいたので、View customize pluginで試行錯誤してみました。

チケット一覧でのコンテキストメニュー生成の仕組み

チケット一覧のコンテキストメニューは、チケット一覧上での右クリックのタイミングで、選択されている行の情報などをサーバ側に送り、コンテキストメニューの情報(HTMLの断片)を受け取って、それをdiv#context-menuに設定しています。

具体的には、context_menu.js内の、contextMenuShow関数で行っています。$.ajaxで通信し、そのコールバックでコンテキストメニューを作ってしまっているので、Redmine側のコード上だとフックするポイントがありません。。

function contextMenuShow(event) {
  var mouse_x = event.pageX;
  var mouse_y = event.pageY;  
  var mouse_y_c = event.clientY;  
  var render_x = mouse_x;
  var render_y = mouse_y;
  var dims;
  var menu_width;
  var menu_height;
  var window_width;
  var window_height;
  var max_width;
  var max_height;

  $('#context-menu').css('left', (render_x + 'px'));
  $('#context-menu').css('top', (render_y + 'px'));
  $('#context-menu').html('');

  $.ajax({
    url: contextMenuUrl,
    data: $(event.target).parents('form').first().serialize(),
    success: function(data, textStatus, jqXHR) {
      $('#context-menu').html(data);
      menu_width = $('#context-menu').width();
      menu_height = $('#context-menu').height();
      max_width = mouse_x + 2*menu_width;
      max_height = mouse_y_c + menu_height;

      var ws = window_size();
      window_width = ws.width;
      window_height = ws.height;

      /* display the menu above and/or to the left of the click if needed */
      if (max_width > window_width) {
       render_x -= menu_width;
       $('#context-menu').addClass('reverse-x');
      } else {
       $('#context-menu').removeClass('reverse-x');
      }

      if (max_height > window_height) {
       render_y -= menu_height;
       $('#context-menu').addClass('reverse-y');
        // adding class for submenu
        if (mouse_y_c < 325) {
          $('#context-menu .folder').addClass('down');
        }
      } else {
        // adding class for submenu
        if (window_height - mouse_y_c < 345) {
          $('#context-menu .folder').addClass('up');
        } 
        $('#context-menu').removeClass('reverse-y');
      }

      if (render_x <= 0) render_x = 1;
      if (render_y <= 0) render_y = 1;
      $('#context-menu').css('left', (render_x + 'px'));
      $('#context-menu').css('top', (render_y + 'px'));
      $('#context-menu').show();

      //if (window.parseStylesheets) { window.parseStylesheets(); } // IE
    }
  });
}

jQueryの関数でフックする

しょうがないので、今回は、jQueryのshow関数でフックするような対応を取りました。

#context-menuに対してのshowの場合に、メニューを追加する処理を行うイメージです。

View customize の設定内容

Path pattern

チケット一覧を対象とします。

/issues$

Code

Type:JavaScriptとして下記を設定します。

送り先のURLなどは適宜変更してください。

$(function() {

  // 対象のURL(同一ドメイン)
  var commandUrl = '/test';
  var commandTitle = 'コマンド実行';

  // コンテキストメニューを表示したタイミングでフックするために
  // jQueryのshow関数を差し替え
  jQuery.fn._show = jQuery.fn.show;

  jQuery.fn.show = function() {
    if (this.attr('id') == 'context-menu') {
      // コンテキストメニューを表示したタイミングでコマンドを追加
      var menu = $('<a>').text(commandTitle).click(function() {
        execute();
        return false;
      })

      this.children('ul').append($('<li>').append(menu));
    }

    return jQuery.fn._show.apply(this, arguments);
  };

  // コマンドの実処理
  var execute = function() {

    // チェックされているチケット番号を取得(複数存在する場合はカンマ区切り)
    var issues = $('input[name="ids[]"]:checked').map(function() { return $(this).val();}).get().join(',');

    // 対象のURIにチケット番号をパラメータとして送信
    $.ajax({
      type: 'GET',
      url: commandUrl,
      data: 'issues=' + issues // チケット番号をパラメータとして
    }).done(function(data, textStatus, jqXHR){

      // 成功時の処理
      alert('成功しました。');

    }).fail(function(jqXHR, textStatus, errorThrown){

      // 失敗時の処理
      alert('失敗しました。 status:' + jqXHR.status);
    });

    contextMenuHide();
  };
});

設定後のイメージ

f:id:onozaty:20161018005342p:plain

コンテキストメニューに項目が追加され、これをクリックすると、対象のURLに対してチケット番号を送るようになります。

jQueryの関数をフックする

jQueryオブジェクトの各関数ですが、jQuery.fnに定義されています。

なので、jQueryオブジェクトの各関数をフックしたい場合には、jQuery.fnに定義されているものを差し替えます。

たとえば、showメソッドをフックして、他の処理を入れたい場合には下記のように書きます。

// 元の関数を退避
jQuery.fn._show = jQuery.fn.show; 

jQuery.fn.show = function() {
  // 追加したい処理をここに記載

  // 元の関数を呼び出し
  return jQuery.fn._show.apply(this, arguments);
};

idがcontext-menuの要素に対してshowメソッドが呼ばれた場合などは、下記のような感じで。

jQuery.fn._show = jQuery.fn.show; 

jQuery.fn.show = function() {

  if (this.attr('id') == 'context-menu') {
    // context-menuの場合には、処理を呼び出し
    appendMenu();
  }

  return jQuery.fn._show.apply(this, arguments);
};

Java7、8で入った機能で知っておいた方がよさそうなもの

Java8でコードを書くことが増えたので、Java7と8の新機能で知っておいた方がよさそうなことを、いまさらながら書き出してみます。

重要度は、自分自身が使う頻度が多かったもので判断しているので、あまりあてにならないかもしれません…
たとえば、Date and Time APIは、日付操作するようなコード書くことになった場合には、使うことになるのではと思っています。(CalendarとDateで頑張るのは苦しいので)

  • 高 (コードを書くときに頻繁に使用することになりそう)
    • try-with-resources [Java7]
      リソースの有効範囲をブロックで指定して、解放漏れを防ぐ。C#でいうusing。
    • More New I/O APIs for Java (NIO.2) [Java7]
      Path/PathsとFilesは覚えておくべき。ファイル、ディレクトリ操作が改善されている。
    • lambda [Java8]
      関数をそのままオブジェクトとして渡せるように書ける。無名クラスのインスタンスを作るような書き方をしなくて済む。
    • Stream API [Java8]
      リストなどに対して、関数型スタイルの操作を行えるように。C#でいうLinqに近い。
  • 中 (知っとくと便利な時も)
    • StandardCharsets [Java7]
      UTF-8を指定する際に、"UTF-8"と文字列を指定するのではなく、定数のStandardCharsets.UTF_8 が利用できる
    • Catching Multiple Exception [Java7]
      catchで複数の例外を指定してまとめてcatchできる。
  • 低 (そういったのがあること自体は頭の中に入れておく)
    • Optional [Java8]
      値がないかもしれないことを表現するクラスとしてOptionalが追加。
    • Date and Time API [Java8]
      日付、時間のAPIが大幅に改善/変更。
    • switchでのString対応 [Java7]
      switch文の指揮にStringが使えるようになった。
  • その他 (参考程度)
    • ダイアモンド演算子 [Java7]
      ジェネリクスを使ったクラスをインスタンス化する際に、型引数を省略可能に

JavaのparallelStream

JavaのparallelStreamについて思いつくままに。

Java8から入ったStream APIで、parallelStreamを使うと簡単に並列処理が書けますが、streamparallelStreamを使い分けるという感覚じゃなくて、並列処理を書きたい場合にparallelStreamを使うといった形で使っています。

Java8になる前は、並列処理はExecutorService周り(ForkJoinPoolとか)を使って書いていましたが、それなりにコードを書く必要があったのと、書いたコードがシンプルじゃなくなる(やりたいことに対して冗長な)感覚がありましたが、parallelStreamだと単にリストに対する操作のプロパティとして切り替えるような感じになるので、とてもすっきり書けるようになりました。

なお、並列処理を書く場合に気にしているのは、

  • オブジェクトの共有は避ける
  • オブジェクトの生成コストが高いようなものは、オブジェクトのプーリングによって生成を抑える(共有ではなく、その時点での所有者は1タスクとなるように)
  • 並列処理にすることによるコストがあるので、並列化する単位を意識する(量が多い、または1つの処理が時間がかかるなど)

といったところです。

Apache Sparkは、並列処理をサーバ跨いで分散できるようにしていますが、分散は1サーバ内での並列処理以上にオーバヘッドがあるので、ちょっとしたものならば、1サーバ上でCPUを使い切るような形で並列処理させてあげるというのも、一つの手だと思っています。 それ以上あげたければ、Spark使うといった感じで。Sparkでもリスト処理を分散するイメージなので、どちらも延長上で考えやすいのではと思っています。

社内勉強会で「30歳過ぎてもエンジニアでいるためにやったこと」というタイトルで発表しました

30歳でアウトプットを意識するようになって、エンジニアとして大きく変われたと思っているので、それについて社内勉強会で発表してきました。

www.slideshare.net

10年間これがしっかりできたかというと、かなりサボった時期もあったので、どうにか、、といった感じでしょうか。

だんだんアウトプットが減ってきているので、今日から心機一転頑張ろうと思っています。

同じ条件のStreamを繰り返し作るためのビルダーを書いてみた

同じ条件のStreamを繰り返し作るためのビルダーを書いてみました。まったく使いどころが見いだせませんが、、とりあえず書いたので晒しておきます。

Streamの中間処理の流れを定義しておいて、、

Streamer<String, Integer> streamer =
        Streamer.create(String.class)
                .map(x -> {
                    try {
                        return Integer.parseInt(x);
                    } catch (NumberFormatException e) {
                        return null;
                    }
                })
                .filter(x -> x != null)
                .sorted();

Streamの元となるデータを指定することで、Streamを作ります。同じ条件で繰り返しStreamを作れます。

streamer.build(Arrays.asList("a", "2", "1", "", "000"))
        .collect(Collectors.toList()); // -> [0, 1, 2]

streamer.build(Arrays.asList("1", "2", "-1"))
        .collect(Collectors.toList()); // -> [-1, 1, 2]

streamer.build(Arrays.asList("a"))
        .collect(Collectors.toList()); // -> []

streamer.build(Arrays.asList("100", "abc"))
        .collect(Collectors.toList()); // -> [100]

書き始めたときは、何か目的があったはずなのですが、今となっては何のためだったかわからなくなりました...