Redmine: View customize plugin の v2.3.0 をリリースしました

View customize plugin の v2.3.0 をリリースしました。

ViewCustomize.contextへプロジェクトのカスタムフィールドを追加しています。

ViewCustomize.contextへの追加基準ですが、いまのところ下記のように考えています。(何でも入れてしまうとメンテナンスが面倒になっていくのと、きりがなくなりそうなので)

  • 他の方法で取得できない、または取得出るがかなり煩雑
  • 利用するシチュエーションが多数想定できる

今回のプロジェクトのカスタムフィールドは、、

  • REST APIを使えば取得できなくないがREST APIを使うこと自体が煩雑
  • 他の人からのPull requestがあったのと、ユーザのカスタムフィールドのように、プロジェクト毎に有効/無効を切り替えたり、表示したいものを変えたりといったような使い方ができそう

ということで追加しました。

ShortcutKey2URL for Chrome のバージョン1.1.0をリリースしました

ShortcutKey2URL for Chrome のバージョン1.1.0をリリースしました。

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

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

f:id:onozaty:20171018003430p:plain

バージョン1.1.0での変更内容

コンテキストメニューからページを追加可能になりました。

f:id:onozaty:20190624000929p:plain

ポップアップからクリックでも実行できるようにしました。(マウスで操作したい人もいるかもしれないということで...)

f:id:onozaty:20190624001536p:plain

複数ウインドウが存在する場合、アクティブではないウインドウに対して処理してしまうことがあるバグを修正しました。

CSVファイルをPostgreSQLにロードするツール(csv2postgresql)を作りました

CSVファイルをPostgreSQLにロードするツールを作りました。

事前にテーブルを作っておく必要は無いので、とりあえずCSVファイルをPostgreSQLにロードしていじりたいって時に便利かと思います。

テーブルはCSVのヘッダに記載のフィールド名を元に作成されます。なお、既にテーブルがある場合は作りません。

内部的にはCOPYコマンドを使っているので、とても早いと思います。(もともとバッチINSERTを使った実装をしていましたが、COPYコマンド使うように変えたら50倍早くなりました)

利用方法

実行にはJava(JDK8以上)が必要となります。

下記から最新の実行ファイル(csv2postgresql-x.x.x-all.jar)を入手します。

入手したjarファイルを指定してアプリケーションを実行します。

java -jar csv2postgresql-1.0.0-all.jar config.properties table1 data.csv

引数は下記の通りです。

  1. 設定ファイルパス
  2. テーブル名
  3. CSVファイルパス

実行すると、下記のように処理したレコード件数、かかった時間が出力されます。

Loading...
Loading is completed. (Number of records: 100,000 / Elapsed millsecods: 322)

設定ファイル

設定ファイルには、PostgreSQLの接続先情報と、CSVファイルのエンコーディングを記載します。

  • database.url JDBC接続URL
  • database.user DBユーザ名
  • database.password DBパスワード
  • csv.encoding CSVファイルのエンコーディング

以下は例です。

database.url=jdbc:postgresql://192.168.33.10:5432/testdb
database.user=user1
database.password=pass1
csv.encoding=UTF-8

テーブル名

ロード先のテーブル名を指定します。

テーブルが存在しなかった場合、新規にテーブルを作成します。この際、各カラムはtext型として作成されます。

CSVファイル

CSVファイルのヘッダは必須です。ヘッダに記載のフィールド名を使って、ロード先のテーブルのカラムとマッピングします。 なお、英数字以外の文字が指定されていた場合、アンダースコア(_)に置換されます。

たとえばUser Nameというフィールドがあった場合、データベースのカラムとしてはuser_nameにマッピングされます。

サンプル

PostgreSQLを起動するためのVagrant環境と、設定ファイルとCSVファイルのサンプルが用意してあります。 これらを使うことによって、簡単に本ツールを試せます。

vagrantフォルダにてvagrant upを実行すると、PostgreSQL11をインストールした仮想環境(192.168.33.10)が立ち上がります。

sampleフォルダ配下の設定ファイルとCSVファイルを利用してロードを行います。

java -jar csv2postgresql-1.0.0-all.jar sample/config.properties test_table sample/sample-100000.csv

ビルド方法

ソースコードからビルドして利用する場合、Java(JDK8以上)がインストールされた環境で、下記コマンドでアプリケーションをビルドします。

gradlew shadowJar

build/libs/csv2postgresql-x.x.x-all.jarという実行ファイルが出来上がります。(x.x.xはバージョン番号)

Ed Sheeran - DIVIDE WORLD TOUR 2019 (2019年4月10日@東京ドーム)

Ed Sheeran(エド・シーラン)のライブに嫁さんと一緒に参戦してきました。

15時過ぎに東京ドームについて、まずは物販からということで、Tシャツとタオルをを購入。物販は10分程度並んだだけで、そんな待たずに買えました。

席は1階スタンドです。ステージから真正面ですが遠いです。S席の範囲がとても広く、どこになるかは運任せなのでしょうがないですね。

17時45分からオープニングアクトでONE OK ROCKです。チケット購入した後に発表されたオープニングアクトですが、ワンオクも聞けるなんて、、すごい得した気分です。エド・シーランと交流があるのは有名な話だったので、もしかしたらなーって思っていたけど、まさか本当にそうなるなんて!!

狭いステージを動き回るTakaかっこいい。

オープニングアクトは45分くらいで終わり、19時からエド・シーランのライブ開始です。

ギターとループステーションだけでライブやりきってしまうというのは知っていましたが、いや、ほんと凄すぎました。ループステーションを使ってどんどん音を重ねていって、歌いながらプラスしてさらにギター、、エド・シーラン一人によってすごい複雑な音楽が作り出されます。

特に最後のYou Need Me, I Don’t Need Youは圧巻で、鳥肌が立ちました。ほんと最高のライブでした。

セットリスト (ONE OK ROCK)

  1. Push Back
  2. Deeper Deeper
  3. Clock Strikes
  4. Head High
  5. Stand Out Fit In
  6. The Beginning
  7. Mighty Long Fall
  8. Wasted Nights

セットリスト (Ed Sheeran)

  1. Castle on the Hill
  2. Eraser
  3. The A Team
  4. Don’t / New Man
  5. Dive
  6. Bloodstream
  7. Love Yourself (Justin Bieber)
  8. Tenerife Sea
  9. Lego House / Kiss Me / Give Me Love
  10. Galway Girl
  11. Feeling Good / I See Fire
  12. Thinking Out Loud
  13. One / Photograph
  14. Perfect
  15. Nancy Mulligan
  16. Sing
  17. Shape of You (アンコール)
  18. You Need Me, I Don’t Need You (アンコール)

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

チケットを作成して、関連付けを行いたいという問い合わせがあったので書いてみました。

チケットの関連付けには下記のAPIを使います。

設定内容

  • 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 currentIssueId =  ViewCustomize.context.issue.id;

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

  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
        },
        dataType: 'json',
        contentType: 'application/json',
        data: JSON.stringify(issue)
      })
      .then(function(response) {
        var createdIssueId = response.issue.id;
        return $.ajax({
          type: 'POST',
          url: '/issues/' + currentIssueId + '/relations.json',
          headers: {
            'X-Redmine-API-Key': ViewCustomize.context.user.apiKey
          },
          dataType: 'json',
          contentType: 'application/json',
          data: JSON.stringify({
            'relation' : {
              'issue_to_id' : createdIssueId,
              'relation_type' : 'relates'
            }
          })
        })
      })
    };
  }
})

画面イメージ

f:id:onozaty:20190324003243g:plain

テキストエリアで入力補完 (Redmine View Customize Plugin)

先日2.1.0をリリースしてHTMLをそのまま埋め込めるようになったので、それを利用したサンプルということで、テキストエリアで入力補完的なことを行うサンプルを書いてみました。

やっていることは、下記のGreasemonkeyで行っていたものと基本的には同じです。

今回は、textile記法の補完だけでなく、テンプレート的なものも埋め込めるようにしています。

  • <preと入力したら、<pre><code class="java">のようなシンタックスハイライト
  • {{と入力したら、{{thumbnail(image.png)}}のようなマクロ
  • templateと入力したら、テンプレート文面

JavaScriptだけでも書けなくはないですが、、外部JSを読み込むのと、CSSを書かなければならないので、HTMLで書けたほうが楽だと思います。

設定内容

  • Path pattern: .*
  • Insertion position: Head of all pages
  • Type: HTML

注:Edgeだとうまく動作しないことがわかったため、外部ライブラリ読み込み時のasyncを消しました。(2019-07-29)

<script src="https://cdnjs.cloudflare.com/ajax/libs/at.js/1.5.2/js/jquery.atwho.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Caret.js/0.3.1/jquery.caret.min.js" defer></script>
<style>
.atwho-view {
    position:absolute;
    top: 0;
    left: 0;
    display: none;
    margin-top: 18px;
    background: white;
    color: black;
    border: 1px solid #DDD;
    border-radius: 3px;
    box-shadow: 0 0 5px rgba(0,0,0,0.1);
    min-width: 120px;
    z-index: 11110 !important;
}
.atwho-view .atwho-header {
    padding: 5px;
    margin: 5px;
    cursor: pointer;
    border-bottom: solid 1px #eaeff1;
    color: #6f8092;
    font-size: 11px;
    font-weight: bold;
}
.atwho-view .atwho-header .small {
    color: #6f8092;
    float: right;
    padding-top: 2px;
    margin-right: -5px;
    font-size: 12px;
    font-weight: normal;
}
.atwho-view .atwho-header:hover {
    cursor: default;
}
.atwho-view .cur {
    background: #3366FF;
    color: white;
}
.atwho-view .cur small {
    color: white;
}
.atwho-view strong {
    color: #3366FF;
}
.atwho-view .cur strong {
    color: white;
    font:bold;
}
.atwho-view ul {
    /* width: 100px; */
    list-style:none;
    padding:0;
    margin:auto;
    max-height: 200px;
    overflow-y: auto;
}
.atwho-view ul li {
    display: block;
    padding: 5px 10px;
    border-bottom: 1px solid #DDD;
    cursor: pointer;
    /* border-top: 1px solid #C8C8C8; */
}
.atwho-view small {
    font-size: smaller;
    color: #777;
    font-weight: normal;
}
</style>
<script>
$(function() {
  $('textarea.wiki-edit').atwho({
    at: '<pre',
    data: [
      {name: 'java', content: '<pre><code class="java">\n</code></pre>'},
      {name: 'sql', content: '<pre><code class="sql">\n</code></pre>'}],
    insertTpl: '${content}',
    suffix: ''
  }).atwho({
    at: '{{',
    data: [
      {name: 'collapse', content: '{{collapse(詳細を表示...)\n}}'},
      {name: 'thumbnail', content: '{{thumbnail(image.png)}}'}],
    insertTpl: '${content}',
    suffix: ''
  }).atwho({
    at: 'template',
    data: [
      {name: 'バグ', content: 'h2. 発生バージョン\n\nh2. 再現手順\n\nh2. ログ\n\n'},
      {name: '問い合わせ', content: 'h2. 問い合わせ内容\n\nh2. 回答期限\n\n'}],
    insertTpl: '${content}',
    suffix: ''
  });
});
</script>

動作

下記のような感じで、特定の文字をトリガーにして候補が表示され、候補を選ぶと文字列が埋め込まれます。

f:id:onozaty:20190322000212g:plain