株式会社スマレジの開発部でスマレジのサーバサイドを作っています

Redmine APIをGASで実行して、ガントチャートを同期する(2)

こんにちは!株式会社スマレジ、開発部のmasaです。

3月になって、忘れたように寒い日が続く一週間でしたが皆様体調はいかがでしょうか?

新型コロナウイルスにも気を付ける意味でも、体調管理はしっかりしていきましょう!

さて、今回は前回の続きです。

ロジックを分割する

前回作ったプログラムはすべてMain.gsにベタ書きしましたが、少し規模が大きくなりそうなので、下記のように記載場所を分けるようにします。

1 Redmineリクエストに含まれるJSONごとにエンティティを作成する。

Redmineのチケット参照APIを実行すると、下記のようなJSONが帰ってきます。

{
    "issues": [
        {
            "id": 1,
            "project": {
                "id": 1,
                "name": "練習プロジェクト"
            },
            "tracker": {
                "id": 1,
                "name": "設計"
            },
            "status": {
                "id": 1,
                "name": "未着手"
            },
            "priority": {
                "id": 1,
                "name": "5(標準)"
            },
            "author": {
                "id": 1,
                "name": "Redmine Admin"
            },
            "assigned_to": {
                "id": 1,
                "name": "Redmine Admin"
            },
            "category": {
                "id": 1,
                "name": "UI"
            },
            "fixed_version": {
                "id": 1,
                "name": "prac-1.0.0"
            },
            "parent": {
                "id": 2
            },
            "subject": "test",
            "description": "",
            "start_date": "2020-02-10",
            "due_date": "2020-02-16",
            "done_ratio": 30,
            "is_private": false,
            "estimated_hours": 10.0,
            "created_on": "2020-02-10T12:25:33Z",
            "updated_on": "2020-03-07T03:56:29Z",
            "closed_on": null
        }
    ],
    "total_count": 5,
    "offset": 0,
    "limit": 25
}

1つのチケットの中にも、projecttrackerなどのJSONがネストされています。 似たような構造(idとnameだけ)のものが多く、これらをいちいち組み立て&分解すると、 後々困りそうなので、下記のように固定のオブジェクトを返すメソッドを別ファイルにして用意します。

/**
 * Project Entity
 * @param {int} projectId プロジェクトID
 * @param {string} projectName プロジェクト名
 * @return {Object} Project Object 
 */
function createProject(projectId, projectName) {
  return res = {
    "id": projectId,
    "name": projectName
  };
}

2 リクエストの送受信をする接続用ファイルを作成する

Redmineにデータを送信する処理はいくつかの機能で使うので、別ファイル化しておきます。 私は下記のように、HTTP_METHODレベルで分けて作成して、関数名の初めにそれを記載するようにします。 optionsをメソッドの外に出して、好きなように接続できるようにもできますが、

  • optionsの中身をチェックしないといけなくなってめんどくさい
  • optionsみないと検索なのか更新なのか削除なのかわからない

という理由から今はつくっていません。 必要になったら作ります。

/**
 *  redmine APIで検索APIを実行
 *  @param String url リクエストURL
 *  @param Object conditions 検索条件
 *  @return Array 取得したチケット情報JSONの配列
 *  @throws Error 受信できないときや受信データにエラーがある場合はエラーを返す
 */
function getRedmineTickets(url,headers) {
    //urlfetchappのオプション情報
    var options = {
        "method" : "GET",
        "headers" : headers,     //header情報を追加
        "muteHttpExceptions" : true,
        "validateHttpsCertificates" : false //SSLエラー回避
    };
   try {
        //外部へアクセスさせる
        var resStr = UrlFetchApp.fetch(url, options).getContentText();
        if (resStr.length === 0) {
            throw new Error("受信データがありませんでした。");
        }
        var resJson = JSON.parse(resStr);
        if (isset(resJson.errors)) {
            throw new Error("取得条件が不正です。");
        }
    } catch(e) {
        Browser.msgBox("エラー:" + e);
    }
  return resJson;
}

3 Main.gsは極力セルの編集処理だけを記載し、他の処理は機能ごとにロジック用ファイルを作って記載する。

事実上のアクションになっているMain.gsのメソッドにごちゃごちゃ処理を書きたくありません。 まだ、ロジックらしいのは少ないですが、増えてくると読みにくくなるので別ファイルに分けます。

/**
 *  redmineの検索URLを作成
 *  
 *  @param Object conditions 検索条件
 *  @return String 生成したURL
 */
function createReferenceUrl(conditions) {
      var url = '<URL>?';
    
    Object.keys(conditions).forEach( function(value) {
      url = url + value + '=' + this[value] ;
    }, conditions)
    return url;
}

/**
 *  スプレッドシートのハイパーリンク(チケットURL)を作成
 *  
 *  @param String url redmineのURL
 *  @param String id チケットのチケットID
 *  @return String 生成したハイパーリンク関数
 */
function createHyperLink(url, id) {
    return '=HYPERLINK("' + url + id + '", "' + id + '")'
}

で、諸々を分離したMain.gsが↓

/**
 * redmineからチケット情報を取得し、プロジェクトのタスクに記載
 */
function createTaskFromRedmine() {
    //redmineのURL(後でセルで指定するように)
    var url = 'https://49.212.209.129';

    //検索条件(とりあえず固定)
    var conditions = {"project_id" : 1};
    //header情報(とりあえず固定)
    var headers = { 'X-Redmine-API-Key' : '8b5f2357309c516ccc5b69e00032e083f0fe9d09' };
    var url = createReferenceUrl(url, conditions);
    var resJson = getRedmineTickets(url);
    try {
        var currentIssue = "";
        var redmineChicket = "";
        var sheet = SpreadsheetApp.getActiveSheet();
        for (var i = 0; i < resJson.issues.length; i++) {
            currentIssue = resJson.issues[i];
            redmineChicket = createRedmineChicketFromRequest(currentIssue);
            setValueOfCellFromRequest(url, sheet, i + 11, redmineChicket);
        }

    } catch(e) {
        Browser.msgBox("エラー:" + e);
    }
}


function setValueOfCellFromRequest(url, sheet, rowNumber, redmineChicket) {
  var chicketUrl = url + "/issues/";
  var hyperLink = createHyperLink(chicketUrl, redmineChicket.id);  
  sheet.getRange(rowNumber, 2).setFormula(hyperLink);
  sheet.getRange(rowNumber, 3).setValue(redmineChicket.subject);
  sheet.getRange(rowNumber, 4).setValue(redmineChicket.author.name);
  sheet.getRange(rowNumber, 5).setValue(redmineChicket.start_date);
  sheet.getRange(rowNumber, 6).setValue(redmineChicket.due_date);
  sheet.getRange(rowNumber, 7).setValue(redmineChicket.estimated_hours);
  sheet.getRange(rowNumber, 8).setValue(redmineChicket.done_ratio * 0.01);
}

大分すっきりしました。ただ・・・・・・

f:id:masa2019:20200308210228p:plain
フォルダ分けできないのか。。。

gsファイルが1つの階層にあるので、まあ見づらい。 clasp導入してローカルで開発しようかな・・・。