Redmineにおけるチケットの単位について

(この記事は Redmine Advent Calendar 2018 - Adventar の7日目の記事です。)

Redmineを運用していると「チケットをどういう単位で切るか」というところに悩む人も多いのではと思います。

何が適しているかは、Redmineの使い方や、そのプロジェクトのフローなどによって異なってくるので、これが正解だ!というものは無いのですが、目安の一つとして「同時に複数人が担当者になってしまわないようにチケットを分ける」といった方法があります。

同時に複数人が担当になってしまうようなチケットの切り方だと、

  • そのチケットを誰が担当しているのかが曖昧になり、結果、放置されてしまう
  • 複数の人の作業状況を加味しなければならないので、ステータスや進捗が設定しづらくなる

といったことがおこり、結果的にチケットから状況がわかりずらくなります。

これを避けるためにも、同時に複数人が担当者とならないような形でチケットを切っていくと良いと思います。

例1:一つのチケットを同時に複数人で担当するような場合

例として、クライアントサイドとサーバサイドを別々の担当者で製造する場合を考えて見ます。

この場合、1機能ということでクライアントとサーバまとめて1チケットにしてしまうと、1チケットを同時に複数人が担当することとなり、先ほど述べたような問題がおこりかねません。

こういったときには、クライアントとサーバで別々のチケットとするのが良いでしょう。また、それぞれのチケットが一つの機能であることをわかりやすくするため、親子チケットにするとわかりやすいと思います。

  • 機能
    • 製造: サーバサイド
    • 製造: クライアントサイト

Redmineの親子チケットは、使い方さえ間違えなければ、とても強力な仕組みだと思います。

例2:一つのチケットがステータスによって別々の担当者となる場合

作業とレビューを1つのチケットで行う場合、ステータスによって担当者が切り替わっていくと思いますが、同時に複数人が担当になるわけではないので、こういった場合は1チケットで十分です。

あくまで、同時に複数人が担当者になる(=同時に作業をする)場合のみ、別チケットにすれば十分となります。

ただ、担当者の切り替え忘れには注意が必要です。View customizeを使えば、ステータスに応じて担当者を自動設定することも可能なので、それでカバーするのも良いと思います。(View customizeでの設定方法は、また別の記事で)

終わりに

この考えに基づくようになってから、チケットの構成で悩むことは少なくなりました。チケットの構成に悩む方のヒントになればと思います。

なお、途中でチケットの構成変えたくなったら、そこで変えればよいので、最初にきっちり決めなくても良いと思っています。最初からうまくいくかどうかなんて、いくら時間かけて考えてもわかりません。まずは試してみて、やりづらいと思ったら、変えていくといったことをお勧めします。(自分のチームでも何度も変えていきました)

ShortcutKey2URL(Firefox版)の 4.0.2 をリリースしました

4.0.2 を先ほどリリースしました。Firefoxのテーマによっては、ポップアップの背景色が白以外となってしまう問題の対処となります。

自分の手元では再現できなかったのですが、報告していただいた方に試してもらいながら解決しました。

REST APIを利用して複数の子チケットをまとめて作成する(Redmine View Customize Plugin)

先週の第15回redmine.tokyoで紹介したスクリプトです。

REST APIを使って複数の子チケットをまとめて作成します。

作業を子チケットに分割して運用しているようなところだと、かなりうれしいのではと思います。 例えば、親として機能開発があって、その下に設計、製造、テストみたいな子チケットを作るような場合、これで1クリックで定型的な子チケットが作れるようになります。

設定内容

  • Path pattern: .*
  • Insertion position: Bottom of issue detail
$(function() {

  var projectId = $('#issue_project_id').val();
  var trackerId = $('#issue_tracker_id').val();
  var subject = $('#issue_subject').val();
  var priorityId = $('#issue_priority_id').val();
  var parentIssueId =  ViewCustomize.context.issue.id;

  // 子チケットとして作成する情報
  var issueChildren = [
    {
      'issue': {
        'project_id': projectId,
        'tracker_id': trackerId,
        'subject': subject + ' - 子チケット1',
        'priority_id': priorityId,
        'parent_issue_id': parentIssueId
      }
    },
    {
      'issue': {
        'project_id': projectId,
        'tracker_id': trackerId,
        'subject': subject + ' - 子チケット2',
        'priority_id': priorityId,
        'parent_issue_id': parentIssueId
      }
    },
    {
      'issue': {
        'project_id': projectId,
        'tracker_id': trackerId,
        'subject': subject + ' - 子チケット3',
        'priority_id': priorityId,
        'parent_issue_id': parentIssueId
      }
    }
  ];

  var link = $('<a title="子チケットの一括作成" class="icon icon-add" href="#">子チケットの一括作成</a>');
  $('#issue_tree').before($('<p>').append(link));

  link.on('click', function() {

    if (!confirm('子チケットをまとめて作成します。よろしいですか。')) {
      return;
    }

    // チケット作成処理(非同期)を順次実行し、最後にリロード
    var defer = $.Deferred();
    var promise = defer.promise();

    for (var i = 0; i < issueChildren.length; i++) {
      promise = promise.then(createIssue(issueChildren[i]));
    }

    promise
      .done(function() {
        // 成功したらリロード
        location.reload();
      })
      .fail(function() {
        alert('失敗しました');
      });

    defer.resolve();
  });

  function createIssue(issue) {

    return function() {

      return $.ajax({
        type: 'POST',
        url: '/issues.json',
        headers: {
          'X-Redmine-API-Key': ViewCustomize.context.user.apiKey
        },
        // 作成時はレスポンスのコンテンツが無く、jsonだとエラーとなるのでtextにしておく
        dataType: 'text',
        contentType: 'application/json',
        data: JSON.stringify(issue)
      });

    };
  }
})

画面イメージ

f:id:onozaty:20181117232408g:plain

第15回 redmine.tokyo勉強会で発表してきました

第15回 redmine.tokyo にて、View customize のバージョン1.2.0について発表してきました。

資料はこちら。

www.slideshare.net

発表中に、View cutomizeを知っている人に手を上げてもらったのですが、7、8割くらいの人が手をあげてくれました。うれしくて、ちょっと涙がでました。

他の方の発表も、いろいろ盛りだくさんで面白かったです。また、Twitterなどでやり取りしていた方と会うことが出来たのも良かったです。(ご挨拶しようと思っていて、タイミング逃してしまった方もいるので、また次回に、、)

声をかけていただいて、こうやって参加して発表できて、とても良い1日になりました。スタッフの皆様、ありがとうございました。

大きなリストを検索するときにArrayListではなくHashSetを使う (Java)

社内でも同じこと書いたのですが、こっちでも。

大きなリスト(例えば10万件を超えるようなもの)から一致するものを探す場合、ArrayList#containsを使うと、パフォーマンス的に問題になる場合があります。ArrayList#containsは、先頭からシーケンシャルに見ていくためです。

で、大きなリストを検索する場合には、検索に向いたデータ形式、例えばHashSetに格納することによって、パフォーマンスが大きく改善する場合があります。HashSetはハッシュテーブルを使って値を格納するので、シーケンシャルに見ていくより高速です。

ということで、試したコードです。 10万件の2つのリストを使って、「リスト1からリスト2に含まれるもののみに絞り込む」といったことをやっています。

package com.example;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.Test;

public class LargeListFilterTest {

    private static final int SIZE = 100000;

    private static final List<String> SOURCE;
    private static final List<String> CANDIDATE;

    static {

        SOURCE = IntStream.rangeClosed(1, SIZE)
                .mapToObj(String::valueOf)
                .collect(Collectors.toList());

        CANDIDATE = IntStream.rangeClosed(1, SIZE)
                .map(x -> x * 2) // 偶数のみに
                .mapToObj(String::valueOf)
                .collect(Collectors.toList());
    }

    @Test
    public void filterWithSetのテスト() {

        List<String> filterd = filterWithSet(SOURCE, CANDIDATE);

        assertThat(filterd)
                .hasSize(SIZE / 2);
    }

    @Test
    public void filterWithListのテスト() {

        List<String> filterd = filterWithList(SOURCE, CANDIDATE);

        assertThat(filterd)
                .hasSize(SIZE / 2);
    }

    private <T> List<T> filterWithSet(List<T> source, List<T> candidate) {

        Set<T> candidateSet = new HashSet<>(candidate);

        return source.stream()
                .filter(candidateSet::contains)
                .collect(Collectors.toList());
    }

    private <T> List<T> filterWithList(List<T> source, List<T> candidate) {

        return source.stream()
                .filter(candidate::contains)
                .collect(Collectors.toList());
    }
}

パフォーマンスの差はあきらかで、下記のような結果になりました。(あくまで自分のPC上でのパフォーマンスですが、差が大きいのは明確です)

  • ArrayListの場合: 26.046秒
  • HashSetの場合: 0.084秒

f:id:onozaty:20181103211322p:plain

なお、小さいリストだったら、いちいちHashSetに変える必要はないです。逆にHashSetにすることによって遅くなる場合もあります。 (小さければシーケンシャルに見たほうが早い場合も多いです。そもそも小さいと気にするレベルの差にもならないですが…)

このようなボトルネックは、プロファイリングかければすぐに見つかるので、パフォーマンスを厳しく求められたり、思った通りのパフォーマンスが出ていないときは、まずはプロファイリングにかけてみると良いです。 JavaだとFlight Recorderが簡単に使えて便利なので、とても助けられています。(それでこういった問題に気がついたことも多いです)

Redmine: View customize plugin の v1.2.0 をリリースしました

View customize plugin の v1.2.0 をリリースしました。2年ぶりの新バージョンになります。

リリース内容

今回のリリースには、2つの新規機能と細々とした変更がいくつか入っています。

  1. コードの挿入位置が選択できるように
  2. ユーザやプロジェクトの情報にJavaScriptでアクセス可能に
  3. ローカライズ対応 (日本語リソースの追加)
  4. 一覧をソート可能に
  5. コメント欄を追加し、一覧表示の際にコメントがあればそちらを表示するように (コメントが無かった時にコードを)
  6. View customizeのアイコンが表示されていなかった問題対処

以降はリリース内容の詳細です。

1. コードの挿入位置が選択できるように

今までView customizeでは、ヘッダ部分にコードを挿入していました。 それが1.2.0からは、下記の3パターンから選べるようになります。

  • Head of all pages (全てのページのヘッダ) ※v1.2.0より前と同じ
  • Bottom of issue form (チケット入力欄の下)
  • Bottom of issue detail (チケット詳細の下)

「Bottom of issue form」は、今まで嵌る人が多かった「チケット入力欄がトラッカーやステータスを変えた際に再構築されてしまい、View customizeでの変更内容がクリアされてしまう」といった問題に対応するためのものです。これを使うと、入力フォームが再構築される際にもコードが再度埋め込まれて実行されるので、今までのような考慮が不要になります。

なお、埋め込み箇所が存在しないページでは埋め込まれないことになるので、Path patternとの組み合わせにご注意ください。 たとえば、「Bottom of issue detail」はチケット詳細でしか埋め込まれないので、Path patternとして.*で全ページを指定していてもチケット詳細画面でのみ実行されるようになります。(Path patternで正規表現を細かく考えなくて済むので、それはそれで便利かと)

2. ユーザやプロジェクトの情報にJavaScriptでアクセス可能に

View customizeでは、画面にある情報以外にアクセスするのは大変でした。まったく参照するのが不可な情報も多いですし、出来たとしてもREST API叩いたり、スクレイピングしたり、、と面倒でした。

私自身も、ロールやグループの情報を使って適用する人を区別したいといったことがあり、それら情報にアクセスする手段として、ViewCustomize.contextといったオブジェクトを用意しました。

ユーザに紐づく情報として、ID、氏名以外にも、APIキー、グループ、ロール、カスタムフィールドといった情報に参照することができます。プロジェクトの情報としても識別子やロール、またチケットの情報としてIDを参照できるようにしました。 プロジェクトの識別子やチケットのIDは、今までも画面に存在する情報から取得可能でしたが、利用頻度が多いため、より直感的に書けるように、、ということで加えました。

オブジェクト全体のイメージは下記の通りです。

ViewCustomize = {
  "context": {
    "user": {
      "id": 1,
      "login": "admin",
      "admin": true,
      "firstname": "Redmine",
      "lastname": "Admin",
      "groups": [
        {"id": 5, "name": "Group1"}
      ],
      "apiKey": "3dd35b5ad8456d90d21ef882f7aea651d367a9d8",
      "customFields": [
        {"id": 1, "name": "[Custom field] Text", "value": "text"},
        {"id": 2, "name": "[Custom field] List", "value": ["B", "A"]},
        {"id": 3, "name": "[Custom field] Boolean", "value": "1"}
      ]
    },
    "project": {
      "identifier": "project-a",
      "name": "Project A",
      "roles": [
        {"id": 6, "name": "RoleX"}
      ]
    },
    "issue": {
      "id": 1
    }
  }
}

追記@2018-10-20 user.roles は、Redmineのバージョンによってはエラーとなってしまっていたため、1.2.2にて削除しています。上記にも反映しました。

例えばユーザのAPIキーにアクセスするにはViewCustomize.context.user.apiKeyとなります。

3. ローカライズ対応 (日本語リソースの追加)

今回日本語リソースを追加し、ロケールとして日本語を選択しているユーザに対しては、日本語で表示されるようになりました。ずっと英語だったので、今まで使っていた方々には違和感を感じる人も多いかもしれませんが、きっと慣れてもらえると思います。(私も最初は違和感ありましたが、やっと慣れました)

母国語じゃない、、ってだけで、敷居をあげてしまうということは実際あるので、いつかやらなきゃな、、と思っていたのですが、今回Pull requestをもらったのもあって、入れることにしました。今後は他の言語も入れて行きたいと思っています。

その他

一覧系に関することで、下記の2点の修正を行っています。

  • 一覧をソート可能に
  • コメント欄を追加し、一覧表示の際にコメントがあればそちらを表示するように (コメントが無かった時にコードを)

コメント欄ですが、View customizeの設定が多くなってくると、内容を判断するための情報がコードの先頭部分だけだと厳しくなってきたため、コメント欄として別途設けることにしました。コメントが入力されていなかった場合には、今までどおりコードの先頭部分が表示されます。

f:id:onozaty:20181001000424p:plain

他は細かいBugfixなので、詳細は省略します。

最後に

今回の新しい機能の使用例を、次回のredmine.tokyoでお話させていただく予定です。

あと、もうすぐリリースされそうなRedime4に対する対応は、Redmine4がリリースされたらすぐに出す予定です。(対応方法は既に確認済みですので、大丈夫かと、、)

Redmineの検索欄に素早く移動する (ShortcutKey2URL)

人から聞いたチケット番号を元にチケットを開いたり、何かキーワードで検索したり、ってのが良くあるのですが、その際にすぐにRedmineの検索欄に飛べるように、ShortcutKey2URLという拡張機能を使っています。

ShortcutKey2URL

ShortcutKey2URLは、ショートカットキーを使用してURLを開いたり、移動したり、JavaScriptを実行できる拡張機能です。Chrome版とFirefox版があります。

スタートアップキーであらかじめ設定しておいた動作の一覧を表示し、次のキーでその動作を実行するような動きとなっています。

キーとして使用できる文字は1文字に限定されません。複数文字として設定しておくことが可能です。ShrotcutKey2URLは、キーとして連続して入力された文字から、対象が1つに絞り込まれた時点でその動作を実行します。

動作として設定できるものには、下記のようなものがあります。

  • URLへの移動。既に開いているURLの場合は、そのタブへ移動し、開かれていない場合には、新しいタブとして開く。
  • 新規タブとしてURLを開く。
  • 現在のタブにURLを開く。
  • 現在のタブで指定したJavaScriptを実行する。
  • 新規タブとしてURLを開いて、その後に指定したJavaScriptを実行する。

Redmineの検索欄に素早く移動する

ということで、Redmineの検索欄に素早く移動できるようにショートカットキーを設定します。

新しいタブでRedmineのページを開いて、その後にJavaScriptで検索欄にフォーカスを当てます。

document.getElementById('q').focus();

こんな感じの設定になります。(http://192.168.33.11/ がRedmineのURL)

f:id:onozaty:20180905005706p:plain

動作イメージは以下のような感じです。

f:id:onozaty:20180905010958g:plain

ShortcutKey2URLは他にもいろいろ使える場面があると思うので、ぜひご利用ください!!