RedmineのJavaScriptからbulk_updateを使用する

Redmineの機能を拡張するためには自分でRuby on Railsのプラグインを作成する方法があるが、プラグインがRedmine（およびRedmineが依存するRuby）のバージョンアップに追従しきれずに埋葬されていく様は度々目撃している。
（嗚呼、redmine_backlogs……本当にいいプラグインだった。ちょっと重かったけど）

私のスタンスとしてはアプリケーションのアップデートに強い制約を受けるプラグインはできる限り作成せずに、View CustomizeプラグインにJavaScriptを組み込むことで、フロントエンド限定ながらRedmineやRubyのバージョンに依存しない機能拡張を行うようにしている。

勿論、フロントエンドのJavaScriptなので基本的にはRedmineのDBを参照するようなことはできない。  
しかし下記の方法ならJavaScriptでもデータの参照やチケットの更新を行うことができる。

* bulk_update
* REST API

この記事では **bulk_update** について解説する。
また動作確認は **Redmine 2.6** 系で行った。

REST APIを利用する記事はこちら。
[RedmineのJavaScriptからREST APIを使用する](https://qiita.com/forenoonM/items/6121399cb9f78b8da9ca)

## View Customizeプラグインとは
[[Github] onozaty/redmine-view-customize](https://github.com/onozaty/redmine-view-customize)
[[SlideShare] View customize pluginを使いこなす](https://www.slideshare.net/onozaty/view-customizeplugin-62005780?ref=http://blog.enjoyxstudy.com/entry/2016/05/18/000000)

@onozaty 氏が作成するRedmineプラグインで、正規表現で指定する任意のページに任意のCSSとJavaScriptを埋め込むプラグイン。
CSSとJavaScriptでHTMLのDOM構造を改変することで、主にUI/UXのかなりの機能拡張を行うことができる。


# bulk_updateについて
チケット一覧の右クリックコンテキストメニューから実行されるチケット更新スクリプト。
GETパラメータで更新するチケットと更新するパラメータを指定しているためJavaScriptからも利用できる。

![チケット一覧でコンテキストメニューHTML](https://qiita-image-store.s3.amazonaws.com/0/85304/fce99c9f-7590-ce0f-c44e-b8728da90735.png)


ログイン中のユーザー情報で更新をかけるためログインしていないと使用できない。
しかし`apikey`や`authenticity_token`が不要であるためJavaScriptからのチケット更新が手軽に行える。


## コンテキストメニューから見るbulk_update
チケット一覧のコンテキストメニューでbulk_updateがどう動いているか調べた。
コンテキストメニュー(`#context-menu`)からbulk_updateまでのDOM構造はおよそ以下のようになっている。

* `#context-menu > ul > li.folder`　…　更新項目のフォルダ
    * `#context-menu > ul > li.folder > a.submenu`　…　テキストが項目名
    * `#context-menu > ul > li.folder > ul > li`　…　更新パラメータ
        * `#context-menu > ul > li.folder > ul > li > a`　…　リンク先がGETパラメータ付きのbulk_update


# bulk_updateの使い方
コンテキストメニューに組み込まれているURLパラメータを解析すると、bulk_updateに指定するパラメータは以下のようになっている。

```
/issues/bulk_update?back_url=%2Fredmine%2Fprojects%2Ftest-2015-11-30%2Fissues%3Fsort%3Dparent%253Adesc%252Cid%253Adesc&ids%5B%5D=7&issue%5Bstatus_id%5D=3

　↓ decodeURI()

/issues/bulk_update?back_url=/redmine/projects/test-2015-11-30/issues?sort=parent%3Adesc%2Cid%3Adesc&ids[]=7&issue[status_id]=3
```

* /issues/bulk_update
    * `back_url`: 更新後に戻ってくるURL
    * `ids[]`: 更新するチケットID。ex) idx[]=7
        * 複数更新する場合は更新するチケット数だけこのids[]を並べる。
    * `issue[{パラメータ名}]`: 更新するパラメータ名と値。  ex) issue[status_id]=3

コンテキストメニューからチケットのステータス変更などを行う際には、このURLにPOST送信している。  
更新するパラメータ名とその値さえわかれば利用可能。
例えば入力可能なパラメータはチケット詳細のHTMLが参考になる。

```html
<select id="issue_priority_id" name="issue[priority_id]">
    <option value="1">低め</option>
    <option value="2" selected="selected">通常</option>
    <option value="3">高め</option>
    <option value="4">急いで</option>
    <option value="5">今すぐ</option></select>
```

* {redmine_root}/issues/bulk_update
    * `ids[]=7`　…　チケット#7
    * `issue[priority_id]=4`　…　優先度"急いで"
    * ※issue[{パラメータ名}]を複数設定することで複数パラメータの一括更新も可能。

```js
$.post(
    "/issues/bulk_update",
    {ids:[7], issue:{priority_id:4}}
)
.done(function(data){
    // success;
})
.fail(function(data){
   // fail
});
```

* `done`イベント
    * 更新が正常終了した　…　`data.status: 200`
    * 存在しないパラメータを指定した　…　`data.status: 200`
        * ※つまりパラメータエラーはエラーとして返ってこない
* `fail`イベント
    * ログインしていない　…　`data.status: 401`
    * 存在しないチケットを指定した　…　`data.status: 404`
    * 権限が無いチケットを指定した　…　`data.status: 422`


# bulk_updateを直接使う上での課題
例えばパラメータ名（優先度`priority_id`やステータス`status_id`など）はRedmine側で定義されている。

しかし、それらに設定可能な値はシステム管理者やプロジェクト管理者が追加／更新できるため、予め値ラベル（"未着手"など）とその数値（"3"など）を調べておくか、何かしらの方法で最新の一覧を取得する必要がある。


## HTMLソースを見て設定可能な一覧をメモしておく
View Customizeプラグインなどで特定の部署やプロジェクト用のスクリプトを組む場合は、決め打ちした値をスクリプトに組み込んでもそれほど問題はない。

設定可能な値の一覧はチケット詳細の「編集」時フォームから読み取れる。

`<input>`のname属性がパラメータ名、`<option>`のvalue属性が数値で、`<option>`のタグ中テキストが値ラベルになっている。

```html
<!-- ステータス -->
<!-- #attributes > div > div > p > #issue_status_id -->
<p>
    <label for="issue_status_id">ステータス<span class="required"> *</span></label>
    <select id="issue_status_id" name="issue[status_id]" onchange="updateIssueFrom('/projects/test/issues/update_form.js?id=11059')">
        <option value="8">Wait</option>
        <option value="1">ToDo</option>
        <option value="2" selected="selected">Doing</option>
        <option value="7">Checking1</option>
        <option value="9">Checking2</option>
        <option value="10">Checking3</option>
        <option value="5">Done</option>
    </select>
</p>
```
```html
<!-- カスタムフィールド -->
<!-- #attributes > div > div > p > #issue_status_id -->
<p>
    <label for="issue_custom_field_values_1"><span title="演出別に振り分けるID。　">演出ID</span></label>
    <input class="string_cf" id="issue_custom_field_values_1" name="issue[custom_field_values][1]" type="text" value="">
</p>
```

## チケット詳細の入力フォームのHTMLから設定可能な値の一覧を取得する
* **※下記の挙動は実際にGET/POSTメソッドやパラメータ変更などを行って挙動を調べたもの。該当するRedmineのソースコードまではちゃんと調べきっていないため自己責任で。**
* **※Redmine自体の細かい挙動を利用しているため、今後のバージョンアップで使えなくなる or 改修する必要が出てくる可能性がある。**
* **※チケット一覧やチケット詳細など`authenticity_token`が必要なので、`authenticity_token`が含まれるページでのみ実行可能。**

チケット詳細ページの「編集」フォームでは、トラッカーやステータスなど一部のプロパティを変更しようとした際にフォームの入力内容をチェックし、選択可能な値が変わったり異常な値がセットされた場合はフォームのコンボボックスなどの更新が行われる。

この処理は`update_form.js`が行っていて、フォームの入力内容チェックののちにフォームのHTMLが上書きされる。
この`update_form.js`を利用して設定可能な値の一覧を取得する。

```html
<!-- update_fom.js を使用している箇所の例 -->
<select id="issue_tracker_id" name="issue[tracker_id]" onchange="updateIssueFrom('/projects/test/issues/update_form.js?id=7')">
    <option value="2">機能</option>
    <option value="3" selected="selected">タスク</option>
</select>
```

まず`authenticity_token`をjQueryのセレクタで取得する。

```js
$(':input[name="authenticity_token"]:first').val()
```

POSTメソッドに最低限のパラメータをつけて実行する。

```js
$.post(
    '/projects/test/issues/update_form.js?id=7',
    {utf8:"✓", authenticity_token:$(':input[name="authenticity_token"]:first').val()}
).done(function(data){
    // レスポンス（data）にはJavaScriptコードが入るが、これにはフォームHTML置き換え処理のコードが含まれる。
    // この先頭行を書き換えてeval()で実行する。
    // 具体的にはreplaceIssueFormWith()というメソッドを実行しようとしているため、文字列置き換えでhtmlIssueFormへの代入に変更する。
    eval(data.split("\n")[0].replace(/^replaceIssueFormWith\(/, 'var htmlIssueForm=').replace(/\);$/, ';'));

    // デバッグ用に内容を表示。
    console.log($(":input", $(htmlIssueForm)));
});
```

このコードでは、doneのレスポンス（data）を加工して`htmlIssueForm`にフォームHTMLが入るようにしている。
htmlIssueFormをjQuery()のcontext引数に渡すことでフォーム中の各入力要素とそのパラメータを取得できる。