View customize plugin の v2.0.0 をリリースしました。Redmine 4.0(Rails5)への対応版となります。
v2.0.0からインストール時にbundle install
が必要となります。注意ください。
今回のバージョンから対応するRedmineが3.1.x以上となります。3.0.x以下の方は、v1.2.2をご利用ください。
View customize plugin の v2.0.0 をリリースしました。Redmine 4.0(Rails5)への対応版となります。
v2.0.0からインストール時にbundle install
が必要となります。注意ください。
今回のバージョンから対応するRedmineが3.1.x以上となります。3.0.x以下の方は、v1.2.2をご利用ください。
(この記事は Redmine Advent Calendar 2018 - Adventar の7日目の記事です。)
Redmineを運用していると「チケットをどういう単位で切るか」というところに悩む人も多いのではと思います。
何が適しているかは、Redmineの使い方や、そのプロジェクトのフローなどによって異なってくるので、これが正解だ!というものは無いのですが、目安の一つとして「同時に複数人が担当者になってしまわないようにチケットを分ける」といった方法があります。
同時に複数人が担当になってしまうようなチケットの切り方だと、
といったことがおこり、結果的にチケットから状況がわかりずらくなります。
これを避けるためにも、同時に複数人が担当者とならないような形でチケットを切っていくと良いと思います。
例として、クライアントサイドとサーバサイドを別々の担当者で製造する場合を考えて見ます。
この場合、1機能ということでクライアントとサーバまとめて1チケットにしてしまうと、1チケットを同時に複数人が担当することとなり、先ほど述べたような問題がおこりかねません。
こういったときには、クライアントとサーバで別々のチケットとするのが良いでしょう。また、それぞれのチケットが一つの機能であることをわかりやすくするため、親子チケットにするとわかりやすいと思います。
Redmineの親子チケットは、使い方さえ間違えなければ、とても強力な仕組みだと思います。
作業とレビューを1つのチケットで行う場合、ステータスによって担当者が切り替わっていくと思いますが、同時に複数人が担当になるわけではないので、こういった場合は1チケットで十分です。
あくまで、同時に複数人が担当者になる(=同時に作業をする)場合のみ、別チケットにすれば十分となります。
ただ、担当者の切り替え忘れには注意が必要です。View customizeを使えば、ステータスに応じて担当者を自動設定することも可能なので、それでカバーするのも良いと思います。(View customizeでの設定方法は、また別の記事で)
この考えに基づくようになってから、チケットの構成で悩むことは少なくなりました。チケットの構成に悩む方のヒントになればと思います。
なお、途中でチケットの構成変えたくなったら、そこで変えればよいので、最初にきっちり決めなくても良いと思っています。最初からうまくいくかどうかなんて、いくら時間かけて考えてもわかりません。まずは試してみて、やりづらいと思ったら、変えていくといったことをお勧めします。(自分のチームでも何度も変えていきました)
4.0.2 を先ほどリリースしました。Firefoxのテーマによっては、ポップアップの背景色が白以外となってしまう問題の対処となります。
自分の手元では再現できなかったのですが、報告していただいた方に試してもらいながら解決しました。
先週の第15回redmine.tokyoで紹介したスクリプトです。
REST APIを使って複数の子チケットをまとめて作成します。
作業を子チケットに分割して運用しているようなところだと、かなりうれしいのではと思います。 例えば、親として機能開発があって、その下に設計、製造、テストみたいな子チケットを作るような場合、これで1クリックで定型的な子チケットが作れるようになります。
.*
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) }); }; } })
第15回 redmine.tokyo にて、View customize のバージョン1.2.0について発表してきました。
資料はこちら。
www.slideshare.net
発表中に、View cutomizeを知っている人に手を上げてもらったのですが、7、8割くらいの人が手をあげてくれました。うれしくて、ちょっと涙がでました。
他の方の発表も、いろいろ盛りだくさんで面白かったです。また、Twitterなどでやり取りしていた方と会うことが出来たのも良かったです。(ご挨拶しようと思っていて、タイミング逃してしまった方もいるので、また次回に、、)
声をかけていただいて、こうやって参加して発表できて、とても良い1日になりました。スタッフの皆様、ありがとうございました。
社内でも同じこと書いたのですが、こっちでも。
大きなリスト(例えば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上でのパフォーマンスですが、差が大きいのは明確です)
なお、小さいリストだったら、いちいちHashSetに変える必要はないです。逆にHashSetにすることによって遅くなる場合もあります。 (小さければシーケンシャルに見たほうが早い場合も多いです。そもそも小さいと気にするレベルの差にもならないですが…)
このようなボトルネックは、プロファイリングかければすぐに見つかるので、パフォーマンスを厳しく求められたり、思った通りのパフォーマンスが出ていないときは、まずはプロファイリングにかけてみると良いです。 JavaだとFlight Recorderが簡単に使えて便利なので、とても助けられています。(それでこういった問題に気がついたことも多いです)
View customize plugin の v1.2.0 をリリースしました。2年ぶりの新バージョンになります。
今回のリリースには、2つの新規機能と細々とした変更がいくつか入っています。
以降はリリース内容の詳細です。
今までView customizeでは、ヘッダ部分にコードを挿入していました。 それが1.2.0からは、下記の3パターンから選べるようになります。
「Bottom of issue form」は、今まで嵌る人が多かった「チケット入力欄がトラッカーやステータスを変えた際に再構築されてしまい、View customizeでの変更内容がクリアされてしまう」といった問題に対応するためのものです。これを使うと、入力フォームが再構築される際にもコードが再度埋め込まれて実行されるので、今までのような考慮が不要になります。
なお、埋め込み箇所が存在しないページでは埋め込まれないことになるので、Path patternとの組み合わせにご注意ください。
たとえば、「Bottom of issue detail」はチケット詳細でしか埋め込まれないので、Path patternとして.*
で全ページを指定していてもチケット詳細画面でのみ実行されるようになります。(Path patternで正規表現を細かく考えなくて済むので、それはそれで便利かと)
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
となります。
今回日本語リソースを追加し、ロケールとして日本語を選択しているユーザに対しては、日本語で表示されるようになりました。ずっと英語だったので、今まで使っていた方々には違和感を感じる人も多いかもしれませんが、きっと慣れてもらえると思います。(私も最初は違和感ありましたが、やっと慣れました)
母国語じゃない、、ってだけで、敷居をあげてしまうということは実際あるので、いつかやらなきゃな、、と思っていたのですが、今回Pull requestをもらったのもあって、入れることにしました。今後は他の言語も入れて行きたいと思っています。
一覧系に関することで、下記の2点の修正を行っています。
コメント欄ですが、View customizeの設定が多くなってくると、内容を判断するための情報がコードの先頭部分だけだと厳しくなってきたため、コメント欄として別途設けることにしました。コメントが入力されていなかった場合には、今までどおりコードの先頭部分が表示されます。
他は細かいBugfixなので、詳細は省略します。
今回の新しい機能の使用例を、次回のredmine.tokyoでお話させていただく予定です。
あと、もうすぐリリースされそうなRedime4に対する対応は、Redmine4がリリースされたらすぐに出す予定です。(対応方法は既に確認済みですので、大丈夫かと、、)