view customize pluginで子チケットが終了したら親チケットも終了させる。

view customize pluginで子チケットが終了したら親チケットも終了させる。
                 
最終更新日から90日以上経過しています。

はじめに

view customize pluginに関するTweetを漁っていたら興味深い呟きを見つけたので反応した。

どうやら2年ほど前から要望はあるものの、実装の気配はない状況のみたい。
確かに子チケットが全て終了したら親チケットも終了したい。需要は自分にもある。
view customize pluginはjQueryが利用でき、Redmine APIも呼び出せる = チケットに対する操作が可能。
というわけでやってみた。

完成イメージ

下記のチケット構成を想定。

親チケット
┣子チケット1
┗子チケット2

子チケット1を終了した時に、子チケット2も終了していたら、親チケットも終了する。
(ダイアログで”OK”クリック時のみ)

環境

各バージョン等は下記の通り。

Redmine

3.4.stable

view customize plugin

1.2.2

設定内容

設定は下記の通りです。

Path pattern

チケットの詳細画面を対象にするパターン

/issues/[0-9]+

Type

JavaScript

Code

コードは下記の通り。
Promiseをたくさん使った。
コールバック地獄にはならないように気をつけたけど、もう少しキレイに書けそうな気がする。。。

// RedmineのAPIキー(適宜、値を変更する)
const API_KEY = 'abcde-foo-bar';

/**
 * チケットを更新する
 */	
const putIssue = function(id) {
    return new Promise(function(resolve, reject) {
        $.ajax({
            type: 'PUT',
            url: '/issues/' + id + '.json',
            headers: {
                'X-Redmine-API-Key': API_KEY
            },
            dataType: 'text',
            data: JSON.stringify({
                "issue": {
                    // 「終了」を意味するステータスのID
                    "status_id": "5"
                }
            }),
            contentType: 'application/json',
        }).done(function(resp) {
            resolve(resp['issue']);
        }).fail(function(jqXHR, textStatus, errorThrown){
            console.log(jqXHR);
            console.log(textStatus);
            console.log(errorThrown);
            reject(errorThrown);
        });
    });
}

/**
 * チケットを取得する
 */	
const getIssue = function(id) {
    return new Promise(function(resolve, reject) {
        $.ajax({
            type: 'GET',
            url: '/issues/' + id + '.json?include=children',
            headers: {
            'X-Redmine-API-Key': API_KEY
            },
            contentType: 'application/json',
        }).done(function(resp) {
            resolve(resp['issue']);
        }).fail(function(error) {
            console.log(error);
            reject(error);
        });
    });
};

/**
 * 同じ親チケットに紐づくチケット(兄弟チケット)を取得する
 */
const getSiblingsIssue = function(id) {
    return getIssue(id).then(function(parent) {
        if (parent['children']) {
            return parent['children'];
        } else {
            return Promise.reject();
        }
    })
    .catch(function(error) {
        console.log(error);
    });
};

$(function() {
    // URLの最後のチケット番号を取得
    const URL = location.href;
    const ISSUE_ID = URL.substr(URL.lastIndexOf("/")+1);

    // submitクリック時
    $('#issue-form').submit(function() {
        var issue = null; // 表示中チケットの情報保持用        
        var status = $('#issue_status_id').val();
        // チケットステータスが「終了」の時
        if(status === "5") {
            getIssue(ISSUE_ID).then(function(respIssue) {
                issue = respIssue;
                if (issue['parent']) {
                    return getSiblingsIssue(issue['parent']['id']);
                } else {
                    return Promise.reject();
                }
            })
            .then(function(siblings){
                const promises = [];
                siblings.forEach(function(sibling) {
                    promises.push(getIssue(sibling['id']));
                });
                return Promise.all(promises);
            })
            .then(function(siblingIssues) {
                var finished = true;
                // 取得した兄弟チケット全てが「終了」か判定
                siblingIssues.forEach(function(siblingIssue) {
                    if (siblingIssue['status']['id'] !== 5) {
                        finished = false;
                    }
                });
                if (finished) {
                    if(confirm("親チケットも終了しますか?")) {
                        return putIssue(issue['parent']['id']);
                    } else {
                        return Promise.resolve();
                    }
                }
            })
            .then(function(resp) {
                $('#issue-form').off('submit');
                $('#issue-form').submit();
            })
            .catch(function(error) {
                console.log(error);
            });
            return false;
        }
    });
});

ポイント、というか詰まった所1:statusの形式がGET時とPUT時で異なる。

※GET/PUTで形式が違うのは普通の事なのかな?ちょっと経験不足で不明。。。

GET時のレスポンスは、

"issue" : {
    "id": "5",
    "name": "終了"
}

上記の形式で返却されるのに対し、PUT時は、

"issue": {
    "status_id": "5"
}

この形式でリクエストする。

ポイント、というか詰まった所2:$ajax()のレスポンスが200(成功)でもfailに入ってくる

Redmineのチケット更新APIが成功(ステータスコード:200)で帰ってきているのに、fail()として処理されていた。
これは、$ajax()はレスポンスをjsonにパースする際にエラーとなっていた為。
というのもチケット更新APIの成功時のレスポンスはnullだから。

調べると幾つかブログやstackoverflowがHITし、同様の事象に苦しんだ人が居る事がわかる。
記載された解消方法は、

リクエスト時に、dataType: 'json' を指定しなければ、
レスポンスの型をよしなに判定してくれる。

というものだったが、今回は解消されなかった。
これは恐らくjQueryのバージョンの違いによるものと思われる。
現在のjQueryの最新バージョンは公式サイトによると3.3.1。
対してRedmineに組み込まれているjQueryは1.11.1。(下記キャプチャ、下から2段目)

1.11.1以降のバージョンで、dataTypeが指定されない場合に自動判定されるようになったのではないか、と推測。

で、どのように対応したかというとjsonにパースされたくないので、リクエスト時に

dataType: 'text'

を指定する事でdone()に入ってくるようになった。

おわりに

今回は色々学ぶことが多かった。特に$ajax()のレスポンスの辺り・・・。
コードについてはES6で書くともう少しきれいに書けるのかな?と思う。
同じ要領で関連チケットが全て終了したら・・・もできそう。

 

Redmineカテゴリの最新記事