はじめに
view customize pluginに関するTweetを漁っていたら興味深い呟きを見つけたので反応した。
これは今ハマっているview customize pluginで実現できそうな予感。
子チケットをクローズする。
↓
親チケットを参照し、それに紐づく子チケットを全て取得する。
↓
取得した子チケットが全てクローズしてたら親チケットをクローズする。
で良いのかな? https://t.co/h5E4FBKIhm— BEKO (@nkwtnb) January 16, 2019
どうやら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で書くともう少しきれいに書けるのかな?と思う。
同じ要領で関連チケットが全て終了したら・・・もできそう。