2015年5月11日月曜日

kintone@本日日付をyyyymmddにする

// 本日をYYYYMMDDにする
var dateToday = function () {
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
if (month < 10) {
month = "0" + month;
}
if (day < 10) {
day = "0" + day;
}
return year + month + day;
};

2015年4月10日金曜日

kintone@日付と日時を比較する方法

●背景
日付と日時を比較した時に問題は起きた。

まずは下図のように日付と日時を並べてみるのである。


そして、両データをJavaScriptでアラート表示させてみるのである。


なんでじゃー。3/31よ、君はどっからきたー。

というわけなのである。
日付の方は入力された通りの正しい日付が入っているように見える、が、
日時の方は一日前の、とんちんかんな時間を指している、ように見える。

●原因
日時はタイムゾーンの影響を受けるようである。
つまり、日時だけは世界標準時で設定されるわけだ。

日本であれば「+9時間の時差がある」とみなされて、
「4/1の0時」からシステムサイドで-9時間の処理を行い、
「3/31の15時」というアラートが出てしまうわけだ。

はー、使いにくい話である。

●対策
「日時の比較」であれば「日付」を-9時間すれば良い。
「日付フォームの値を送るとその地域の時差に合わせて日時が取得できるコード」
をサンプルコードとして作った。


●サンプルコード
    /***************************************************************************************************
    * function getDateTime
    * @param:date:「event['record']['日付']['value']」等をそのまま引き渡す
    ***************************************************************************************************/
    function getDateTime(date) {
        // ログインユーザのタイムゾーン名を取得する。日本なら「Asia/Tokyo」と取得
        var timeZone = kintone.getLoginUser().timezone;
        
        // タイムゾーン名から時差を取得
        var timeZoneOffset = getTimeZoneOffset(timeZone);
        
        // 年月日を抽出
        var dateFromYYYY = event['record']['日付']['value'].substring(0, 4);
        var dateFromMM = event['record']['日付']['value'].substring(5, 7);
        var dateFromDD = event['record']['日付']['value'].substring(8, 10);
        
        // 月(0~11)調整しながら、時差を加味したDateオブジェクトを生成
        var dateFromYMDT = new Date(dateFromYYYY, dateFromMM - 1, dateFromDD, timeZoneOffset, 0);
        
        // Dateオブジェクトから「日時フォーム」形式に整形する
        var dateFromYYYY2 = dateFromYMDT.getFullYear();
        var dateFromMM2 = ('00' + String(dateFromYMDT.getMonth() + 1)).slice(-2);
        var dateFromDD2 = ('00' + dateFromYMDT.getDate()).slice(-2);
        var dateFromHH2 = ('00' + dateFromYMDT.getHours()).slice(-2);
        var dateFromMI2 = ('00' + dateFromYMDT.getMinutes()).slice(-2);
        var dateFromYMDT2 = dateFromYYYY2 + '-' + dateFromMM2 + '-' + dateFromDD2
            + 'T' + dateFromHH2 + ':' + dateFromMI2 + ':' + '00Z';

        return dateFromYMDT2;
    }

    /***************************************************************************************************
    * function getTimeZoneOffset
    * @param:timeZoneName:タイムゾーン名
    ***************************************************************************************************/
    function getTimeZoneOffset(timeZoneName) {
        var ret = -9;
        var timeZoneOffsetObj = new Object();
        timeZoneOffsetObj['Etc/GMT+12'] = 12;
        timeZoneOffsetObj['Etc/GMT+11'] = 11;
        timeZoneOffsetObj['Pacific/Honolulu'] = 10;
        timeZoneOffsetObj['America/Anchorage'] = 9;
        timeZoneOffsetObj['America/Santa_Isabel'] = 8;
        timeZoneOffsetObj['America/Los_Angeles'] = 8;
        timeZoneOffsetObj['America/Chihuahua'] = 7;
        timeZoneOffsetObj['America/Phoenix'] = 7;
        timeZoneOffsetObj['America/Denver'] = 7;
        timeZoneOffsetObj['America/Guatemala'] = 6;
        timeZoneOffsetObj['America/Chicago'] = 6;
        timeZoneOffsetObj['America/Regina'] = 6;
        timeZoneOffsetObj['America/Mexico_City'] = 6;
        timeZoneOffsetObj['America/Bogota'] = 5;
        timeZoneOffsetObj['America/Indiana/Indianapolis'] = 5;
        timeZoneOffsetObj['America/New_York'] = 5;
        timeZoneOffsetObj['America/Caracas'] = 4.5;
        timeZoneOffsetObj['America/Halifax'] = 4;
        timeZoneOffsetObj['America/Asuncion'] = 4;
        timeZoneOffsetObj['America/La_Paz'] = 4;
        timeZoneOffsetObj['America/Cuiaba'] = 4;
        timeZoneOffsetObj['America/Santiago'] = 4;
        timeZoneOffsetObj['America/St_Johns'] = 3.5;
        timeZoneOffsetObj['America/Sao_Paulo'] = 3;
        timeZoneOffsetObj['America/Godthab'] = 3;
        timeZoneOffsetObj['America/Cayenne'] = 3;
        timeZoneOffsetObj['America/Argentina/Buenos_Aires'] = 3;
        timeZoneOffsetObj['America/Montevideo'] = 3;
        timeZoneOffsetObj['Etc/GMT2'] = 2;
        timeZoneOffsetObj['Atlantic/Cape_Verde'] = 1;
        timeZoneOffsetObj['Atlantic/Azores'] = 1;
        timeZoneOffsetObj['Africa/Casablanca'] = -0;
        timeZoneOffsetObj['Atlantic/Reykjavik'] = -0;
        timeZoneOffsetObj['Europe/London'] = -0;
        timeZoneOffsetObj['Etc/GMT'] = -0;
        timeZoneOffsetObj['Europe/Berlin'] = -1;
        timeZoneOffsetObj['Europe/Paris'] = -1;
        timeZoneOffsetObj['Africa/Lagos'] = -1;
        timeZoneOffsetObj['Europe/Budapest'] = -1;
        timeZoneOffsetObj['Europe/Warsaw'] = -1;
        timeZoneOffsetObj['Africa/Windhoek'] = -1;
        timeZoneOffsetObj['Europe/Istanbul'] = -2;
        timeZoneOffsetObj['Europe/Kiev'] = -2;
        timeZoneOffsetObj['Africa/Cairo'] = -2;
        timeZoneOffsetObj['Asia/Damascus'] = -2;
        timeZoneOffsetObj['Asia/Amman'] = -2;
        timeZoneOffsetObj['Africa/Johannesburg'] = -2;
        timeZoneOffsetObj['Asia/Jerusalem'] = -2;
        timeZoneOffsetObj['Asia/Beirut'] = -2;
        timeZoneOffsetObj['Asia/Baghdad'] = -3;
        timeZoneOffsetObj['Europe/Minsk'] = -3;
        timeZoneOffsetObj['Asia/Riyadh'] = -3;
        timeZoneOffsetObj['Africa/Nairobi'] = -3;
        timeZoneOffsetObj['Asia/Tehran'] = -3.5;
        timeZoneOffsetObj['Europe/Moscow'] = -4;
        timeZoneOffsetObj['Asia/Tbilisi'] = -4;
        timeZoneOffsetObj['Asia/Yerevan'] = -4;
        timeZoneOffsetObj['Asia/Dubai'] = -4;
        timeZoneOffsetObj['Asia/Baku'] = -4;
        timeZoneOffsetObj['Indian/Mauritius'] = -4;
        timeZoneOffsetObj['Asia/Kabul'] = -4.5;
        timeZoneOffsetObj['Asia/Tashkent'] = -5;
        timeZoneOffsetObj['Asia/Karachi'] = -5;
        timeZoneOffsetObj['Asia/Colombo'] = -5.5;
        timeZoneOffsetObj['Asia/Kolkata'] = -5.5;
        timeZoneOffsetObj['Asia/Kathmandu'] = -5.75;
        timeZoneOffsetObj['Asia/Almaty'] = -6;
        timeZoneOffsetObj['Asia/Dhaka'] = -6;
        timeZoneOffsetObj['Asia/Yekaterinburg'] = -6;
        timeZoneOffsetObj['Asia/Rangoon'] = -6.5;
        timeZoneOffsetObj['Asia/Bangkok'] = -7;
        timeZoneOffsetObj['Asia/Novosibirsk'] = -7;
        timeZoneOffsetObj['Asia/Krasnoyarsk'] = -8;
        timeZoneOffsetObj['Asia/Ulaanbaatar'] = -8;
        timeZoneOffsetObj['Asia/Shanghai'] = -8;
        timeZoneOffsetObj['Australia/Perth'] = -8;
        timeZoneOffsetObj['Asia/Singapore'] = -8;
        timeZoneOffsetObj['Asia/Taipei'] = -8;
        timeZoneOffsetObj['Asia/Irkutsk'] = -9;
        timeZoneOffsetObj['Asia/Seoul'] = -9;
        timeZoneOffsetObj['Asia/Tokyo'] = -9;
        timeZoneOffsetObj['Australia/Darwin'] = -9.5;
        timeZoneOffsetObj['Australia/Adelaide'] = -9.5;
        timeZoneOffsetObj['Australia/Hobart'] = -10;
        timeZoneOffsetObj['Asia/Yakutsk'] = -10;
        timeZoneOffsetObj['Australia/Brisbane'] = -10;
        timeZoneOffsetObj['Pacific/Port_Moresby'] = -10;
        timeZoneOffsetObj['Australia/Sydney'] = -10;
        timeZoneOffsetObj['Asia/Vladivostok'] = -11;
        timeZoneOffsetObj['Pacific/Guadalcanal'] = -11;
        timeZoneOffsetObj['Etc/GMT-12'] = -12;
        timeZoneOffsetObj['Pacific/Fiji'] = -12;
        timeZoneOffsetObj['Asia/Magadan'] = -12;
        timeZoneOffsetObj['Pacific/Auckland'] = -12;
        timeZoneOffsetObj['Pacific/Tongatapu'] = -13;
        timeZoneOffsetObj['Pacific/Apia'] = -13;
        for (var key in timeZoneOffsetObj) {
            if (key == timeZoneName) {
                ret = timeZoneOffsetObj[key];
                break;
            }
        }
        return ret;
    }

2015年4月8日水曜日

kintone@REST API@レコードを登録(INSERT)する

●背景
「つべこべ言わずに、動くソースをくれよ」

パート2である。

・指定したアプリに1レコード登録する。
・データは当該アプリで画面入力されている名前と年齢
 (自アプリにも、指定先アプリにも名前、年齢のテキストボックスが必要)
・登録できたらレコード番号を、できなかったら-1を返却する

●サンプルコード
    /***************************************************************************************************
    * function insertRec
    * 登録できた場合はレコード番号を返却する。
    * 登録できなかった場合は-1を返却する。
    ***************************************************************************************************/
    function insertRec(event, appID) {
        // 返却値
        var lastRID = -1;

        var param =
        {
            "app": appID,
            "record":
            {
                "名前": { "value": event['record']['名前']['value'] },
                "年齢": { "value": event['record']['年齢']['value'] },
            }
        }
        // CSRFトークンの取得
        var token = kintone.getRequestToken();
        param["__REQUEST_TOKEN__"] = token;
        // 同期リクエストを行う
        var appUrl = kintone.api.url('/k/v1/record');
        var xmlHttp = new XMLHttpRequest();
        xmlHttp.open('POST', appUrl, false);
        xmlHttp.setRequestHeader('Content-Type', 'application/json');
        xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xmlHttp.send(JSON.stringify(param));
        if (xmlHttp.status == 200) {
            var respdata = JSON.parse(xmlHttp.responseText);
            lastRID = respdata["id"];
        }
        return lastRID;
    }
●解説
・SELECTでやった解説は除外します。

・param
 appとrecordで構成されるよ。詳しくは「レコードの登録(POST)

・xmlHttp.open('POST', appUrl, false);
 GETじゃなくてPOSTになる。

・xmlHttp.send(JSON.stringify(param));
 今回は取得ではなく登録なので、
 nullではなくて、JSON形式のデータを送る。

・var respdata = JSON.parse(xmlHttp.responseText);
 登録結果のレスポンス内容を受け取る。

・lastRID = respdata["id"];
 レスポンスの中からレコード番号を取得。

・今回は1レコードなので'/k/v1/record'を指定した。

・複数レコードの場合は以下の内容が変わる
  →'/k/v1/record'が'/k/v1/records'になる
  →paramの"record"が"records"になる
  →paramの"records"の右辺(:の右側)が配列になる

2015年4月3日金曜日

kintone@データセットを作ってしまう

●背景
DBがない。1アプリ1データセットのkintone思想が
「くれぐれもコーディングするなよ!」と念を押してくるようだ。
でもやる。


●対応
appIDカラムと、枝IDカラム1~5、value値あたりまでを持った
コードマスタ的なアプリを作り、
閲覧はEveryone、登録+編集+削除は制限してしまえばいい。

2015年4月2日木曜日

kintone@プロセス管理(ステータス変更時)のイベント

●背景
1)承認済みに変更された日付を更新したい
2)承認済みに変更できる作業者を制限したい


●APIの記載事項
・eventオブジェクトを return することでレコード情報を更新できます。
  (参考:フィールドの値を書き換える)※レコード編集権限が必要です。
・false を return した場合アクションがキャンセルされます。
・eventオブジェクトに error プロパティを設定して return した場合、
  error に設定した文字列でアラートが表示され、アクションがキャンセルされます。
・不正な値を return した場合エラーが表示されてアクションがキャンセルされます。
・何も return しない場合ステータスのみが更新されます。


●サンプルコード
// プロセス管理アクション実行時
kintone.events.on(["app.record.detail.process.proceed"], function(event){
    var record = event.record;
    var nStatus = event.nextStatus.value;

    // ステータスが「承認済み」の場合、承認日と承認者を設定する
    switch(nStatus){
        case "承認済み":
            var user = kintone.getLoginUser();
          if(user.code == "上長") {
                record['承認日']['value'] = moment().format("YYYY-MM-DDTHH:mmZ");
                record['承認者']['value'][0] = {code : user.code};
            } else {
                event.error = "上長以外は承認済みに設定できません";
            }
            break;
    }
    return event;
});

2015年4月1日水曜日

kintone@アプリAのINSERT時にレコード番号を取得したい

●背景
アプリAでレコードを登録する際の(app.record.create.submit)では
レコードがまだ作成されていないので「レコード番号」は未採番だ。
番号の予約も出来ていないので何番になるか分からない。

だがアプリAのINSERT時に、アプリBにも同時にINSERTしたい場面はあるだろう。
そしてアプリBには「アプリAのレコード番号」をカラムとして持っておき、
両データを関連付けたい、そんな場合、どうすれば良いかを考えた。

●解決策1
アプリAの最終レコード番号+1を自レコード番号とする方式
 →よく目にする、一見して無駄のない処理方式である、
  が、正常な挙動が望めない場合がある。
  例えば「作成者がログインユーザのレコードのみ閲覧可能」のような、
  ユーザに対してレコードの閲覧権限をいじっている場合がそうだ。

  「JavaScriptでの閲覧権限」は「ログインユーザ」と同じレベルのため、
  最終番号を保持したレコードが当該ログインユーザではない場合、
  IDが参照できず、重複してしまう。

  これ以外にも例えばアプリ操作者が複数いる場合、
  ユーザ1が最新レコードを参照してる間に
  ユーザ2も同じレコードを参照してしまい、
  それぞれが+1した同じレコード番号を登録してしまう可能性がある。

  上述の心配がまったくいらない業務では、以下のソースを使えば良い
●サンプルコード1
/***************************************************************************************************
* 最新レコード番号を取得する1
* @param event イベント
* @param appID 取得対象のアプリID
* @return recID 現レコード番号の最大値+1。レコード非存在時は1。処理失敗時は-1。
***************************************************************************************************/
function getLastRecID1(event, appID) {
    // 戻り値の設定(処理失敗時は-1)
    var recID = -1;

    // URLを設定する
    var appUrl = kintone.api.url('/k/v1/records', true)
        + '?app=' + appID
        + '&query=' + encodeURI('order by レコード番号 desc limit 1');

    // 最新レコード番号を取得
    try {
        var xmlHttp = new XMLHttpRequest();
        xmlHttp.open("GET", appUrl, false);
        xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xmlHttp.send(null);
        if (xmlHttp.status == 200 && window.JSON) {
            var obj = JSON.parse(xmlHttp.responseText);
            if (obj.records[0] != null) {
                // recID = 現レコード番号の最大値+1
                recID = parseInt(obj.records[0][' id']['value']) + 1;
            } else {
                // recID = 1(レコード非存在時)
                recID = 1;
            }
        }
    } catch (e) {
    }
    return recID;
}
●解決策2
・アプリCを作り、アプリAの新規登録時、まずアプリCにINSERTを行う。
 INSERTしたレスポンスで取得できるIDをアプリAのレコード番号として使う方式。
 →アプリCというアプリを作る必要があり、
  無駄な採番が行われてしまいやすい等の欠点はあるが、
  レコード番号の予約をほぼ完ぺきな形で行うことができる。
●サンプルコード2
/***************************************************************************************************
* 最新レコード番号を取得する2
* @param event イベント
* @param appID_RecIDManager レコードID採番用アプリ(アプリC)のAppID
* @return recID 予約した最新レコード番号。処理失敗時は-1。
***************************************************************************************************/
function getLastRecID2(event, appID_RecIDManager) {
    // 戻り値の設定(処理失敗時は-1)
    var lastRecID = -1;

    /*=========================================
     * 「RecIDManager(アプリC)」に登録
     =========================================*/
    var param =
    {
        "app": appID_RecIDManager,
        "record": {}
    }
    // CSRFトークンの取得
    var token = kintone.getRequestToken();
    param["__REQUEST_TOKEN__"] = token;
    // 同期リクエストを行う
    var appUrl = kintone.api.url('/k/v1/record');
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open('POST', appUrl, false);
    xmlHttp.setRequestHeader('Content-Type', 'application/json');
    xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    xmlHttp.send(JSON.stringify(param));
    if (xmlHttp.status == 200 && window.JSON) {
        var respdata = JSON.parse(xmlHttp.responseText);
        lastRecID = respdata["id"];
    }
    return lastRecID;
}

kintone@一覧からの編集を制限する方法

●背景
「javascriptでの操作権限はユーザ権限と完全に等しいkintone世界」では、
「ユーザには隠して持っておきたい情報」を、
ユーザに完全に非表示にすることは事実上不可能だ。

レコードの新規登録時(app.record.create.showやedit)、
レコードの編集時(app.record.edit.showやedit)では
項目の表示/非表示や活性/非活性などの入力/表示制限が実装可能だが、

レコードの一覧更新時(app.record.index.edit.showやsubmit)では
上述の入力/表示制限が全く仕事してくれない。
「一覧:(すべて)」の前では非表示にしたいあらゆる項目がユーザに剥き出しだ。

現状では「見せてはやるが編集はさせない」を次善策として、
我慢して運用していくしかない。
とはいえ一覧では項目の表示/非表示はおろか、
活性/非活性さえ機能してくれない。

というわけで、一覧保存時('app.record.index.edit.submit')に
とにかくエラーを吐かせて編集させないこととした。

●サンプルコード
    /*******************************************************************************************
    * 一覧画面      保存
    ********************************************************************************************/
    kintone.events.on('app.record.index.edit.submit', function (event) {
        event.error = "一覧画面からの編集機能は制限しております";
        return event;
    });