kintoneにシステム移したいんや


kintone Advent Calendar 2020の22日目の記事です。

  Topへ↓

おかげさまで今年はkintoneの案件が大幅に増え、ありがたい限りです。引き続き、頑張っていきまっせ^+++^

さて、案件が増えたのはめっちゃうれしいんやけど、今年は今までと比べ、案件の傾向に少し変化がありましてん。その傾向っちゅうのは旧システムからkintoneへ移行する案件の増加ですねん。
もちろん、昨年までも移行案件は請けてました。でも、今年はC/S(Cliend/Server)型の旧システムからkintoneへの移行案件をぎょうさんご依頼いただきまして。その数はめっちゃ増えましたわ。

おそらく今後もこうした移行案件は増えていくと思てますねん。そやさかい今年のkintone Advent Calendarは7回目の参加にして、初めて連携開発ではなく、移行について書いてみようと思とります。
これからkintoneへのシステム移行を手掛ける方の参考になればうれしいねん。

システム移行の建前と本音

  Topへ↑

システムを移行する目的はお客様によって千差万別です。
でも、システムを移行すると決断するのは経営層、少なくとも部署の責任者でっしゃろ?

この時、経営層と現場でシステムを扱う方の見る視点は違います。
経営層は人件費の削減や処理効率の改善に重きを置きます。一方で現場の視点は日々の業務の負担を軽減することと、業務を止めないことに向きがちです。

その時、建前上では業務改善を同時に行いながらシステム導入も行う、という意見が出ます。そりゃせやんなぁ。わてら提案側にとっても業務改善を錦の御旗に立てたほうが提案は通りやすいっちゅうもんで。

そやけど、往々にして現場の本音とは従来の業務フローを変えずに、なおかつ業務負担が軽くなることにあると思とります。業務改善によってオペレーションが大きく変わるのはいややなあ。新たなやり方になれるまでが大変やわ。てなわけで抵抗を覚えがちです。

わてがこの時に心がけとるんは、営業の相手である経営層や上層部とのお話がまとまったら、なるべく早いうちに現場の業務担当に挨拶し、現場の方とのコミュニケーションを増やし、現場の本音を伺いながら開発することですねん。
この時の建前と本音と取り違えると、あとあとまでボタンの掛け違いがえらいこっちゃになります。
また、営業目線でお客様の上層部とだけ話をしていると、現場のニーズが後から後から噴出してがっちゃがちゃになります。わては何度もこれでえらい目に遭っとります。

構築の順番

  Topへ↑

現場の方にとっては、慣れ親しんだ旧システムからkintoneという未知のシステムに触れるわけです。不安になって当然やんなぁ。
そやからわてはkintoneの良さ、つまり現場の方々が簡単にシステムが触れまっせ、簡単に設定ができまっせ、ということを早めにご説明します。

このあたりは旧来のシステム開発のセオリーとは違いますわな。要件定義、内部/外部設計、コーディング、単体テスト、結合テストが終わったあたりでようやくマニュアルが用意され、ユーザー受け入れテストで現場の方が触る。それが旧来のシステム開発。

わてはkintoneの場合はより早いうちに現場の方に触ってもらうことを重視します。フィールドコードは触らんといて、プラグインやJavaScriptはいじらんといて、という点だけお願いし、あとは積極的に使ってもらいます
その結果、kintoneってうちらでも触れるんや、現場でもシステム開発に参加してええんや、という実感を持ってもらえればもうしめたもんですわ。
この場合の現場というのは現場の統括者ではなく、文字通り日次の業務で手を動かしている方全員のことやからね。

これで現場の方からkintoneへの警戒感や拒否感が取り除けて、システム開発へ参加しているという実感を持ってもろたら、kintoneのファンにだってなってもらえます。そうなればシステム導入の成功は約束されたも同然ですねん。

その時、最後になるまでシステムを見せへん、という手法は逆効果。
まず移行が順調にできていることを示すためにも、一番やりやすいマスタ系からアプリを作っていくとよろしおま。
マスタは業務の基本であり、なおかつマスタアプリ自体には複雑なロジックはそれほど必要ないよって、導入側にとっても取り掛かりやすいはずですわ。
あと、早めにお客様にシステムに関わってもらうことによって、kintoneが苦手な部分をわかってもらうのも重要!なんでかいうたら、システム導入間近になっての仕様変更で断れるからやねん。

また、フィールドの追加や編集も自在にできるkintoneの特性がお客様に喜ばれるのもこの時。もちろん、ビジネスロジックに関わりの少ないフィールドを除いてやで。
例えば分析用に得意先種別を増やしたいねん、というご要望にもさくっと対応できるkintone。このかっこええ姿をアピールするだけで、好感度アップは間違いなしや。

また、ここで現場の方に新旧両方のシステムにマスタの入力を行ってもらえれば、次に述べるデータ移行の手間が大幅に減ります。ほんまやで。

データ移行の基本

  Topへ↑

kintoneへのシステム移行でいっちゃん肝心なんは、おそらくこの部分だと思とります。
kintoneはご存じの通り、簡単にアプリが構築できますやろ。そやから、元のシステムのデータの項目を再現するのはそれほど難しくない。そないに思うかたもおるんとちゃいますか?
ところが、ここを甘く見とったら後で苦労しますねん。

とくに、もともとのシステムの仕様で、マスタ上で管理する項目が制限されとったら要注意です。
入れたい情報を入れるべき項目がない。その場合、おうおうにしてお客様は備考欄にあれこれ詰め込みますねん。苦肉の策で本来ならメールアドレスやURLを入れるべき欄にまで雑多な情報を入れてしまいます。
kintoneでは簡単にフィールドを追加できまっしゃろ?でも、おうおうにして旧システムでは項目を追加するのに別途費用がかかります。だから、そないな状態になってしまうんですわ。
こうした雑多な情報がどこで管理されとるんか。その情報をkinotneのどの項目に移すんか。きちんと体系化されたデータとして活用したいんやったら、お客様ときちんと押さえとかなあきません。

次に、マスタをルックアップで呼び出す際の仕様は早めに定めとかなあきまへんで。御存じの通り、ルックアップでは関連付けるアプリを設定しますよって。
そしたら、コピー元のフィールドも設定せんならん。ルックアップフィールドではフィールドに文字を入力することで、候補を事前に絞り込むことができます。ただしコピー元のフィールドに設定した値に限りますけどな。

例えば得意先マスタの得意先コードを指定しておくと、マスタの得意先コードで検索が可能です。ですが、一度ルックアップフィールドをこさえた後にお客様から得意先名でも検索したい、と言われたらもうバンザイせなあきません。なんでかちゅうたら、コピー元のフィールドは一度設定すると変更でけへんよって。
もしどうしても変更の必要が生じたら、新たにルックアップフィールドを追加せんならんのです。もちろん古いルックアップのデータから新しいルックアップにデータを移さなあかん。そんなんいけずやんかぁ。

また、検索したい対象は得意先名だけに限りません。よくいわれるのは着信時の電話番号の末尾四桁ですわ。それ以外にもフリガナやらなんやら。つまり複数の項目で検索したいというご要望が出てきますねん。
この時は、わては検索キーっちゅう項目を設け、複数の項目の値を連結させとります。
この時はスペースなしでがっちゃんこしてまうとうまく検索がでけへんようになるから、半角スペースなどを間に挟んで連結するとよろしい。

さらに、その時は文字数にも気ぃ付けましょう。コピー元のフィールドに指定できるのは文字列(1行)です。
こちらのヘルプにも書かれとるけど、文字列(1行)の文字数に制限はあらへん。
ところが、今のページにはこうも書かれとったよね。
値の重複の禁止を設定すると、入力できる文字数が全角または半角で64文字までに制限されます、てな感じ。なんでやねん。

では、値の重複の禁止を設定せんかったらええんちゃうん?そう思いたなるやん?え?ならへん? いや、なってぇや。
なぜなら、ヘルプには以下のように書かとるからやねん。
既存のレコードを更新する場合、CSVファイルを読み込んで一括更新する方法が便利やと思うですわ。
けど、ヘルプにはこうも書かれとるねん。

こないな制限があったら、もうあかん。更新がでけへんのですわ。
APIで更新する場合も同じですわ。
ここのリファレンスにも。
システムの移行には、データの更新が欠かせませんわな。CSVで更新する場合も、APIで更新する場合も。
ルックアップ項目を更新するためには、値の重複の禁止せなあきません。ちゅうことは、連結した文字列の文字数が64文字を越えんようにせんならんのです。つまり、移行元のシステムの項目の文字数を考慮にいれなあかんちゅうわけですわ。難儀やなぁ。

もう一つ、移行にあたって注意しておかなあかんことがあります。
それは住所データの扱いです。日本の住所の場合、以下の4つの項目からなっとります。

  • 都道府県
  • 郡市町村
  • 住所(町・字・地番)
  • 建物・マンション名

これが旧システムでも四つに分かれとったら問題ないんよ。
そやけど、住所1、住所2という感じで二つだけのフィールドでしか管理されていない場合、えらいこっちゃになります。

なんでかゆうたら、それを分割する作業が発生するからですわ。分割?そんなんせんでええわ。てゆわれたかて、後々の分析のこと考えたらそうもゆわれへん。正味、お客様に後々の分析のことを考えてと提案してみたら、kintoneでは分割して管理するお客様が多いんですわ。
例えば旧システムの住所1に「東京都中央区日本橋2−7−1」。住所2に「東京日本橋タワー」の値が入っていたとしますやろ。そしたら、住所1から「東京都」「中央区」「日本橋2-7-1」を分割せんならんことになります。さて、ぼんやったらどないする?

わてはそんなとき、VBA(Excelマクロ)を活用しますねん。
その中でこちらのサイトを参考に正規表現から分割しますねん。
この正規表現をVBAの中に仕込んだら、都道府県と郡市町村と住所(町・字・地番)を分割できるんよ。
以下に簡単やけどコードを掲示してみたから、よかったらみてみて。必ず本番では使う前にテストしたってぇやぁ。もちろん当方では一切の責任は負われへんから。

Sub ConvertSplitAddressData()
    Dim regExp              As Object           '正規表現オブジェクト
    Dim strPattern          As String           '正規表現パターン
    Dim lngRowCounter       As Long             '行カウンター
    Dim intRegMatchCount    As Object           '結果
    Const clngMaxRowCount   As Long = 65535     '行の末尾
    Const cintTargetColumn  As Integer = 18     '結合された住所列番号
    Const cintPrefColumn    As Integer = 14     '結果の都道府県を格納する列番号
    Const cintCityColumn    As Integer = 15     '結果の郡市町村を格納する列番号
    Const cintAddressColumn As Integer = 16     '結果の住所を格納する列番号
    Const cintAnotherColumn As Integer = 17     '結果の建物を格納する列番号

    Set regExp = CreateObject("VBScript.RegExp")
    strPattern = "(...??[都道府県])((?:旭川|伊達|石狩|盛岡|奥州|田村|南相馬|那須塩原|東村山|武蔵村山|羽村|十日町|上越|富山|野々市|大町|蒲郡|四日市|姫路|大和郡山|廿日市|下松|岩国|田川|大村)市|.+?郡(?:玉村|大町|.+?)[町村]|.+?市.+?区|.+?[市区町村])(.+)"
    With regExp
        .Pattern = strPattern
        .IgnoreCase = True
        .Global = True
        For lngRowCounter = 2 To clngMaxRowCount
            If ActiveSheet.Cells(lngRowCounter, cintAddressColumn).Value = "" Then
                Set intRegMatchCount = .Execute(ActiveSheet.Cells(lngRowCounter, cintTargetColumn))
                If intRegMatchCount.Count > 0 Then
                    ActiveSheet.Cells(lngRowCounter, cintPrefColumn).Value = Trim(intRegMatchCount(0).SubMatches(0))
                    ActiveSheet.Cells(lngRowCounter, cintCityColumn).Value = Trim(intRegMatchCount(0).SubMatches(1))
                    If InStr(intRegMatchCount(0).SubMatches(2), " ") > 0 Then                '建物以降が全角空白で分割されている場合
                        ActiveSheet.Cells(lngRowCounter, cintAddressColumn).Value = Trim(Mid(intRegMatchCount(0).SubMatches(2), 1, InStr(intRegMatchCount(0).SubMatches(2), " ")))
                        ActiveSheet.Cells(lngRowCounter, cintAnotherColumn).Value = Trim(Mid(intRegMatchCount(0).SubMatches(2), InStr(intRegMatchCount(0).SubMatches(2), " ") + 1, 100))
                    ElseIf InStr(intRegMatchCount(0).SubMatches(2), " ") > 0 Then             '建物以降が半角空白で分割されている場合
                        ActiveSheet.Cells(lngRowCounter, cintAddressColumn).Value = Trim(Mid(intRegMatchCount(0).SubMatches(2), 1, InStr(intRegMatchCount(0).SubMatches(2), " ")))
                        ActiveSheet.Cells(lngRowCounter, cintAnotherColumn).Value = Trim(Mid(intRegMatchCount(0).SubMatches(2), InStr(intRegMatchCount(0).SubMatches(2), " ") + 1, 100))
                    Else                                                                      '建物がないか続けて入力されている場合
                        ActiveSheet.Cells(lngRowCounter, cintAddressColumn).Value = Trim(intRegMatchCount(0).SubMatches(2))
                    End If
                End If
            End If
        Next lngRowCounter
    End With
    Set intRegMatchCount = Nothing
    Set regExp = Nothing
End Sub

データ移行の方法

  Topへ↑

kintoneにデータを取り込む方法はいくつかあります。そやけど、だいたいはCSVによる一括登録・更新か、APIでの登録/更新かcli-kintoneの三択ちゃうやろか。
わてはCSV経由で取り込むことがほとんどですわ。
なんでかいうたら、APIやとHTTP Client Tool for kintoneを使う場合でも、何かのプログラムで書く場合でも、コードのマッピングの記述が面倒になるからやねん。
あと、一リクエストでも100件しか登録や更新ができないAPIの仕様制限があるやんか。そやから、100万件をこえるデータを移行する場合、一日のAPIリクエスト回数制限にも引っかかってしまうんや。
cli-kintoneは便利やねんけど、上と同じくマッピングの部分が手間なのであまり使用しとりません。ただ、余談すると、Bashとcli-kintoneを使った効率化は追及せなあかんと思とります。そやから弊社としてはcli-kintoneを使ったツールの作成ははよ進めよ思てます。
ただ、それでも画面上でマッピングを確認できるcsv取込の方が安全ちゃうかと思います。ここはよぉ考えてみてください。その時、旧システムが出すcsvの見出し項目名とkintoneのフィールドラベルは合わせといたほうが移行マッピングが圧倒的に楽になるで。

先に挙げた住所や備考以外にも、データの加工はあちこちのフィールドで発生しますやろ。それらはめっちゃ面倒やと思うねん。それらをチェックとか加工とかせなあかんから。例えば下のような感じやね。

  • 項目の値の中に半角カンマは入っていないか。
  • 項目の値の中にダブルクォーテーションなどは入っていないか。
  • 項目の値の前後に空白は入っていないか。
  • 旧システムから出力されたファイルの文字コードは統一されているか。(複数のシステムからそれぞれ出力して取り込む場合、Shift-JISとUTF-8が混在しているとkintone側で文字化けの恐れあり)
  • ルックアップの値はkintoneの関連付けるアプリの参照アプリのコピー元のフィールドに等しいか。
  • 電話番号や郵便番号、メールアドレスやURLの書式は正しいか。
  • 日付書式は正しいか。(元データがyyyymmdd形式の場合、空白セルがあると空白に変換してくれずエラーになる等)
  • データの重複はないか。
  • kintoneのラジオボタン/チェックボックス/複数選択/ドロップダウンの選択肢に等しい値のみが含まれているか。

もしでけるんやったら、VBAマクロやcli-kintoneなんかでツールを作ったほうがええと思うで。
あと、ツールの作成と簡単に書いとるけど、旧システムによって項目がまちまちなんは、わかりますやろ?旧システムごとに工数と時間を使うから、わては移行ツールの作成には消極的で、あまりやってへんかったわ。

そやけど、毎度Excelの関数を駆使して移行データを作るんはもうしんどい。わても実は今年手掛けた数々の移行の中で、一つだけめっちゃ苦労した移行がありましてん。それをしおに、めんどいかもしれんけど、移行ツールを作ったほうがええなぁと痛感しましたわ。

ただし、早めにマスタの項目を固め、マスタアプリを作り、初回のデータ移行がでけたところで、それ以降は本番までお客様にマスタデータの入力をしてくれまへんやろか?とお願いしたほうがよろし。もしそれがでけたらマスタの移行ツールはいらんからね。
ま、それでもトランザクションデータの移行に関しては何かしら作らんといかんけどね。

なんでかいうたら、マスタのデータは生き物やねん。お客様によってガーっと追加され、あちこちでバァーっと更新されますやろ。
しかも住所の更新が起こるし、しかも項目が連結されている住所データやったら、さっき書いたみたいに分割の作業が毎度いるんでっせ?
それに、本番移行直前でせーので一回でガバっと取り込めば済むほどシステム移行は甘いわけやあらへん。

もちろん、どっかの時点で初回分を取り込み、定期的に差分データを取り込み、最後に移行日までの残り差分がきれいに取り込めた場合は楽ですわ。そやけど、そうした移行の運用ができるのは、こっち側が旧システムにアクセスできる場合だけやねん。

遠方のお客様で、しかもVPNでつながれへんような旧システムの場合、データの取り出しはお客様にご依頼するほかありませんやろ。
お客様のやり方によっては項目に抜け漏れがあるやろし、項目の順番が違っていたりします。ましてや差分データの時間の基準もあいまいになってしまいますねん。そやから、きれいなデータを毎度もらえないと考えなあきません。
そやから、移行ツールを作らんでええ場合は、旧システムにこちらから簡単にアクセスできる場合に限ったほうがええね。もしそないにアクセス出来るんやったら、毎回Excelのフィルターや並び替えや置換などを駆使しても円滑に移行できると思うけどね。
ただ、実際はそうでない場合が多いから、そないなリスクを考えたら、最初に工数と時間をかけてでもお客様専用の移行ツールを作り、それを運用したほうが格段に楽やと思うんやけどどない?

データ移行のTIPS

  Topへ↑

なんぼかTIPSを列挙しておきますわ。

  • データの本番移行までは、取り込み時には変更履歴はオフにしといたほうがええで。
    設定は、アプリの設定→高度な設定→変更履歴のチェックを外す。
    そうしないとサイボウズさんに注意されまっさかい。また、変更履歴だけで契約のディスク使用量を軽く超過してしまいますねん。(一度、うっかりしていて100万件のデータを何度も取り込みなおし、サイボウズさんに注意されてもうた。※EvaCamp 2020で言いそびれた失敗談の一つ)
  • 可能であれば、本番移行後は旧システムのトランザクションデータ(売上や仕入などの伝票データ)は、kintoneでも別アプリにした方がええで。
    しかも、旧システムのトランザクションデータのフィールドはルックアップをなくし、文字列(1行)と数値のみにしといたほうが楽やで。なんでかいうたら、過去分のデータはマスタデータの変更などで古い値として入っとるからやねん。過去のルックアップはその時のマスタデータの値を再現して保存しといて、てな具合にご要望もろたら、移行作業はめっちゃごっつい苦行になってまう。さっきも書いたけど、ルックアップのコピー元のフィールドはマスタにある値やなかったらエラーになるからね。そやから、ここは初めのころに決めておいた方がええかも。
    旧システムの部分については別アプリに分け、ルックアップを外してしまえば、旧システムの生データをそのまま取り込むだけ済むさかい、ごっつい楽ですわ。
    そやけど、一つだけ注意しとかな。アプリを分けた場合、新旧両方のアプリをまたいだ分析に対応できませんやろ。その場合は、自分でカスタマイズビューを作成するか、トヨクモさんのDataCollectのようなプラグインを活用するとよろし。便利でっせー。
  • 添付ファイルの移行が必要な場合は先にも挙げたcli-kintoneを使うとええで。また、要件定義の段階でDropboxやboxなどのオンラインストレージの移行を提案しとったら、kintoneではそちらのストレージとの連携を行えばええから、あんじょういきまっせ。
    オンラインストレージへのデータ移行と整理作業は、お客様にお願いしといたほうがええやろし。
    2019年のわてのAdvent Calendarもご参考にしたってやー。
  • C/S側の旧システムは、たいがいレイアウトが小さくまとまっとるやろ。それに比べるとkintoneのフラットデザインは、項目の間の余白もがっつりとられとるし、そこは一目で情報が見られる旧システムに劣ると思われるかもしれへん。
    無理やりJavaScriptでレイアウトを調整したったりもしたけど、それはあんまり本筋のやり方ちゃうしなぁ。
    これも早い段階で現場の担当者と話を詰めといたほうがええで。
    あ、もう一個。デザインについてはkintoneのアプリの設定→デザインテーマの設定でブラックを選ぶとお客様の印象が変わるかもしれんで。一度試すとよろしおま。
  • Excel上でデータの加工を行う時、何使てます?フィルタ? もしそやったら、スライサーの機能とか試すと幸せになれるで。
    スライサーはこの記事が参考になるんちゃうかな。
  • kintoneの難儀な仕様ってあるやんか。例えば英単語の一部分の文字だけで検索でけへんとか。
    ヘルプにもこない書かれとるし。
    もしそれやったら、Excelのフィルタ使たらええねん。まあJavaScriptで
    一文字検索の機能を作ってもええし、プラグインでもええのんあるけどね。
  • C/S側の旧システムのユニークの項目は早めにお客様と共通認識をもっといたほうがええ。さっきも書いたけど、お客様からデータをもらう際は、旧システムのデータ出力方法を把握して、その設定や出力操作は完全に共通認識をもっといたほうがええ。あと、これも上で書いたけど、旧システムのデータを先に修正できるのならお客様に頼んで修正しといたほうがええ。

まとめ

Topへ↑

ちゅうわけで、本記事は皆様のkintoneへの移行のお役に、そしてご参考になればよろし思ぅて書きました。もちろん、これは弊社にとっても参考にすべき自社のノウハウですわ。
弊社も今年は一件、移行で苦労した案件がありましてん。だからこそ、一度ノウハウを言語化すべきやなぁと思ったんよ。それがきっかけです。
また、弊社の新規の案件の際にもお客様にも読んでもらお思ぅて、ここで知見を共有したいと思います。
なので、今後も折をみてアップデートを重ねていこうと思います。よろしゅうお願いいたします
また、こんなTipsや失敗談などありましたら、ガァーっとご意見をお寄せください。また本稿の内容に不備があった場合も遠慮せんとご指摘ください。

本稿が旧システムからkintoneへの移行をお考えの皆様にとってちょっとでも手助けになったら幸せやわ。


弊社はCYBOZU DAYS 2020に出展いたします。


9月が始まった今日、
弊社にとって三つのニュースを発表できる機会が到来しました。

一つ目は、
CYBOZU DAYS 2020への出展です。
2020年11月に千葉の幕張メッセで開催されるCYBOZU DAYS 2020のシルバースポンサーとしてブースを出展いたします。
弊社とPolaris Infotech株式会社(https://www.polarit.co/)との共同出展の形をとらせて頂きます。

URL:
https://cybozuconf.com/
日時:
2020/11/11-2020/11/13
場所:
幕張メッセ 国際展示場1-3ホール
(弊社の出展ブースの場所はすでに決まっていますが、詳細は当日お越しになられた際に配布される地図をご覧くださいませ)

来場方法:
JR京葉線 – 海浜幕張駅 (東京駅から約30分、蘇我駅から約12分)から徒歩約5分
JR総武線・京成線 – 幕張本郷駅(秋葉原駅から約40分)から「幕張メッセ中央」行きバスで、約17分
出展内容:
お楽しみに

Cybozu Daysは、毎年大勢のお客様がお越しになります。今年はコロナ禍のため、感染対策の中で開催されます。
若干の制約事項もあるでしょうが、来場された方は必ず楽しめますし、持ち帰って糧としていただけるだけの内容になっています。これは毎年参加している代表が心から実感していることです。
是非、来場のお申込みをお待ちしております。

二つ目は、
弊社の代表がインタビューを受けた記事がkintone エバンジェリストのサイトで公開されました。
https://www.kintone-eva.com/
代表は今までも何度かインタビューされた経験や、記事に登場したことがあります。
ですが、今回のインタビューは、kintoneを通した代表自身の生き方にまで踏み込みましたので、とても思い入れがあります。

こちらもお読みいただければと思います。

最後は、
弊社のロゴの刷新です。
akbを組み合わせた従来のロゴの色合いをkintoneに近づけたものです。
このデザインは弊社代表の妻と長女によるものです。

長女も現在、デザイン専門学校生ですが、弊社の社名のakbの文字を組み合わせつつ、同時に弊社の代表の頭文字「N」を表したデザインは、デザインやプレゼンテーションのプロの方からもおほめ頂きました。

よりkintoneにデザインを近づけたことで、kintoneに賭けようとする弊社の意気込みを感じて頂ければと思います。

まだ発表していませんが、この夏には
・社是
・企業理念
・経営理念
・9つ(ナイン)のない
・アクアビットに合わない方
という五つの指針も作成しています。

また機会があればご紹介したいと思います。

今後ともよろしくお願いいたします。


ワーキングツリーにはkintoneとboxをお飾り!


kintone Advent Calendar 2019の24日目の記事です。

  Topへ↓

ん?この著者、この間もAdvent Calendarでみたで? はい。二度目の登場です。

kintoneは優れたツールですが完全ではない?
なーんてディスられても動じず、欠点を正直に認めるのもkintoneの憎めないところ。そう思ってやまない著者です。

kintoneの欠点のいくつかはすぐに挙げられます。
例えばブラウザーベースで動いているので、ファイルアップロードの作業が面倒、とか。
添付ファイルフィールドに画像データを放り込みまくると、一ユーザーあたり5GBの容量の制限が足かせになってくる、とか。

そんな限界を解消するためのささやかなクリスマスプレゼントを皆様にお届けしたいと思います。
なに、ちょっとした贈り物です。クリスマスツリーによくぶら下がっている箱のような。
箱・・・つまりboxです。
今回の記事では私の2019年の失敗事例も公開しているので、ひょっとしたら皆さまのご参考になるかもしれません。

box for kintoneのご紹介

  Topへ↑

世の中にオンラインストレージ製品はたくさんありますよね。その中でもboxが存在感を出しているのはご存じでしょうか。

kintoneとboxの連携はbox for kintoneというプラグインとして公開されています。
それを使えばブラウザー上でアップロードなどせず、ドラッグ&ドロップでブラウザー上から操作できちゃうのです。

クライアントツールのbox Driveをインストールすれば、Windowsのエクスプローラと同じ操作でbox上にファイルをアップロードできてしまう。なんて優れもの。

boxを使うと無尽蔵(契約プランによる)を誇る容量にファイルを置きまくり。
それをkintoneの画面上からに自由に呼び出せる。素晴らしい!
上に書いたkintoneの弱点を周囲のツールが補ってくれるいい例です。
本稿もそうしたkintoneを補ってくれる一つの例としてお役に立てれば幸いです。お日柄もよいので。

box for kintoneの使い方は、Cybozu developer networkにも出ています。
https://developer.cybozu.io/hc/ja/articles/205070124-Box-for-kintone その記事に従えば、簡単にbox for kintoneを導入できるはずです。

ところが、この記事で書かれているのは、一つのレコードに一つのフォルダーを対応させるところまで。
kintoneでちょっとしたシステムを作ろうとすれば複数アプリにまたがった構成が必要です。それに応じてboxのフォルダー構成も複数の階層にまたがってしまいます。

合同会社アクアビットダム設計なる会社

  Topへ↑

たとえば、大阪と東京に支店がある合同会社アクアビットダム設計があったとします。
この会社はダムを独自の技術で製造し、お客様にお納めしている設定です。

受注システムをkintoneで構築するにあたり、
大阪支店[組織]の
長井何某[個人]が担当する
担当案件[案件]の
施行状況[施工]と
湛水状況[湛水]を管理すると仮にしましょう(工程はしょりすぎ。ちなみに最後の工程は水を貯める工程です)。

他に顧客マスタがあるでしょうがここは割愛。また、[組織]と[個人]はアプリではなく、kintoneのアカウントを使用する想定です。
この場合3アプリですね。

ここでご注文からの流れをkintoneで管理したとしましょう。
各アプリの連動はkintoneのアクション機能を使ったとします。

ダム完成までにはさまざまな状況を報告していかねばなりません。するとダムの進捗に合わせて写真が大量に溜まっていきます。
kintoneの添付ファイルフィールドにファイルをアップしていると、すぐに容量が危うくなりかねません。
ここでboxの出番です。

ここでboxで写真を管理しようとした場合、box内のフォルダー構成はこのようになると思います。

さて、先ほどご紹介したbox for kintoneを思い出してみましょう。
プラグイン設定画面にルートフォルダーのIDを設定していましたね。

つまり、アプリ自体にルートフォルダーのみを作る仕様。
それって、どのレコードであろうと共通で1つのフォルダーだけ、、、
いやいや多層boxと多層アプリでは対応できないのはちょっと、、、
結論! box for kintoneだとちょっとキツイかも。

合同会社アクアビットダム設計にboxを

  Topへ↑

ということで、本稿では多段階にわたるboxの連動例をお伝えしたいと思います。また、その時にしでかしてしまった失敗と、そのリカバリ例もお伝えしたいと思います。

まず、話を簡単にするため、合同会社アクアビットダム設計としてのルートフォルダーを設定しておきましょう。

さらに、支店ごとにフォルダーを設定し、支店の配下に担当ごとのフォルダーも生成しておくと話が早いですね。

実際のboxのフォルダー構成はこんな感じ。

ここでルートフォルダーのフォルダーIDを取得しておきます。boxの画面から取れます。

その状態で、案件アプリに新規レコードを登録します。
案件アプリの項目には案件の主管支店と、案件の主担当を指定するフィールドも忘れずに。もちろん必須項目として。

なぜ必須項目にするのでしょう。
その理由は、レコードが保存成功後、案件フォルダーを作る際にどこのフォルダーの配下に作成するか決めなければならないためです。

boxのフォルダー生成APIについて

  Topへ↑

ここでboxの仕様を押さえておきましょうか。
サービスの仕様を確認するには、APIから逆引きしたほうが理解しやすい。いわゆる技術者あるあるです。
boxのAPIはこちらのサイトをご覧になると良いでしょう。
https://ja.developer.box.com ・・developerサイトトップ
https://ja.developer.box.com/reference ・・APIレファレンス

boxのフォルダー作成の項を読むと、親であるフォルダーのIDがパラメーターとして必須のようです。
編集時には親フォルダーのIDは必須ではなくなりますが、もしフォルダーの場所を移動する際は親フォルダーのIDは指定しなければなりません。

案件フォルダーを作る際は、親となる担当者フォルダーのIDを把握しておかねばなりません。
そしてその上の支店フォルダーも。

つまり、案件レコードの保存のタイミングで行うべきことは、まず、そのレコードの支店フィールドの値に等しい支店フォルダーを検索することです。
その際、基準となるのはルートフォルダーです。

ルートフォルダーの下にある支店フォルダーを検索し、そのIDを特定します。
間髪入れずに支店フォルダーの配下にある担当者フォルダーを検索します。
これは同一担当者が複数支店にフォルダーを持っている場合など、運用も考慮していますが、支店フォルダーのIDを内部で保持できるのであれば、いきなり担当者フォルダーから検索してもよいです。
重要なのは案件フォルダーを作成するにはその親フォルダーのIDを事前に必ず保持しておくことです。

先ほど、kintoneの案件アプリの支店と担当者の両フィールドは必須でなければならないとしたのには、そういう理由があったのです。
これら二つのフィールドの値がないと、案件を保存する際に生成されるべき案件フォルダーの保存先が迷子になってしまうので。

JavaScriptで実装してみた

  Topへ↑

続いてはいよいよboxの操作を行います。

その前に本稿ではboxの権限周りには踏み込まないことを言っておきます。
OAuthについては、もともとbox  for  kintoneで用意されていたclient IDを使用します。本当はbox内でアプリを作成し、そのアプリ内で設定した権限を認証しなければならないのですが。
box for kintoneに甘えてしまいましょう。

ついでにpromise処理が考慮されたAPI実行部分もbox for kintoneの処理を流用させていただきましょう。

処理の大まかな順序としては以下の通りです。
ただ、のちに述べますが、このコードは動きません。なのでコードは画像として参考程度に載せます。

まず、イベントが動くタイミングはapp.record.create.submit.successです。新規作成処理成功後ですね。

処理の都合上、この中で別のアプリに更新を行い、その結果が成功した場合にboxフォルダー生成処理を呼び出しています。

boxフォルダー生成処理では、まずルートフォルダーから支店フォルダーを検索します。
続いて支店フォルダーから担当者フォルダーを検索します。

ここでboxの検索の仕様が立ちふさがってきます。
boxの検索仕様として、対象の種類、生成時刻、オーナーIDや親フォルダーIDなどは指定できるのですが、肝心の文字列を完全一致で検索できないのです。queryというパラメータがあるにもかかわらず、そこに指定した文字列は曖昧検索として処理されてしまうのです。
APIレファレンス

つまり、親フォルダーに属する検索対象が複数ありうる場合、検索文字列に工夫が必要です。例えば姓名の間にスペースが入る場合など。
「長井 権兵衛」と「長井 主水」が対象のフォルダー配下にあって「長井 権兵衛」を検索したい場合、queryに「長井 権兵衛」を設定してもマッチしません。
ではどうやればよいか。
スペースの前後の文字列で検索するのです。
この場合、「長井」または「権兵衛」で検索します。すると前者は二件がヒットし、後者は一件がヒットします。
その結果を再度ループして回し、一件ごとにname属性の値が検索文字列に一致するかを確認する。
そのような面倒な処理がboxの検索には必要です。

このコードも実際は使っていませんが、軽く提示します。

これで、担当者フォルダーIDまで求められました。

boxのフォルダー生成と検索にまつわる問題

  Topへ↑

続いてはフォルダーの生成に移りましょう!
APIレファレンス

生成にあたっては、名前と親フォルダーのIDを指定するだけです。

これで、案件レコードが保存されたら案件フォルダーを作成するところまでができました。

ここで当初想定していたboxの構成を見てみましょう。
案件フォルダーの配下に「提案状況」「施工状況」「湛水状況」の三フォルダーがあります。
このフォルダーの生成にも実は厄介な問題が潜んでいます。

例えば、案件のレコードが保存されました。そして案件フォルダーが生成されました。
そしたら、アクション機能によって施工状況アプリにレコードをコピーし、施工状況アプリでもレコードが保存された瞬間、案件レコードの時と同じように施工状況フォルダーを生成すればええんちゃうの?と思ったでしょう。

ところが、フォルダーを生成するには親のフォルダーの指定が必須です。
親フォルダー、つまり案件フォルダーをフォルダー生成処理の直前で検索してフォルダーIDを取得しなければなりません。
ところがこの親フォルダーの検索取得にはひとつハードルが控えています。そのハードルとは、コンテンツが生成されてから検索可能となるまでに時間がかかる、というものです。
boxはなんらかのコンテンツが作成されてから、それが検索可能となるまでにbox内部でindexを構築しており、それに時間が掛かるのです。
boxの APIレファレンスには以下のように書かれています。

つまり、案件フォルダーの生成からすぐ、施工状況アプリのレコードを保存した場合、親となるべきフォルダーが検索できないため、親フォルダーの指定ができないのです。

boxを多層構造でkintoneと連動させる場合、この仕様上の制限は現状では避けられません。

この制限を回避するため、発想を切り替えました。
つまり、案件フォルダーが生成された後、同時に配下のフォルダーも作ってしまうのです。

box APIでは、フォルダー生成が成功した時点で返り値として生成されたフォルダーのIDが得られます。このIDを使えば配下のフォルダーも即時に生成できます。

このコードも実際は使っていませんが、軽く提示します。

この下の処理ではさらに生成した案件フォルダーのURLを取得し、そのURLやフォルダーIDをkintoneの2アプリに更新して設定しています。

ここまででkintoneのapp.record.create.submit.successイベントを見てきました。その結果、実装ができそうです。
テストでもフォルダーが意図通りに生成されました。開発用のPCでも、お客様のご担当者様のPCでも。

バグ大魔王降臨!

  Topへ↑

ところが! やったと思った安心のかげに潜むのが落とし穴。バグが出てしまったのです。
テストではうまく動いていたのに、いざ本番になるとうまくいかない。なんということでしょう!

実は、その根本的な原因は今もなお究明できていません。
事象としてはboxにAPIリクエストを投げた後、何も戻ってこないのにプログラムが終了してしまいます。httpレスポンスすら帰ってこずに。
それも終了する場所がまちまちなのが始末が悪い。複雑なPromiseの構造に加え、referredを混在させたことにも問題があったのかもしれません。
この不具合がやっかいなのは、boxからのレスポンスを待つ間、app.record.create.submit.successの結果が完了できないことにあります。その間、ブラウザーは固まってしまい、kintoneを利用されている皆様にはただ困惑が。

そして、この不具合の原因がブラウザーにあるのか、box側にあるのか、kintone内部にあるのか、それともPCのスペックにあるのか。はたまたネットワーク環境によるものなのか。いまだに分っていません。
ただ、ブラウザー上でレスポンスを待つ運用はまずい、という悔いだけは骨身に沁みました。
私はその原因を追究するよりもお客様の運用を円滑に進めることを優先しました。
その決断として、ブラウザーに依存する実装を止めました。

AWSへ処理を移管

  Topへ↑

では、どうすればよいか。

幸いなことにkintoneにはWebhookという機能が備わっています。Webhookには、レコード保存時にWebhookのリクエストを受け付けてくれるWebhook URLを設定できます。

私がWebhook URLとして設定したのはAWSのAPI Gatewayで設定したURLでした。
API Gatewayについての説明は割愛しますが、kintoneから受け取ったWebhookのリクエストに含まれるJSONを読み取り、それを後続の処理に渡すことができます。
後続の処理にはAWS Lambdaを選びましたので、同じAWS上で処理が連携できます。

AWS LambdaではNode.jsを使い、ほぼkintoneのkintone.app.create.submit.successで実装したのに近いコーディングを行いました。
box Node SDKがAWS Lambdaから簡単に使用できるので、それを使えば似たような実装ができるのです。
ただし、boxのアプリは一から作る必要があります。設定もあれこれ行う必要が生じました。
最初、こちらのブログの力も借りました。ありがとうございました。
Lambda関数のコードを以下に掲示します。なお、このコードは動いているものを基にいろいろといじっているので参考になると思います。

/**
 * This sample demonstrates how to call Box APIs from a Lambda function using the Box Node SDK.
 *
 * For step-by-step instructions on how to create and authorize a Box application,
 * see https://github.com/box/samples/tree/master/box-node-lambda-sample.
 */
const BoxSDK = require('box-node-sdk');                                                // Node.jsのbox-node-sdkモジュールを呼び出す
const request = require('request');                                                    // Node.jsのrequestモジュールを呼び出す
const boxConfig = JSON.parse(process.env.BOX_CONFIG);                                  // AWS Lambdaの環境変数のBOX_CONFIGの値をJSONで扱えるように

boxConfig.boxAppSettings.appAuth.keyID = boxConfig.boxAppSettings.appAuth.publicKeyID; // 9行目で取り出したkeyIDにpublicKeyIDを代入

const sdk = new BoxSDK(boxConfig.boxAppSettings);                                      // 9行目で取り出したboxAppSettingsをsdkに代入

/**
 * Create a service account client that performs actions in the context of the specified
 * enterprise.  The app has a unique service account in each enterprise that authorizes the app.
 * The service account contains any app-specific content for that enterprise.
 * Depending on the scopes selected, it can also create and manage app users or managed users
 * in that enterprise.
 *
 * The client will automatically create and refresh the service account access token, as needed.
 */
const client = sdk.getAppAuthClient('enterprise', boxConfig.enterpriseID);             // boxアプリが適用できるアカウントのグローバル設定を管理

var DOMAIN = '*********.cybozu.com'; //kintone環境のドメイン                            // *****はご使用のkintoneのサブドメインを
var APP_ID_1287 = 1287;   //案件管理アプリのアプリID
var BASE_URL = "https://" + DOMAIN + '/k/v1/';
var APITOKEN_1287 =  "kintoonkaramottekitatookunwokokoniiretene";                      // kintoneの案件アプリのAPIトークン
var headers_1287 = {'X-Cybozu-API-Token': APITOKEN_1287};                              // リクエストで使用するヘッダ
var FolderId;
var updaterecordid_1287;

exports.handler = (event, context, callback) => {                                      // eventはkintoneのWebhookからAPI Gatewayを経由したレコード情報
                                                                                       // contextはLambda関数に関する情報
    const API_BASE_PATH = 'https://api.box.com/2.0';                                   // box Node SDKの文法に準拠

    // targetnameはコンテンツの文字列
    // typeはコンテンツの対象。本稿の場合はfolder
    // content_typesは検索対象とするプロパティ。本稿の場合はname
    // limitは検索結果として戻す件数。
    // idsは親フォルダーのフォルダーID
    // methodは本稿では全てGETなので使用していない
    // dataは本稿の場合検索対象(支店,担当者,案件No)のうち、担当者の場合["担当名"]で渡ってくる。
    function searchFolder(targetname, type, content_types, limit, ids, method, data, success, error) {  //157,159,161行目から呼び出されて検索処理を実施
        if (data !== undefined) {                                 // dataが指定されている場合
            if (data[0] === "担当名") {                            // dataの配列の最初の要素が"担当名"の場合
                targetname = targetname.split(' ')[0];           // 受け取るtargetnameは「長井 権兵衛」の様に全角スペースで区切られた姓名なので姓を取得
            }
        }

        return new Promise(function (resolve, reject) {           // Promiseを設定
            client.search.query(                                  // 24行目でclientとして承認されたbox Node SDKのsearchクラスのquery関数を呼び出し
                "\"" + targetname + "\"",                         // 最初のパラメーターは検索対象文字列。文字列なのでエスケープした""で囲む。でも曖昧検索
                {                                                 
                    fields: 'id,name,modified_at,extension,permissions,collections',  //検索結果として返すコンテンツのプロパティ
                    type: type,                                                       //folder
                    content_types: content_types,                                     //検索対象はnameプロパティ
                    limit: limit,                                                     //結果として返す件数
                    ancestor_folder_ids: ids,                                         //親フォルダーID 
                    offset: 0                                                         //オフセットしないので0
                })
                .then(function(results){                                              //結果が取得されたのでこのPromiseチェーンへ
                    if (data !== undefined) {                                         //dataが指定されている場合
                        if (data[0] === "担当名") {                                   //dataの配列の最初の要素が"担当名"の場合
                            for (var i = 0; i < results.entries.length; i++) {        //戻り値の件数分(limitで指定した件数分)
                                if (results.entries[i].name === data[1]) {            //戻り値のnameプロパティがdataの2番目の要素(担当名)か
                                    resolve(results.entries[i].id);                   //Promiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                                }
                            }
                        }
                    } else {
                        resolve(results.entries[0].id);                               //Promiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                    }
                })
                .catch(function(error){ // エラーの場合
                    reject(error);
                });
        });
    }

    // createParamは生成フォルダー名と親フォルダーIDが含まれたJSONオブジェクト
    function postFolder(createParam) {                     //176行目から呼び出されてフォルダー生成処理を実施
        return new Promise(function (resolve, reject) {    // Promiseを設定
            client.folders.create(createParam.parent.id, createParam.name)  //24行目でclientとして承認されたbox Node SDKのfolderクラスのcreate関数を呼出
                                                                            //1つ目は親フォルダーID、2つ目は生成するフォルダーの名称 
                .then(function(results){                                    //85行目の処理が成功したのでこのPromiseチェーンへ
                    var ankenid = results.id;                               //生成したフォルダーIDを以下のforeach内で使うためにankenidに代入 
                    var subfolders = [                                      //生成した案件フォルダーの配下に作成する三つのフォルダー名を配列にしています
                        "提案資料",
                        "施工状況",
                        "湛水状況"
                    ];

                    var promiseset = [];                                    //三つのフォルダーの生成が終わるまで待つPromiseを三つ作るので配列を設定
                    subfolders.forEach(function(val,index,ar){              //89行目で生成した配列の各要素をループします
                        promiseset[index] = new Promise( function( resolve, reject ) {  //95行目で生成した配列にPromiseを設定します。
                            client.folders.create(ankenid, val)             //24行目でclientとして承認されたbox Node SDKのfolderクラスのcreate関数を呼出 
                                                                            //1つ目は親フォルダーID(案件フォルダー)、2つ目は生成するサブフォルダーの名称
                                .then(function(results){                    //98行目の処理が成功したのでこのPromiseチェーンへ
                                    resolve(results.id);                    //97行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                                }).catch(function(error){                   //98行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                          //97行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                        });
                    });
                    Promise.all( promiseset )                               //97行目で設定した三つのPromiseが全て完了したらここに来る
                        .then( function ( message ) {
                        resolve(ankenid);                                   //84行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                    })
                        .catch( function ( reason ) {                       //97行目で設定した三つのPromiseのどれかが失敗したらここに来る
                            console.log( reason ) ; // "失敗!!"
                        reject(false);                                      //84行目のPromiseは失敗したとエラーオブジェクトを返す
                    });
                })
                .catch(function(error){                                     //85行目の処理は失敗したらここに来る
                    reject(error);                                          //84行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }

    // boxフォルダーIDは更新対象となるフォルダーID
    // createParamは更新フォルダー名と親フォルダーIDが含まれたJSONオブジェクト
    function putFolder(boxフォルダーID, updateParam) {             //165行目から呼び出されてフォルダー更新処理を実施
        return new Promise(function (resolve, reject) {         // Promiseを設定
            client.folders.update(boxフォルダーID, updateParam)    //24行目でclientとして承認されたbox Node SDKのfolderクラスのupdate関数を呼出
                                                                //1つ目は対象となるフォルダーID、2つ目は更新するフォルダー情報の含まれたJSONオブジェクト 
                .then(function(results){                        //126行目の処理が成功したのでこのPromiseチェーンへ
                    resolve(results.id);                        //125行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                })
                .catch(function(error){                         //126行目の処理が失敗したのでこのPromiseチェーンへ
                    reject(error);                              //125行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }

    function getFolderURL(createdid) {                          //178行目から呼び出されてフォルダーの共有処理を実施
        return new Promise(function (resolve, reject) {         // Promiseを設定
            client.folders.update(createdid, {shared_link: client.accessLevels.OPEN}) 
                                                                //24行目でclientとして承認されたbox Node SDKのfolderクラスのupdate関数を呼出
                                                                //1つ目は対象となるフォルダーID、2つ目は更新するフォルダーのプロパティ(共有設定)
                .then(function(results){                        //139行目の処理が成功したのでこのPromiseチェーンへ
                    resolve(results.shared_link.url);           //138行目のPromiseは完了したと戻り値の共有URLプロパティ(リンクURL)を返す
                })
                .catch(function(error){                         //139行目の処理が失敗したのでこのPromiseチェーンへ
                    reject(error);                              //138行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }
    function createBoxFolder(支店, 担当者, 案件No, boxフォルダーID) {  //212行目から呼び出されてフォルダーの共有処理を実施
        return new Promise(function (resolve,reject) {             // Promiseを設定
            var rootfolder = "12345678910";                        //boxのフォルダー制御のルートとなるフォルダーのフォルダーIDを静的に代入
            var ownerbranchfolder;                                 //支店フォルダーのフォルダーID
            var personinchargefolder;                              //担当者フォルダーのフォルダーID
            var createParam;                                       //searchFolder関数へはダミーオブジェクト。putfolderとpostfolderへはJSONオブジェクト

            searchFolder(支店, "folder", "name", 10, rootfolder, 'GET', createParam).then(function (branchfolderid) {  //45行目へ
                ownerbranchfolder = branchfolderid;                           //searchFolderからの返り値を上位スコープのownerbranchfolderへ代入
                searchFolder(担当者, "folder", "name", 10, ownerbranchfolder, 'GET', ["担当名",担当者]).then(function (personfolderid) {  //45行目へ
                    personinchargefolder = personfolderid;                    //searchFolderからの返り値を上位スコープのpersoninchargefolderへ代入
                    searchFolder(案件No, "folder", "name", 10, personinchargefolder, 'GET', createParam).then(function (projectfolderid) {  //45行目へ
                        var name = "案件No" + " " + 案件No;                    //生成/更新する案件フォルダーの名称を設定する
                        createParam = {name: name, parent: {id: personinchargefolder}};  //案件フォルダーの設定情報をJSONオブジェクトに組み立てる
                        if (projectfolderid.length > 0) {                     //161行目で案件フォルダーが存在した場合(フォルダー情報更新)
                            putFolder(projectfolderid, createParam).then(function (updatedid) {  //124行目へ
                                FolderId = updatedid;                         //161行目の処理で得た更新したフォルダーIDをスコープ外の168行で使うため
                                getFolderURL(updatedid).then(function (updatedurl) {             //137行目へ
                                    resolve(FolderId+"****"+updatedurl);      //151行目のPromise完了をフォルダーIDと共有URLプロパティ(リンクURL)で返す
                                }).catch(function(error){                     //167行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                            //151行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                            }).catch(function(error){                         //165行目の処理が失敗したのでこのPromiseチェーンへ
                                reject(error);                                //151行目のPromiseは失敗したとエラーオブジェクトを返す
                            });
                        } else {
                            postFolder(createParam).then(function (createdid) {       //83行目へ
                                FolderId = createdid;                                 //生成したフォルダーIDを以下の179行目で使うためにFolderIdに代入
                                getFolderURL(createdid).then(function (createdurl) {  //137行目へ
                                    resolve(FolderId+"****"+createdurl);      //151行目のPromise完了をフォルダーIDと共有URLプロパティ(リンクURL)で返す
                                }).catch(function(error){                             //178行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                                    //151行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                            }, function(res) {                                        //176行目のフォルダー生成処理でrejectレスポンスが返った場合
                                if (res.status && res.status === 409) {               //176行目のフォルダー生成処理でrejectレスポンスが409返った場合
                                    if (res.context_info                              //176行目のフォルダー生成処理でrejectレスポンスが競合を示した場合
                                        && res.context_info.conflicts
                                        && res.context_info.conflicts.length > 0) {
                                        return;                                       //150行目のcreateBoxFolder関数を終える
                                    }
                                }
                            }).catch(function(error){                                 //176行目のフォルダー生成処理でエラーが帰った場合
                                reject(error);                                        //151行目のPromiseは失敗したとエラーオブジェクトを返す
                            });
                        }
                    }).catch(function (error) {                                       //161行目のフォルダー検索処理でエラーが帰った場合
                        // 非同期処理失敗。呼ばれない
                        console.log(error);
                    });
                }).catch(function (error) {                                           //159行目のフォルダー検索処理でエラーが帰った場合
                    // 非同期処理失敗。呼ばれない
                    console.log(error);
                });
            }).catch(function (error) {                                               //157行目のフォルダー検索処理でエラーが帰った場合
                // 非同期処理失敗。呼ばれない
                console.log(error);
            });
        });
    }

    var recordjson = JSON.parse(event.body);                                 //34行目で受け取ったkintoneのWebhookのレコード情報をJSON形式で扱えるように
    updaterecordid_1287 = recordjson.record.レコード番号.value;               //210行目のレコードデータの「レコード番号」フィールドの値を代入
    createBoxFolder(recordjson.record.支店.value[0].code,                    //150行目へ
                    recordjson.record.担当者.value, 
                    recordjson.record.案件No.value, 
                    recordjson.record.boxフォルダーID.value).then(function(idurl) {
        if (idurl) {                               //212行目のcreateBoxFolderの戻り値(168、179行目で値設定)
            var targetrecordids = [updaterecordid_1287+"**"+APP_ID_1287];   //211行目で設定したレコード番号と27行目で設定したアプリID
            var kintonepromiseset = [];                                     //kintoneのレコードアップデートが終わるまで待つPromiseの配列を設定
            targetrecordids.forEach(function(val,index,ar){                 //217行目で生成した配列の各要素(本稿では1つ)をループします
                kintonepromiseset[index] = new Promise( function( resolve, reject ) {  //218行目で生成した配列にPromiseを設定します。
                    var body_post = {                                                  //kintoneの既存案件アプリを更新するレコードを組み立てます。
                        app: val.split("**")[1],                                       //217行目で設定した配列の**で区切られた右側(アプリID)
                        id: val.split("**")[0],                                        //217行目で設定した配列の**で区切られた左側(レコード番号)
                        record: {
                            boxフォルダーID: {
                                value: idurl.split("****")[0]                       //212行目のcreateBoxFolderの戻り値の****で区切られた左のフォルダーID
                            },
                            表示: {
                                value: idurl.split("****")[1].replace("*******.box.com","app.box.com")
                                         //212行目のcreateBoxFolderの戻り値の****で区切られた右のURL(契約のboxのサブドメインをapp.box.comに置換の必要あり)
                            }
                        }
                    };
                    var options_getsalesamount = {                            //リクエストのbody部分を組み立てます。
                        url: BASE_URL + 'record.json',                        //28行目で設定したURLのルートと一行レコードの更新なのでrecord.jsonを連結
                        method: 'PUT',                                        //更新なのでPUT
                        headers: headers_1287,                                //30行目で設定したAPIトークン
                        'Content-Type': 'application/json',                   //リクエストのボディ部分のタイプ
                        json: body_post                                       //221行目で設定したボディ部分
                    }
                    //レコードを取得
                    request(options_getsalesamount, function (error, response, body) {    //Node.jsのrequestモジュールで234行のリクエストを送信
                        if (error) {                                                      //242行目の値がerrorだったら
                            console.log('Error: ' + error.message);
                            reject();                                                     //220行目のPromiseは失敗したとエラーオブジェクトを返す
                        }
                        console.log("kintone recordput:succcess"+val);
                        resolve();                                                        //220行目のPromise完了を返す
                    });
                });
            });
            Promise.all( kintonepromiseset )                                              //220行目で設定したPromiseが全て完了したらここに来る(本稿は1つ)
                .then( function ( message ) {                                             //252行目の処理が成功したのでこのPromiseチェーンへ
                    context.done(null, {text: "kintone POST and Box Folder Create success!"});  //Lambdaの処理結果をログとして残す
            })
                .catch( function ( reason ) {
                    context.done(null, {text: "Box Folder Create failed!"});              //Lambdaの処理結果としてエラーログ
                return;
            });
        } else {
            context.done(null, {text: "Box Folder Create failed!"});                      //Lambdaの処理結果としてエラーログ
        }
    }, function(res) {                                                                    //212行目の返り値がrejectで戻ってきた場合
        context.done(null, {text: "Box Folder Create failed!"});                          //Lambdaの処理結果としてエラーログ
        return false;                                                                     //212行目の結果としてfalseを返す
    });
};

 

なんとか実装

  Topへ↑

いずれにせよ、私が2019年に出した唯一の大きなバグがこれでした。
結局、バグが出てから実運用にこぎつけるまでにさらに二カ月ほどの時間をいただきました。お客様にも多大なご迷惑をおかけしてしまいました。

これが実装できたことで、案件アプリにレコードを登録した時点で、Webhookが発動し、AWS API GatewayからAWS Lambdaを介してboxへのフォルダー生成と、レコードに対応するboxのURLとフォルダー番号をkintoneの該当レコードに登録することができました。

kintoneの画面上にboxのフォルダーを出す部分はbox for kintoneの内部にも書かれている通りです。
実際それを使わせていただいています。ありがとうございます。
以下にコードを載せていますが、疲れてきたのでコード内のコメントは割愛します。ごめんなさい。

(function() {
    'use strict';

    var BOX_CLIENT_ID = 'wkgp4k64whsha8mwvg7k5k63cim82mmv';   //sample_plugin_default
    // localStorage
    var LOCAL_STORAGE_PREFIX = 'kintone.plugin.' + BOX_CLIENT_ID;
    var LOCAL_STORAGE_JUDGED_ALLOW_ACCESS = LOCAL_STORAGE_PREFIX + '.judgedAllowAccess';

    var config = [];

    var BOX_EMBED_WIDTH = 840;
    var BOX_EMBED_HEIGHT = 420;

    var getUrl = function(path) {
        var matchedGuestSpacePath = location.pathname.match(/^\/k\/(guest\/\d+\/)/);
        var guestSpacePath = '';
        if (matchedGuestSpacePath !== null && matchedGuestSpacePath.length === 2) {
            guestSpacePath = matchedGuestSpacePath[1]; // "guest//"
        }
        var apiPath = '/k/' + guestSpacePath + path;
        return apiPath;
    };

    var boxApi = {
        clientInfo: {'provider': 'box', 'client': BOX_CLIENT_ID},

        getAccessToken: function() {
            // add a hash parameter for distinguishing OAuth redirect
            var delimiter = (location.hash.indexOf('#') === 0) ? '&' : '#';
            location.hash += delimiter + BOX_CLIENT_ID + '.oauth_redirect=true';
            kintone.oauth.redirectToAuthenticate(this.clientInfo, location.href);
        },
        hasAccessToken: function() {
            return kintone.oauth.hasAccessToken(boxApi.clientInfo);
        }
    };

    var validateConfig = function(record) {
        config['folderId'] = '0';//Box親フォルダーID
        config['keyFld'] = '顧客名';//kintoneキーフィールド
        config['boxUrl'] = '表示';//Box共有リンクの格納先
        config['boxFolderId'] = "boxフォルダーID";
        config['access'] = 'Open';//Box共有リンクのアクセス権[Collaborator/Company/Open]
        config['prohibitToDownload'] = 'false';//コラボレータにのみダウンロードを許可する

        if (!config) {return false; }
        return true;
    };

    var decorateBoxLinkField = function(boxUrl) {

        var boxLinkPattern = /^https:\/\/([a-zA-Z0-9]+).box.(com|net)(\/s\/[a-z0-9]+)$/;
        var match = boxUrl.match(boxLinkPattern);
        if (!match) {
            return;
        }
        var iframeSrc =
            'https://app.box.com/embed_widget/000000000000' +
            match[3] +
            '?theme=gray' +
            '&show_parent_path=no' +
            '&show_item_feed_actions=no' +
            '&partner_id=233';

        var elEmbed = kintone.app.record.getFieldElement(config.boxUrl);
        if (elEmbed === null) {return; }
        $(elEmbed).empty();

        var width = BOX_EMBED_WIDTH;
        var height = BOX_EMBED_HEIGHT;

        $(elEmbed).parent().css({
            'width': (width + 100) + 'px',
            'height': 'auto',
            'background-color': 'rgba( 255, 255, 255, 0 )'
        });
        var embedIframe = $('', {
            src: iframeSrc,
            width: width,
            height: height,
            frameborder: '0',
            allowfullscreen: 'true',
            allowscriptaccess: 'always'
        });
        $(elEmbed).append(embedIframe);
    };

    var judgedAllowAccessFlag = {
        isSet: function() {
            return (localStorage.getItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS) !== null);
        },

        set: function() {
            localStorage.setItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS, 'true');
        },

        remove: function() {
            localStorage.removeItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS);
        }
    };

    kintone.events.on('app.record.detail.show', function(e) {
        if (validateConfig(e.record)) {
            var boxUrl = e.record[config.boxUrl].value;
            if (!e.record[config.boxUrl].value) {

                var elEmbed = kintone.app.record.getFieldElement(config.boxUrl);
                if (elEmbed === null) {return null; }
                $(elEmbed).empty();

            } else {
                decorateBoxLinkField(boxUrl);
            }
        }

        return e;
    });

    var checkAccessToken = function() {
        var oauth_redirect_param = BOX_CLIENT_ID + '.oauth_redirect=true';
        if (location.hash.indexOf(oauth_redirect_param) !== -1) {
            judgedAllowAccessFlag.set();

            // remove a hash parameter
            location.hash = location.hash.replace(oauth_redirect_param, '');

            var t = setInterval(function() {
                if (location.hash.indexOf(oauth_redirect_param) !== -1) {
                    // cancel button was clicked
                    clearInterval(t);
                    location.href = getUrl(kintone.app.getId() + '/');
                }
            }, 500);
        } else if (!judgedAllowAccessFlag.isSet() || !boxApi.hasAccessToken()) {
            kintone.oauth.clearAccessToken(boxApi.clientInfo, function(body, status, headers) {
                boxApi.getAccessToken();
                return null;
            });
        }
    };

    kintone.events.on('app.record.create.show', function(e) {
        if (validateConfig(e.record)) {
            checkAccessToken();
            e.record[config.boxUrl]['disabled'] = true;
            e.record[config.boxFolderId]['disabled'] = true;
        }

        return e;
    });

    kintone.events.on('app.record.edit.show', function(e) {
        if (validateConfig(e.record)) {
            if (!e.record[config.boxUrl].value) {
                checkAccessToken();
            } else {
//                e.record[config.keyFld]['disabled'] = true;
                e.record[config.boxFolderId]['disabled'] = true;
            }
            e.record[config.boxUrl]['disabled'] = true;
        }
        return e;
    });

    kintone.events.on('app.record.index.edit.show', function(e) {
        if (validateConfig(e.record)) {
            e.record[config.boxUrl]['disabled'] = true;
            e.record[config.keyFld]['disabled'] = true;
            e.record[config.boxFolderId]['disabled'] = true;
        }
        return e;
    });
})();

 

 

まとめ

  Topへ↑

実案件ではさらに凝った実装(フォルダー数も階層も本稿の例よりさらに多い)が施されています。
そして、古くboxが設定されていないレコードには手作業がたまに発生しているものの、実運用に乗っています。
この記事ではそれ以上の情報を出すことはお客様の業務に関わるのでここまでにしとうございます。

本稿がkintoneを運用している皆様にとって少しの手助けになれば幸せです。

kintone上で大量の添付ファイルに困っていらっしゃる方や、社内ファイルサーバーからの移行でお困りの方。他のPaaSからkintoneへ移行する作業があって、添付ファイルの扱いにお困りの方。
弊社では本稿のようなboxとkintoneの連動事例を何例も手掛けております。お困りの際はおっしゃってくださいませ。

最後に蛇足ですが、boxの案件で例に挙げた三つのダムは、私が実際に訪れてダムカードを入手した場所です。


当エントリーの参考にさせていただいたブログ

  Topへ↑

最後になりましたが、このエントリー作成にあたり、以下の2サイトからの情報を参考にさせていただきました。ありがとうございました。

 box APIレファレンス
 AWS Lambda上でBox Node SDKを利用する-九龍堂雑録


コーチングのグラフってkintoneで出せるんやって!


kintone2 Advent Calendar 2019の5日目の記事です。

  Topへ↓

突然ですが皆さん、コーチングって聞いたことがありますか?
あっ! そこのあなた、ページはそのままに!
これは間違いなくkintone Advent Calendarの記事ですから。
ほら!

きとみちゃん楽しいですよね!
https://kintone.cybozu.co.jp/jp/kitomi/

日々、お仕事に励むきとみちゃん。
きとみちゃんとお仕事をする仲間はとっても個性が豊か。

ちょっぴりあわてんぼうでドジっ子のきとみちゃんがkintoneに救われる姿は微笑ましいです。
ちなみに私は巻物で見積書を出してくださる麻呂な方が好きです。この方のお名前はなんでおじゃる?

さて、きとみちゃんがお仕事をする上で助けになる手法はkintoneの他にもさまざまなものがあります。

その中の一つが冒頭に書いたコーチングなのです。

コーチングを一言で言い表すなら、
・相手の学習や成長、変化を促し、相手の潜在能力を解放させ、最大限に力を発揮させる。
でしょうか。

詳しくはWikipediaの「コーチング」
をご覧くださいませ。

ビジネスにフォーカスを当てたコーチングの歴史はまだまだ浅いです。
ここでお伝えしておかなければならないのは、自己啓発セミナーとは違う、ということです。

と、偉そうにウンチクを述べる私ですが、コーチングを受けた経験は人生で1,2回だけ。
では、そんな私がkintone Advent Calendarで何を語るというのでしょうか。

結論を先に書いちゃうと、kintoneでこんなグラフを作ってみましょう!
ということなんですね。

グラフとデータのご説明

  Topへ↑

上に登場したのは四つの傾向を円グラフにしたものです。
それぞれの傾向の文字列にマウスを合わせると、事前に登録しておいたキーワードが出てくる。
これ、実は以前、お客様に依頼されて作ったkintoneにChart.jsを組み込んだグラフ生成の仕掛けです。

私はコーチングには無知です。
ですから、kintoneに入力画面を作り、その結果を集計することで、設問に応じた四つの傾向が算出できる、ということを知ったときは新鮮でした。

お客様によれば、
相手をほめる場合の、個人に響くキーワードは4つの傾向に分けられる
だそうです。

それに合わせて、こんな入力画面を作ってみました。

仮に20問の設問としています。それぞれの4つの傾向ごとに5問を設問しました。
それぞれの問いごとに
・よく当てはまる
・当てはまる
・当てはまらない
・まったく当てはまらない
の4種類の答えをラジオボタンで設定しています。

もちろん、さらに設問数を増やすことも可能ですし、設問数を自在に増減させたい、というご要望もあるでしょうね。
その場合はサブテーブルを使えばよさそうです。
この記事ではサブテーブルではなく、20問に固定したバージョンでお届けしてみます!

実際の内容

  Topへ↑

はい。ではアプリの設定画面です。フォームはこんな感じ。

一番左の文字列フィールドは設問の文字列を入力します。
フィールドコードは上から順にquestion_1からquestion_20としています。

真ん中のドロップダウンフィールドは4つの傾向を選びます。
フィールドコードは上から順にtrend_1からtrend_20としています。

右のラジオボタンフィールドはそれぞれの答えを入力する欄です。
フィールドコードは上から順にanswer_1からanswer_20としています。

で、続いてはグラフを表示するカスタマイズビューを設定してみましょう。

こんな感じですね。

続いてはロジックです。
実は、このグラフを作るには以下の二つのJavaScriptファイルを設定するだけ。

上に設定したのは、Chart.jsです。
Cybozu Developer Network
からCDNのページに移動してもらえれば。

そこのChart.jsに書かれているURLをコピーし、上の画面の
から

に貼って保存するだけ! きとみちゃんでもできますよね?
htttps://がダブらないようにだけ気を付けて!

続いてグラフ表示のロジックです

  Topへ↑

では続いてきとみちゃんとグラフ.jsの内容を。
ここからはVisual Studio Codeの画面にコメントを入れています。




ちょっと見にくいので、直に貼ったコードも提供します。右にスクロールしてくださいね。

(function () {
  "use strict";

  // 一覧ページ
  kintone.events.on('app.record.index.show', function(event) {                       //一覧画面表示時の定型文です
    var record = event.records[0];
    var itemcount = 20;
    var 1_Score = 0;
    var 2_Score = 0;
    var 3_Score = 0;
    var 4_Score = 0;
    var selectedScore = 0;
    var dataLabelPlugin = {                                                          //ここは以下の162行目で呼び出されるチャートのプラグインコンフィグで呼び出される部分です。
      afterDatasetsDraw: function (Chart, easing) {                                  //afterDatasetsDrawとはプラグインコアAPIとして呼び出されるChart.js内部のフックです。要は描画後です。
        var ctx = Chart.ctx;                                                         //チャートが描画されている対象のDOM要素です。157行目で定義され、159行目でChartオブジェクトに渡されます。
        Chart.data.datasets.forEach(function (dataset, i) {                          //対象チャートをループしています。データは77行目で一種類で指定していますのでループは一回のみです。               
          var meta = Chart.getDatasetMeta(i);                                        //チャートのメタデータを取得しています。データやラベルも含まれています。
          if (!meta.hidden) {                                                        //チャートのhiddenプロパティがTrueの場合そもそもチャートが描画されません。
            meta.data.forEach(function (element, index) {                            //メタ要素のデータをループします。今回は4種類ですね。
              ctx.fillStyle = 'rgb(0, 0, 0)';                                        //円グラフの中の文字の色です。rgb(0, 0, 0)は黒を表しています。
              var fontSize = 16;                                                    //36-37行目で文字の場所を設定するためのフォントサイズを16pxで設定しています。表示フォントのサイズとは別に。
              ctx.font = "24px \"Helvetica Neue\", Helvetica, Arial, sans-serif";    //これが実際に描画される文字のフォント情報です

              var sum  = function(arr) {                                             //ここでは対象となるデータの合計値を返します。4種類のデータの合計です。
                  return arr.reduce(function(prev, current, i, arr) {
                      return prev+current;
                  });
              };
              var percentString = ((dataset.data[index] / sum(dataset.data))*100).toFixed(1) + "%";  //それぞれのデータの値を全体の合計で割り、パーセントの文字列を構築します。
              var dataString = Chart.data.labels[index];                                             //それぞれのデータのラベルです。79行目で定義した4つの傾向のラベルですね。 
              ctx.textAlign = 'center';
              ctx.textBaseline = 'middle';

              var padding = 5;
              var position = element.tooltipPosition();
              ctx.fillText(dataString, position.x, position.y - (fontSize / 2) - padding);           //30行目で設定したラベルの値を計算した位置に表示します。
              ctx.fillText(percentString, position.x, position.y - (fontSize / 2) - padding + 35);   //29行目で設定した値のパーセントの文字列を計算した位置に表示します。
            });
          }
        });
      }
    };
    for (var i=1 ; i<=itemcount ; i++){                                                              //ここから76行目までは大人の事情でいろいろとあいまいですがお許しを
      switch( record['answer_' + i]['value'] ) {                                                     //要するに20レコードの設問の答えを基に四つの傾向に加算しているのです
        case 'よく当てはまる':
          selectedScore = 係数は内緒よ♪;
          break;
        case '当てはまる':
          selectedScore = 係数は内緒よ♪;
          break;
        case '当てはまらない':
          selectedScore = 係数は内緒よ♪;
          break;
        case 'まったく当てはまらない':
          selectedScore = 係数は内緒よ♪;
          break;
      }
      switch( record['trend_' + i]['value'] ) {
        case '一つ目の傾向':
          1_Score = 1_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;           //要するに20レコードの設問の答えを基に四つの傾向に重みづけしているのです
          break;
        case '二つ目の傾向':
          2_Score = 2_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
        case '三つ目の傾向':
          3_Score = 3_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
        case '四つ目の傾向':
          4_Score = 4_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
      }
    }
    1_Score = ロジック関数は内緒よ♪(1_Score);
    2_Score = ロジック関数は内緒よ♪(2_Score);
    3_Score = ロジック関数は内緒よ♪(3_Score);
    4_Score = ロジック関数は内緒よ♪(4_Score);                                          //さらに四つの傾向に値を秘密ロジックで精緻化しています。この辺も大人の事情が絡んでいます。
    var pieChartData = {                                                             //161行目でChartオブジェクトに渡されるデータとラベルと背景色のホバー色や枠の組み合わせです。四要素です。   
      labels : ["リーダー合理系","アイディア活動系","ヘルプ支援系","クール分析系"],       //ラベルですね。四つの要素に分かれています。
      datasets : [                                                                   //四つの要素のそれぞれの色の指定です。
        {
          backgroundColor: [
            '#ff6384',
            '#36a2eb',
            '#cc65fe',
            '#ffce56'
          ],
          hoverBackgroundColor: [
              "#FF2384",
              "#3662EB",
              "#cc25fe",
              "#FF8E56"
          ],
          hoverBorderColor: [
              "#000000",
              "#000000",
              "#000000",
              "#000000"
          ],
          hoverBorderWidth: [
              2,
              2,
              2,
              2
          ],
          data : [1_Score,2_Score,3_Score,4_Score]                                   //四つの要素の値です。大人の事情で実際の回答から複雑に計算された結果が格納されます。
        }
      ]
    }
    var tooltipkeyword = {                                                           //ここは四つの傾向ごとに176行目で乱数を設定し、任意のキーワードを表示するようにしています。
      type : [
        {
          word : [
            '同業者もあの人を噂している',
            '他の部署でも話題になっている',
            '○○さんしかできない',
            '自分で判断し、動ける人',
            '部署のメンバーに信頼されている',
            'あのひとには任せられる'
          ],
          title : "任せる、難題、未知の分野、他に頼めない、誰にもできない"
        },
        {
          word : [
            '発想がおもしろい!!',
            '一緒にいるだけで楽しい!!',
            'さすがアイデアマン!!',
            'すばらしいサービス精神!!',
            'うちの部署のムードメーカー!!',
            'その自由な発想がうらやましい!!'
          ],
          title : "自由にして、思いっきり、楽しく、面白く、みんなでいっしょ"
        },
        {
          word : [
            'みんなが働きに感謝している',
            '縁の下の力持ち',
            '一緒にいて落ち着く',
            '丁寧で親切で信頼できる',
            '細かいところによく気が付く',
            '相手の気持ちを分かってくれる'
          ],
          title : "感謝、ありがとう、仲良く、話し合い、相手の気持ち"
        },
        {
          word : [
            '詳しく業務を理解している',
            '商品のことをよく知っている',
            'わが社のことになんでも詳しい',
            'うちの課の歩く辞書',
            'あの人に聞けば間違いない',
            'このデータ量は大したもの'
          ],
          title : "情報、正確、正しく分析、予定通り、計画通り"
        }
      ]
    }
    var canvas = document.getElementById('canvas').getContext("2d");                 //Chartが描画されるDOM要素を指定するChart.jsの定型文です。id="canvas"はカスタマイズビューで指定しました。
    canvas.canvas.height = 256;                                                      //描画される領域の高さを指定しています。
    var test_chart = new Chart(canvas, {                                             //ここでChartオブジェクトをインスタンスとして実体化させています。
      type: 'pie',                                                                   //type: 'pie'はグラフの種類ですね。円グラフです。
      data: pieChartData,                                                            //77行目で定義したデータの実態です。
      plugins: [dataLabelPlugin],                                                    //プラグインコンフィグで関数を呼び出すことができます。その関数は13行目をご参照ください。
      options: {                                                                     //ここからはオプション情報です。
        animation: {
          animateRotate: true,
          animateScale: true
        },
        tooltips: {
          titleFontSize: 48,
          bodyFontSize: 36,
          callbacks: {
            label: function (tooltipItem, data){                                     //ここは描画後にマウスカーソルが乗った時の事前に内部でtooltipItemに定義された情報を基に値を返します。
                return pieChartData["datasets"][0]['data'][tooltipItem['index']] + "ポイント"       //77行目で定義されたデータから該当するデータを表示し
                  + "  キーワード → " + tooltipkeyword["type"][tooltipItem['index']]["title"];      //さらにキーワードとして109行目で定義された四つの傾向のタイトルを表示します。
            },
            afterLabel: function (tooltipItem, data){                                //172行目のラベルの後に別の情報を表示させるにはafterLabelツールチップコールバックが呼び出せます。
                  return "「" + tooltipkeyword["type"][tooltipItem['index']]["word"][Math.floor(Math.random()*(6-0)+0)] + "」";
            }                                                                        //さらにテキストとして109行目で定義された四つの傾向の文言のオブジェクトから乱数で選ばれた文言を表示します。
          }
        },
      }
    });
  });
})();

あとはこのJavaScriptファイルを

にのようにアップロードしていただければ。

どうでしょう。kintoneのデータにChart.jsを組み合わせるだけで、
kintoneのデータを分析することができてしまうのです。

Chart.jsにはさまざまなグラフが用意されているので、
kintoneの標準グラフでは表現できないことも可能です。

コーチング用の分析ツールとしても使えてしまうkintoneの奥深さを楽しんでいただけたらきとみちゃんも喜ぶはずです!
よかったら以下にChart.jsの公式サイトのリンクも貼っているのでご参考になさってくださいね。

当エントリの参考にさせていただいたブログ

  Topへ↑

最後になりましたが、このエントリ作成にあたり、以下の2サイトおよび、コーチングについて教えて頂いたお客様からの情報を参考にさせていただきました。ありがとうございました。

 Chart.jsドキュメント翻訳
 Chart.js公式サイト


ライフログのkintone盛り alasql仕込みのGoogle Chart添え


1.読まなくてもいい献立の前書き

  目次へ↓

唐突ですが皆さん、ライフログって言葉覚えてますか? 何!忘れた? そんな言葉あったっけ?

それならば説明しましょう!システムが行った作業の結果がログ。ウェブ上にログを残すからウェブログ。略してブログ。そして、人生のイベントを記録するのがライフログ。今やこの言葉は、数多くの新語とともにハードディスクの肥やしと成り果てています。

ところが、まだ忘れるのは早い!という訳で、kintoneでライフログです。

このライフログという概念は2000年代の初め頃に産声をあげていたといいます。ITジャーナリストの佐々木俊尚氏は、2010年の秋に発表した『キュレーションの時代』の中でライフログの概念は2010年代の間にはまだ根付かないだろう、と的確に予言されています。なぜなら、日本にはプライバシーの公開に気持ち悪さを持つ人が大多数だから、と。つまり、ライフログとはまだこれからの概念なのです。佐々木氏はその本の中で、ライフログを集めるツールとしてFourSquare_logoをプッシュしています。このFourSquare_logoですが、ある時期に仕様が迷走したこともあって日本ではすっかり影を潜めてしまいました。ですが、海外では位置とモノを関連付けたデータをがっちり握った企業として存在感を保っているようです。FourSquare_logoとは、一言でいうと訪問場所でチェックインすることで自分がいつどこにいたかを後世の自分、またはどこかの物好きのために記録しておくツールです。それはまさにライフログ。なお『キュレーションの時代』はかつて弊社のブログでも取り上げています。

kintoneは言うまでもなく優れたプラットホームサービスですし、仕事の改善にはテキメンに効きます。それはもう間違いない。でも、プライベートな使い方だってできるのですよ。ワークライフバランスを充実させるにはプライベートでもkintoneを使い倒したほうがいいと思いませんかみなさん!?

私にとって幸いなことにkintoneFourSquare_logoの連携事例はウェブにほぼ皆無です。そこは青い大海原、ブルーオーシャン!というわけでささやかですが、連携事例を公開したいと思います。あわせて、これもkintoneとの連携事例が極めて少ないGoogle Chartと絡めて。題して「ライフログのkintone盛り alasql仕込みのGoogle Chart添え」

2.ご用意いただく材料

  目次へ↑

ちなみにこのレシピの内容を再現せんと志す奇特な方は、以下のものをご用意ください。

  • FourSquare_logoのアカウント(チェックイン用のアプリSwarm_logoも忘れずに)
  • kintoneのスタンダードプラン
  • phpが動き、cronが実行できるサーバー(レンタルサーバーでも可)
  • Google Maps Platform上で動くライブラリを呼び出すためのAPI KEYの入手。こちらをご参考に

このレシピは2018年12月時点の「さくらのレンタルサーバー スタンダード」(phpは7.2.10(CGI版))で動作することを確認しております。
また、Google Maps Platformで動くAPIについては、月あたり40000リクエストまでが無料枠内だそうです。このレシピはプライベート利用なので、そこまでいかないことを念頭においています。
ちなみに、みんな大好き「zapier」でも、Swarm_logoでチェックインする度にkintoneに新たなレコードを追加できます。が、それをいっちゃぁおしめえよ、ということでお付き合いいただければ幸いです。
このレシピは一日単位でkintoneにライフログを一括で登録することに意義を持たせています。そして激動の一日を振り返りつつ、自分をねぎらいたい、そして昨日を忘れ去りたい「あ・な・た」の味方になることも!

なお、いうまでもなく、ライフログはみだりに公開するものではありませんぞ。このレシピのせいであなたのデイリーライフに何か問題が起こっても責任は取れませんのであしからず〜

3.献立の出来上がりイメージとレシピ

  目次へ↑

FourSquare_logoはAPIがOAUTH付きで公開されています。なので私の策略はこうです。まず、午前0時を過ぎたあたりでFourSquare_logoの前日分のチェックインデータをkintoneにシュっと放り込んでやろう。そして溜まったデータをウヒャヒャと一覧で眺めてみよう。ついでに今年、どんぐらい国や県を訪れたんやろう、と地図に出して一人ニヤけて悦にいる。うーん、ダークロースト。

まずは着地点をお見せします。上が世界の長井。下が日本の長井です。

世界の中心で長井を晒す

日本の中心で長井を晒す

ともにkintoneのカスタマイズビューではなく通常の一覧を使って長井を晒しています。世界地図は日本と台湾に色がついており、日本地図は東京が最も濃く、次に神奈川。データはFourSquare_logoSwarm_logo)でチェックインしたデータをphpでkintoneに取り込みます。地図を書き込むのは一覧画面のヘッダスペースです。そのため、世界地図版と日本地図版で一覧を分けています。

kintoneのアプリの構造はこんなんです。

フィールド名 フィールドコード
場所 場所 文字列(1行)
文字列(1行)
都道府県 都道府県 文字列(1行)
コメント コメント 文字列(1行)
日時 日時 日時

あと、アプリの設定画面で下図の3つのファイルを登録してください。
https://www.gstatic.com/charts/loader.js
とhttps://cdnjs.cloudflare.com/ajax/libs/alasql/0.4.11/alasql.min.js
はCDNから使います。最後のjsファイルはこの後説明します。
kintone_app

ここであらためて全体の構成図をご覧いただきましょう。一目瞭然とはこのことですね。

構成図

4.FourSquareの下ごしらえ

  目次へ↑

日々のチェックイン履歴をFourSquare_logoに溜める方法は、簡単に書くとこんな感じです。

  • Swarm_logoのアプリを立ち上げ、チェックインしたい場所でチェックインボタンを押す。
  • Swarm_logoの候補の一覧にその場所が登場します。
  • 登場しなくても検索すれば場所が指定できます。
  • 指定した場所でチェックイン(構成図の①)を行えば時間とともに自動的に記録(構成図の②)されます。気が向けばコメントも入れられます。

どうです?モンスターボールを投げなくてもよいし、ポータルを三角形で囲わなくてもよいのです。簡単ですよねFourSquare_logo

さて、今回の記事ではAccessTokenをFourSquare_logoから取得する処理(構成図の③)から説明します。AccessTokenはこのレシピの中で一度取得するだけです。なぜ一度だけでよいかというと、AccessTokenはユーザーを特定するのに使われるからです。つまり、一度AccessTokenを取得しておけば、何度でも使いまわしができるのです。FourSquare_logoには他にもたくさんのAPIが用意されています。処理によってはその都度AccessTokenを取らねばならず、ソースも複雑な下ごしらえが求められることもあります。ですが、このレシピではユーザーのチェックイン情報だけをとるのが目的なので、一度きりでよいのです。

5.FourSquareのAPI KeyとAPI Secretを取り寄せる

  目次へ↑

まず、https://ja.foursquare.com/にアクセスしてください。

FourSquarePage

そして、右上の開発者をクリックします。

すると、https://developer.foursquare.com/にページが遷移します。

FourSquareDeveloperPage

がでたら、右上のCreate Accountからアカウントを作成してください。

多分、FourSquare_logoで作ったアカウントがそのまま開発者アカウントとして引き継がれるはずです。

そしたら下図のようにMy Appsを選びましょう。アプリの作成のリンクがあるはずです。

FourSquareMyApp

こんな風に入れてください。

FourSquareMyAppRegist

この中で重要なのは3点。

  • Client ID、Client Secretは次の作業で使うのでちゃんとメモっておきましょう。
  • Client ID、Client Secretは墓まで持っていきましょう。人に教えちゃダメ!
  • Redirect URI (s)もちゃんと考えておきましょう。これは次の作業でこさえるスクリプトのURIです。

Application Urlという項目もありますが、ここは適当でよいです。他の項目も商用で使わなければ空白でよいはず。

6.FourSquareのアクセストークンの取り寄せとソースの仕込み方

  目次へ↑

続いて、アクセストークンを取得するためのphpを示しましょう。98行。38ステップです。

コメントに内容は記載しているので、まずはソースを味わってみてください。

設定A ⇒ 条件分岐B ⇒ 条件分岐C ⇒ 処理D ⇒ 処理E ⇒ 処理F ⇒ 条件分岐G ⇒ 処理H ⇒ 条件分岐I ⇒ 処理Jの順に進みます。

  // 設定A //
  // マイアプリで表示されたClient ID
  $client_id = 'KOKONIHAHONMAYATTARARANDAMUNAMOJIRETSUGAHAITTORUNEN';
  // マイアプリで表示されたClient Secret
  $client_secret = 'SEYAKEDOKOKONISOREDASHITARAORENOKOUDOUGABARERUKARADASAHENNEN';
  // リダイレクトURL (このスクリプト自身のアドレスです)
  $redirect_uri = 'https://*****.*****.ne.jp/*****/****_*********.php';
  // アクセストークン取得URLのベース
  $access_token_baseurl = 'https://foursquare.com/oauth2/access_token';
  // 認証URLのベース
  $authenticate_baseurl = 'https://foursquare.com/oauth2/authenticate';

  // 結果表示HTML用
  $html = '';
  // 結果表示見出し
  $html .= '<h2>実行結果</h2>';

  // 条件分岐B //
  //初回は$_GET['code']がなく中の処理は実行されない。「許可」され、リダイレクトされた場合に実行
  if( isset( $_GET['code'] ) 
    && !empty( $_GET['code'] ) 
    && is_string( $_GET['code'] ) ) {
    // 処理E //
    // 認証画面でFourSquareの実行が許可されると$_GET['code']付きでこのスクリプトが呼び出される。
    // アクセストークンの取得に利用するコード
    $code = $_GET['code'];
    // 処理F //
    // アクセストークン取得のためのパラメータを設定したUrlを組み立てる。
    // リクエストURL
    $request_url = 
      $access_token_baseurl . 
        '?client_id=' . $client_id . 
        '&client_secret=' . $client_secret . 
        '&grant_type=authorization_code' . 
        '&redirect_uri=' . rawurlencode( $redirect_uri ) . 
        '&code=' . $_GET['code'] . 
        '&state=users/self';
    // curlを初期化する
    $curl = curl_init();
    curl_setopt( $curl , CURLOPT_URL , $request_url );
    curl_setopt( $curl , CURLOPT_HEADER, 1 );
    // 証明書の検証を行わない
    curl_setopt( $curl , CURLOPT_SSL_VERIFYPEER , false );
    // curl_execの結果を文字列で返す
    curl_setopt( $curl , CURLOPT_RETURNTRANSFER , true );
    // タイムアウトの秒数
    curl_setopt( $curl , CURLOPT_TIMEOUT , 5 );
    // 実行し、結果を$jsonに代入
    $res1 = curl_exec( $curl );
    $res2 = curl_getinfo( $curl );
    curl_close( $curl );
    // 取得したJSONデータ(ヘッダーサイズでTrimしないとFourSquareはエラーになる)
    $json = substr( $res1, $res2['header_size'] );

    // JSONをオブジェクト型に変換する
    $obj = json_decode( $json );

    // 条件分岐G //
    if( !isset( $obj->access_token ) ) {
      // アクセストークンを取得できなかった場合
      $error = 'アクセストークンを上手く取得することができませんでした。';
    } else {
      // 処理H //
      // アクセストークンを[$access_token]に代入する
      $access_token = $obj->access_token;
      // アクセストークンをブラウザーに出力する
      $html .= '<p>取得したアクセストークンは <b><mark>' . 
        $access_token . '</mark></b>です。</p>';
    }
  } elseif( isset( $_GET['error'] ) ) {
    // 「拒否」して返された場合怒る。
    $error = 'なんで「許可」してくれへんの!?';
  } else {
    // 条件分岐C //
    // 初回はこの処理が行われるはず。
    header( 'Location: ' . $authenticate_baseurl . 
      '?client_id=' . $client_id . 
      '&response_type=code' . 
      '&redirect_uri=' . rawurlencode( $redirect_uri ) );
    // 処理D //
    // headerの後はexit()
    exit;
  }

  // 条件分岐I //
  // エラー判定
  if( isset( $error ) && !empty( $error ) ) {
    $html .= '<p><mark>' . $error . '</mark>' . 
      'もう一度、認証をするには、' . 
      '<a href="' . explode( '?' , $_SERVER['REQUEST_URI'] )[0] . '">こちら</a>' . 
      'をクリックして下さい。</p>' ;
  } else {
    // 処理J //
    // ブラウザーに[$html]を出力 (結果としてアクセストークンの文字列が表示されます)
    echo $html;
  }

もう一度上の処理をおさらいします。
・最初にこのスクリプトが呼ばれた際、URLにcodeパラメーターはついていません。
・だからBの条件でEFGHの処理は行われず、条件分岐Cの処理が実行されます。
・その中ではLocationヘッダを送信しています。なので、その下の処理Dでただちに認証画面にリダイレクトされます(認証画面は割愛します)。
・そこで認証が終われば、再びこのスクリプトがcodeパラメーター付きで呼ばれるよう、処理DのLocationヘッダの中にredirect_uriパラメーターを指定しています。
・再びこのスクリプトが実行されると、今度は条件分岐Bで処理E、Fが実行されます。
・FではFourSquare_logoのアクセストークンを返すAPIが呼ばれます。
・なので、無事にアクセストークンが返ってくるという流れです。お判りでしょうか?

そしたらこのスクリプトファイルをどこかのウェブサーバーに送ってください。phpファイルの実行権限付きで。先ほどFourSquare_logoのマイアプリの設定でRedirect Url (s)に入力していただきましたが、その内容と合わせておいてください。

たとえばhttps://hanamogera.com/mokeke/foursqaure_tokun_yokose.phpに送るとします。そしたらブラウザーのアドレスバーに直接上のアドレスを指定し、実行します。

すると結果が以下のように表示されるはずです。このマーカー部分をコピーしておきましょう。

アクセストークン

おめでとうございます。アクセストークンはこれであなたのものです。ついに調理人として存分に腕を振るう時が来たのです!!

7.FourSquareからCheckinデータの取り寄せとソースの仕込み方

  目次へ↑

続いては、いよいよAccessTokenを使ってFourSquare_logoにデータくれ!とおねだり (構成図の④) してみましょう!もらったデータをkintoneにシュッと投げ込むまで(構成図の⑤)のphpも説明します。まずはphpを以下に示します。115行。64ステップです。

こちらもコメントに内容は記載しているので、まずはソースを味わってみてください。
先のスクリプトはソースに大分シェフの手を加えましたが、このスクリプトは素材の味を生かすような作りにしています。

設定A ⇒ 設定B ⇒ 設定C ⇒ 設定D ⇒ 設定E ⇒ 条件分岐F ⇒ 条件分岐G ⇒ 処理H ⇒ 処理I ⇒ 処理J ⇒ 処理Kの順に進みます。

  // 設定A //
  // アクセストークン (前の処理で取ってきた文字列です)
  $access_token = 'YATTAYOAKUSESUTOKUNTORETAYOKOREDESIAWASENINARERUNE';

  // 設定項目 (ここはアクセストークン以外はデフォルト)
  $params = array(
    'oauth_token' => $access_token ,// アクセストークン (これでユーザーが認識される)
    'locale' => 'ja' ,              // ローカライズ (jaは日本)
    'm' => 'swarm' ,                // モード (foursquare OR swarm)
    'v' => '20150801' ,             // バージョン (APIのバージョン。今のところ左の年月日)
    'limit' => '250'                // 取得件数 (250が上限)
  ) ;
  // 設定B //
  // GETメソッドで指定した場合 (設定項目のパラメーターを差し替える)
  foreach( array( 'locale' , 'm' , 'limit' , 'sort' , 'afterTimestamp' , 'beforeTimestamp' ) as $val ) {
    if( isset( $_GET[ $val ] ) && $_GET[ $val ] != '' ) {
      $params[ $val ] = $_GET[ $val ] ;
    }
  }

  // 設定C //
  // 設定項目 (日付や並び替えなどの条件を追加します。例えば2018/11/30の00:15時点で実行されたとします。
             するとstrtotime("today")は2018-11-30 00:00:00が返されます。
              strtotime("yesterday")は2018-11-29 00:00:00が返されます。
              それをもとにした$params["afterTimestamp"]は2018-11-29 00:00:00より後のチェックインデータを、
              それをもとにした$params["beforeTimestamp"]は2018-11-30 00:00:00より前のチェックインデータを取得します。
              $params["sort"]はoldestfirstを指定すると上記の日付範囲のチェックインデータのうち古いものを最初に取得します。
              ここ重要です。kintoneライフログ調理師試験に出ますので、要おさらい!!) //
  $today = strtotime("today");
  $yesterday = strtotime("yesterday");
  $params["afterTimestamp"]=$yesterday;
  $params["beforeTimestamp"]=$today;
  $params["sort"]="oldestfirst";

  // リクエストURL (usersの次のselfは固定文字)
  $request_url = 'https://api.foursquare.com/v2/users/self/checkins?' . http_build_query( $params );

  // 設定D //
  // cURLでリクエスト
  $curl = curl_init();
  curl_setopt( $curl , CURLOPT_URL , $request_url );
  curl_setopt( $curl , CURLOPT_HEADER, 1 );
  curl_setopt( $curl , CURLOPT_SSL_VERIFYPEER , false );
  curl_setopt( $curl , CURLOPT_RETURNTRANSFER , true );
  curl_setopt( $curl , CURLOPT_TIMEOUT , 5 );
  $res1 = curl_exec( $curl );
  $res2 = curl_getinfo( $curl );
  curl_close( $curl );

  // 設定E //
  // 取得したJSONデータをオブジェクト形式に変換する (ヘッダーサイズでTrimしないとFourSquareはエラーになる)
  $json = substr( $res1, $res2['header_size'] );
  $obj = json_decode( $json );

  // 条件分岐F //
  // エラー判定 (metaのcodeの値が200だと正常に取得されている)
  if( !$obj || !isset($obj->meta->code) || $obj->meta->code != 200 ) {
    //ログ出力して調査!
  } else {
    // 説明
    $data = array();
    $count = 0;
    // 条件分岐G //
    //取得したデータオブジェクトの -> response -> checkins -> itemsの中をループする //
    foreach( $obj->response->checkins->items as $item ) {
      // 処理H //
      //$itemの中にはチェックインの場所についての栄養が豊富に含まれています。緯度経度やコメントや市長など。詳しくはhttps://developer.foursquare.com/docs/api/users/checkins //
      // チェックインID
      $id = $item->id ;
      // ベニューのID
      $venue_id = $item->venue->id ;
      // ベニューの国 (国名は日本語で取得できる)
      $venue_country = $item->venue->location->country;
      // ベニューの都道府県 (取得した後、Google Chartの都合で末尾の「都」「府」「県」は除去する)
      $venue_prefecture = $item->venue->location->state;
      switch ($venue_prefecture){
        case '北海道':
          break;
        default:
          $venue_prefecture = mb_substr($venue_prefecture,0,-1, "UTF-8") ;
          break;
      }
      // ベニューの名前 (改行は除去しておく)
      $venue_name = str_replace(array("\r\n", "\r", "\n"), '', $item->venue->name);
      // チェックイン日時(オフセットと合わせる。要は日本の時間に合わせる)
      $createdAt = $item->createdAt + 54000 - 86400;
      // 日時の整形 (kintoneにあった日付データ形式に変更する)
      $createdAt = date( 'Y-m-d' , $createdAt )."T".date( 'H:i:s' , $createdAt ) ;
      // コメント (改行は除去しておく)
      $shout = ( isset($item->shout) ) ? str_replace(array("\r\n", "\r", "\n"), '', $item->shout) : '';
      // kintoneに投げるデータです。フィールドコードとデータ型を合わせることを忘れずに。
      // 処理I //
      //kintoneに投げるデータをここで指定します。文字列データは文字列型にキャストしておくとふっくら仕上がります //
      $data[$count] = array(
        "場所" => array("value" => (string)$venue_name),
        "コメント" => array("value" => (string)$shout),
        "国" => array("value" => (string)$venue_country),
        "都道府県" => array("value" => (string)$venue_prefecture),
        "日時" => array("value" => $createdAt)
      );
      $count++;
    }
  }

  // 処理J //
  // kintoneの対象のアプリIDを指定する。(1111というのはダミーです)。FourSquareからのデータもあわせて。
  $postdata = array("app" => 1111,records=>$data);
  // 処理K //
  // kintoneのアクセストークンを指定する。ベーシック認証がある場合はそちらもあわせて指定。
  $headers = array(
    "X-Cybozu-API-Token:" . "kintoneNOAPURISETTEIDESHUTOKUDEKIRUTOKUN",
    "Authorization:" . "Basic " . base64_encode("YUUZAAID:PASUWAADO"),
    "Content-Type:" . "application/json"
  );
  // 以下の******はお使いのkintoneのサブドメインを入れてください。
  $curl = curl_init('https://******.cybozu.com/k/v1/records.json');
  curl_setopt($curl, CURLOPT_POST, true);
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($postdata,JSON_UNESCAPED_UNICODE));
  curl_setopt($curl, CURLOPT_HEADER, true);
  // curlで出たエラーを補足するためのものです。
  $fp = fopen('curl.log', 'a');
  // 詳細な情報を出力する
  curl_setopt($curl, CURLOPT_VERBOSE, true);
  // STDERR の代わりにエラーを出力するファイルポインタ
  curl_setopt($curl, CURLOPT_STDERR, $fp);
  if(curl_exec($curl) === false){
    //ログ出力して調査!
  } else {
    return true;
  }
  curl_close($curl);

このスクリプトもウェブサーバーに送ってください。ファイルの実行権限付きで。あと、ファイルのUrlはきちんとメモしておいてくださいね(^_^)

次にcronの設定を行います。cron実行のための構文はサーバーによって違います。ここに載せているcron設定はさくらインターネットの例ですが、phpファイルの実行パスと、スクリプトのファイル名はどのサーバーでも求められるはずです。このレシピは毎日00:15に自動で実行するように設定しています。

Cron設定1

cron2-1

8.kintone上でデータの盛り付け

  目次へ↑

さて、7までのレシピ(2018/12時点の)に忠実に行うとチェックインデータは毎日順調にkintoneに流れ込むはず。あとはデータを盛り付けるだけ。「ライフログのkintone盛り alasql仕込みのGoogle Chart添え」とうたっている以上、最後の仕上げにGoogle Chartを添えるのを忘れるなかれ。それぞれの一覧ごとに違うマップを表示するJavaScriptを以下に示します。この中でkintoneのデータをalasqlで集計し、その結果をGoogle Chartで地図に表示しています。

世界地図の場合、処理A ⇒ 条件分岐B ⇒ 処理C ⇒ 処理D ⇒ 処理E ⇒ 処理V ⇒ 処理F ⇒ 処理W ⇒ 処理G ⇒ 処理H ⇒ 処理I ⇒ 処理J ⇒ 処理Kの順に進みます。
日本地図の場合、処理A ⇒ 条件分岐L ⇒ 処理M ⇒ 処理N ⇒ 処理O ⇒ 処理V ⇒ 処理P ⇒ 処理W ⇒ 処理Q ⇒ 処理R ⇒ 処理S ⇒ 処理T ⇒ 処理Uの順に進みます。

(function () {
  "use strict";
  // 処理V //
  // 再帰処理による一回のリクエスト制限を超えた全レコードを取得 //
  function fetchRecords(appId, opt_query, opt_fields, opt_offset, opt_limit, opt_records) {
    var query = opt_query || '';
    var offset = opt_offset || 0;
    var limit = opt_limit || 500;
    var allRecords = opt_records || [];
    var params = {app: appId, query: query + ' limit ' + limit + ' offset ' + offset };
    if (opt_fields) params.fields = opt_fields;
    return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params).then(function(resp) {
      allRecords = allRecords.concat(resp.records);
      if (resp.records.length === limit) {
        return fetchRecords(appId, query, opt_fields, offset + limit, limit, allRecords);
      }
      return allRecords;
    });
  }

  // 処理W //
  // json形式で取得したkintoneのレコードをalasqlで扱えるようレコードセット形式に変換 //
  function convertToRows(records) {
    var rows = records.map(function(record){
      var keys = Object.keys(record);
      var row = {};
      keys.map(function(key){
        row[key] = record[key].type === 'NUMBER' ? Number(record[key].value) : record[key].value;
      });
      return row;
    });
    return rows;
  }

  // 一覧ページ
  kintone.events.on('app.record.index.show', function(event) {
    // 処理A //
    //Google Cloud Platformで発行したAPI KEYです。
    //API KEYがない場合、以下の処理Cでgoogle.charts.load('upcoming', {'packages':['geochart']});としても動きますが、世界地図の色塗りができません。また、コンソールでエラーが表示されます。
    //作成したプロジェクトの認証はリファラーを限定するとよいです。https://subdomain.cybozu.com/* のように指定すると、対象のサブドメインに対して動作します。https://akvabit.cybozu.com/だと動きませんので注意が必要です。
    //あと、許可するAPIですが、「Maps JavaScript API」「Geolocation API」「Geocoding API」の三つを有効にしています。
    const ApiKey = 'k54u6 jkrawyeie-wkjykethiudarwhyeiu_rekyjur';
    // 条件分岐B //
    //世界地図ビューの場合 //
    if (event.viewId === 5351051) {
      // 処理C //
      //google chartを読み込む。種類は地図 //
      google.charts.load('upcoming', {'packages':['geochart'],'mapsApiKey':ApiKey});
      // 処理D //
      //google chartの読み込みが完了したらコールバックでdrawWorldMapを呼び出す //
      google.charts.setOnLoadCallback(drawWorldMap);
      function drawWorldMap() {
        var obj = {};
        // 処理E //
        //このファイルの3-17行のfetchRecords関数を呼び出す。対象アプリは自分で、フィールドは[場所][国] //
        fetchRecords(event.appId, '', ['場所', '国']).then(function(records) {
          // 処理F //
          //取得したレコードをSqlで扱えるようなレコードセットの形式に変換するconvertToRows関数(このファイルの20-30行)をご参照 //
          obj.rs1 = convertToRows(records);
          // 処理G //
          //alasqlでsql文のソースを組み上げ、バインドパラメータに処理Fで得たレコードセットをセットする //
          var result = 
            alasql(
              "SELECT t.[国], COUNT(t.[場所]) as [回数] \
              FROM ? AS t \
              GROUP BY t.[国] \
              ORDER BY t.[国]", [obj.rs1]);
          // 処理H //
          //google chartのデータテーブルのインスタンスを新たに確保する //
          var data = new google.visualization.DataTable();
          // 処理I //
          //google chartのデータテーブルの列と行を追加します。alasqlでグループ集計された国ごとの訪問回数です。 //
          data.addColumn('string', '国');
          data.addColumn('number', '訪問場所数');
          result.forEach(value => {
            data.addRow([value["国"], value["回数"]]);
          });
          // 処理J //
          //google chartの世界地図の色塗りの書式を設定する //
          var options = {
            datalessRegionColor: '#ffffff',
            colorAxis:{
                maxValue:500,
                colors:['#D8F6CE','#21610B']
            }
          };
          // 処理K //
          //google chartの世界地図のデータに処理Iで格納した内容を代入し、kintoneのヘッダスペースに描画する //
          var chart = new google.visualization.GeoChart(kintone.app.getHeaderSpaceElement());
          chart.draw(data, options);
        });
      }
    // 条件分岐L //
    //日本地図ビューの場合 //
    } else if (event.viewId === 5351053) {
      // 処理M //
      //google chartを読み込む。種類は地図 //
      google.charts.load('upcoming', {'packages':['geochart'],'mapsApiKey':ApiKey});
      // 処理N //
      //google chartの読み込みが完了したらコールバックでdrawJapanMapを呼び出す //
      google.charts.setOnLoadCallback(drawJapanMap);
      function drawJapanMap() {
        var obj = {};
        // 処理O //
        //このファイルの3-17行のfetchRecords関数を呼び出す。対象アプリは自分で、フィールドは[場所][都道府県] //
        fetchRecords(event.appId, '', ['場所', '都道府県']).then(function(records) {
          // 処理P //
          //取得したレコードをSqlで扱えるようなレコードセットの形式に変換するconvertToRows関数(このファイルの20-30行)をご参照 //
          obj.rs1 = convertToRows(records);
          // 処理Q //
          //alasqlでsql文のソースを組み上げ、バインドパラメータに処理Pで得たレコードセットをセットする //
          var result = 
            alasql(
              "SELECT a.[都道府県], COUNT(a.[場所]) as [回数] \
              FROM ? AS a \
              GROUP BY a.[都道府県] \
              ORDER BY a.[都道府県]", [obj.rs1]);
          // 処理R //
          //google chartのデータテーブルのインスタンスを新たに確保する //
          var data = new google.visualization.DataTable();
          // 処理S //
          //google chartのデータテーブルの列と行を追加します。alasqlでグループ集計された都道府県ごとの訪問回数です。 //
          data.addColumn('string', '都道府県');
          data.addColumn('number', '訪問場所数');
          result.forEach(value => {
            data.addRow([value["都道府県"], value["回数"]]);
          });
          // 処理T //
          //google chartの地図を日本地図の都道府県とし、色塗りの書式を設定する //
          var options = {
            region: 'JP',
            resolution: 'provinces',
            datalessRegionColor: '#ffffff',
            colorAxis:{
                maxValue:600,
                colors:['#F2FBEF','#21610B']
            }
          };
          // 処理U //
          //google chartの世界地図のデータに処理Sで格納した内容を代入し、kintoneのヘッダスペースに描画する //
          var chart = new google.visualization.GeoChart(kintone.app.getHeaderSpaceElement());
          chart.draw(data, options);
        });
      }
    }
  });
})();

いかがでしょうか? ここはカスタマイズビューでリッチにデータを表現しても良いですし、条件に応じて絞り込めば、さらに面白いこともできます。チェックインデータにはカテゴリーもありますので、例えば訪れた酒場だけを抜き出し都道府県で図示したり、訪れたラーメン屋だけを都道府県で抜き出すことだってできます。例えば私の趣味ですが、今までに訪れた滝を色分けすればこうなります。城とすればこう。駅だとこう。他にも登った山や訪れた蒸留所や酒蔵、ブルワリー。各地の日本酒や世界の酒、料理などで色分けしても面白そうです。
残念なことにGoogle chartはまだ市町村には対応していません。もしそうなればもっと面白いライフログが作れそうです。もちろん、地図で塗り分けるほかにもGoogle chartはかなりのグラフの種類を用意しています。そこは皆様の自由です。
なお、ライフログとは、別に人様に見せて放浪癖を誇るものでも、旅行経験を自慢するものでもありません。あくまでもプライベートな利用がよろしいかと思います。ご自身の今までの人生とこれから残された人生に何を成すかを定める助けになればそれで十分です。そのあたり、ライフログについての私の考えは、このレシピ同時に書いたこちらで世に問うてみました。またご覧頂ければ幸いです。
このレシピをまとめるなら、要するにkintoneをプライベート用途に使おうよ、との主旨です。そういう使い方がもっと増えればkintoneはより身近なものになるのですから!

9.当レシピの参考にさせていただいたブログ

  目次へ↑

最後になりましたが、このレシピを作るのに、以下の5サイトを参考にさせていただきました。ありがとうございました。

 Foursquare(Swarm) APIの使い方まとめ (サンプルコード付き)
 Get Check-Ins for a User
 kintone でSQLを使う
 GoogleのGeochartを使ってみた
Google Maps Platform


kintoneのPromiseを説明できるスキル


以前よりお付き合いさせていただいている株式会社アディエム様(https://adiem.jp/)より、
先日ご依頼を受けたたkintone開発案件は、何重にも入れ子になった多重Promise処理が必要でした。

弊社にてコーディングと単体テストを行い、無事納品にこぎつけられたのですが、
アディエム社の技術者様にもコードの説明を行う必要が生じました。

弊社の代表もPromiseの習得にはかなり手を焼いたのですが、そのスキルを習得できたかの判断基準は、その内容を人に説明できるかどうかです。つまり今回、うまく説明できたかどうかは、弊社代表がPromiseを理解できているかのベンチマークにもなりました。

説明を行った結果、アディエム社の技術者様にPromise処理をご理解していただけたようです。追加の処理を実装し、さらにテストまでも行えるまでになったとか。その結果を以下のようなメッセージでいただきましたのでご紹介します。

先日はコードのレクチャーをありがとうござました。
本日、検索部分のエラーハンドリングを追加し、本番環境にリリース致しました。
負荷テストとして4001件のデータを使用して、正常に更新されることも確認しました。

kintone APIのノーマル呼び出しパターン、kintone promise を使ったパターン、promiseでも thenにresolve, rejectを引き渡すパターン、thenとcatchを書くパターンと、かなりケースの整理が できました。また返値の扱いもデバックすることで理解が進みました。
まだうまく関数化して可読性の良いソースを書く自信はありませんが、長井さんソースを参考にさせて 頂きたいと思います。

以上、ご報告まで。

弊社代表は以前より、kintoneのエバンジェリストとしてサイボウズ社より任命されております。
最近はこうしたマンツーマンに近い形で、技術をお伝えする案件も増えつつあります。
その中でこうしたご評価を頂戴したことは、弊社代表にとっても自信になりました。もちろん、スキルの習得に終わりはありません。新たな技術も次々と世の中に生まれています。まだまだ切磋琢磨していかねば。精進します。

今後もアディエム社とはkintone案件のご提案からコーディング・テストまでを協業できる関係を築いていければと思います。
kintoneでのシステム開発のご相談、アディエム社、および弊社にお気軽にお寄せ下さいませ。

また、もし御社の技術者に対し、こうしたマンツーマン形式でのレクチャーをご要望の際は、
ご連絡をください! ご相談に乗らせていただきます。


box for kintoneでERR_CONNECTION_REFUSEDが出てしまった場合


kintoneをカスタマイズする際は、デバッグが欠かせません。
ブラウザーのコンソールに出るエラーメッセージはデバッグを行う上で重要なメッセージですよね。

とくに複数のカスタマイズやプラグインを入れたアプリでは、どのエラーがどのJavaScriptによって出力されるのか把握しておかねばなりません。そうしないと私のように脳のフラッシュメモリー容量が少ない人はすぐにCPU100%に達してしまうのです。

今回、弊社で別のプラグインを作るにあたり、box for kintoneで謎のエラーが出ていました。
エラーが出ているのにbox for kintoneのプラグイン自体はきちんと動作しているという。おやまあ。
謎です。全くの謎です。

と、そんな混乱は、以下にご紹介する方法で解消されました。
ウェブ上でもあまりこの件についての記事がなかったので、ニッチですが皆様のご参考になればとアップしました。

同様にファイアウォール系拡張機能とオンラインストレージの組み合わせだと出るかも。

0.box for kintoneとは

 オンラインストレージとkintoneを連動するプラグインです。こちらは「box for kintone」で検索していただければいくつも見つかると思います。使用上の注意をよく読み、用法 用量を守って正しくお使い下さい。
 こちらとか

1.謎のエラー

 ところが、一見すると想定通りに動いているように見えるbox for kintoneですが、コンソールには
 謎のエラーがうごめいています。

 な、なんやこれ?
 上図では見えませんが、エラーに「その心は?」と聞いてみると「ERR_CONNECTION_REFUSED!」と叫んでいます

 他のカスタマイズを進める都合上、こういうエラーは先につぶしておかないと。

 普通、ERR_CONNECTION_REFUSEDはウェブサイトにアクセスできないときに出るエラー。
 なのにkintoneの画面上にあるboxの窓には接続できていて、ドラッグするとファイルのアップロードもできる。謎です。

 何かが起これば、ファイアウォールを疑う。これは某氏(誰?)の金言です。

 と、そこで、ブラウザー(Google Chrome)に拡張機能として入れているマカフィーの存在に思い至ります。

2.拡張機能を探す。

 拡張機能については、下図のようにたどれば設定画面にたどりつけるはずです。
 

 ここでマカフィーウェブアドバイザーなる拡張機能が登場。これかな。
 

3.拡張機能の「ファイルのURLへのアクセスを許可する」機能をオフにする。

 詳細を押すと、こんな画面が。うむ。OFFになっている模様。これをOnにするといかがかな?
 


 
 こんな風にぽちっと。青くなりましたね。

4.念のためキャッシュやクッキーも消しておきましょうね。

 やってみたらうまくいかないので、ブラウザーがクッキーの消化不良を起こしているに違いない。ここは胃を空っぽにしてあげましょう。

 ここでは閲覧履歴データの消去を選びます。

 ここで閲覧履歴にチェックをいれて消去してしまうと、いろんなサイトで再アクセスが必要になるのでご用心ください。

 すると、謎のboxエラーはいなくなりました。めでたしめでたし。

5.終わりに

これは、2018/9/5時点の情報です。
もちろん、折角ファイアウォールさんがファイルへのURLを許可しないようにしてくれているのに、それを許可するということは、セキュリティ上リスクも増します。
そのあたりをご理解の上、参考にしていただければ。もちろん何かあっても弊社では責任はとれませんので。

なお、弊社はこれから、box for kintoneを拡張し、サブテーブルの行ごとにBOX上でサブフォルダーも作れるようにする予定です。


サイボウズのクラウドサービスが2018年6月10日より一部利用できなくなる件の対応


このような案内をサイボウズ社から頂きました。

さて、本稿はこのような案内をサイボウズ社からいただいたけれど、どうすればよいかお困りの方へ向けて書いています。

実は弊社にも数カ月前、同様の内容が書かれたメールがサイボウズ社から届いていました。
その時は私はまだ先のことと思い、後回しにしました。
ところが今回、このような内容の案内を郵送で受け取るに至り、さすがにまずいと思って調べてみました。

この案内の中で対処法として書かれている通り、ウェブブラウザーやOSが古い場合は簡単です。
サイボウズのサービスを、新しいパソコンでしか使わないようにすればよいのです。

ところが問題は、ウェブブラウザーやOSが最新版なのにこのような案内が来る場合です。
それはリンクに記載されているチェックツールで問題なしと出る場合です。
問題なしと表示されるのに、なぜこのような案内が来るのか不安になりませんか。
もしかしたら、どうすればよいのか分からない方がいらっしゃるのではないでしょうか。
本稿はそういう方のために書かせていただきます。

実は私は最初にメールを受け取った時点でほぼ見当はついていました。
それはウェブブラウザー以外の場所から、サイボウズのクラウドサービスに接続している場合です。
ウェブブラウザー以外の場所とは、たとえばExcel、Word、Access、PowerPointなどのソフトウエアのことです。

弊社の場合、Excelからkintoneへデータを参照しているプログラムが問題でした。

    Set objweb = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    If Err.Number = 0 Then
        Set CreateHttpObject = objweb
        Exit Function
    Else
        MsgBox "MSXML2.ServerXMLHTTP.6.0エラー"
    End If

    Err.Clear
        
    Set objweb = CreateObject("MSXML2.ServerXMLHTTP")
    If Err.Number = 0 Then
        Set CreateHttpObject = objweb
        Exit Function
    Else
        MsgBox "MSXML2.ServerXMLHTTPエラー"
    End If

上のプログラムは、Excelのマクロからkintoneに接続するライブラリの設定箇所を抜粋しています。
上で設定したライブラリを、以下のコードでkintoneに接続していました。

    Set objweb = CreateHttpObject()
    
    If objweb Is Nothing Then
        GetData = ""
        Exit Function
    End If
    
    Call objweb.Open("GET", "kintoneのURL", , "basic認証ID", "basic認証PW")

ここで最初に挙げた、

    Set objweb = CreateObject("MSXML2.ServerXMLHTTP.6.0")

または

    Set objweb = CreateObject("MSXML2.ServerXMLHTTP")

が問題です。

要するにこの二つのライブラリが古いのです。古いライブラリですがWindows 10でもサポートされているため、
エラーは特に起きません。

ところがこのライブラリは、kintoneへの接続の際にTLS 1.0という通信プロトコルを使うのです。
これがサイボウズ社のクラウド基盤側で検知されたため、メールや郵送でご案内を出されたのでしょう。
いつもながら、早め早めを見越したサイボウズ社の対応には感謝です。

弊社の場合、上記の処理を2社様にのみ使っていました。
とくに最初のお客様への導入が2012年の春でした。
それはkintoneがリリースされる前、つまりプレリリースの段階でした。
その時点でMSXML2.ServerXMLHTTP.6.0を採用したことが誤りでした。

それを受けて、

    Set objweb = CreateObject("WinHTTP.WinHTTPRequest.5.1")
    If Err.Number = 0 Then
        Set CreateHttpObject = objweb
        Exit Function
    Else
        MsgBox "WinHTTP.WinHTTPRequest.5.1エラー"
    End If

と改修しました。WinHTTP.WinHTTPRequest.5.1であればTLS1.0は使いません。
3月の初めに改修を行いましたが、今のところエラーもなく動いています。
(サイボウズ社側でまだTLS1.0として検知されていたら問題ですが)

なお、

    Set objweb = CreateObject("MSXML2.ServerXMLHTTP.6.0")

を使っている場合は、

    Call objweb.Open("GET", "kintoneのURL", , "basic認証ID", "basic認証PW")

でエラーなく通ります。
ですが、

    Set objweb = CreateObject("WinHTTP.WinHTTPRequest.5.1")

に変えた場合、

    Call objweb.Open("GET", "kintoneのURL", , "basic認証ID", "basic認証PW")

ではエラーが発生します。

    objweb.Open "GET", "kintoneのURL", False

と変えるとよいでしょう。
ちなみにベーシック認証の指定は、

    Call objweb.setrequestheader("Authorization", "Basic " & encode64("basic認証ID:basic認証PW"))

とopenの後のコードで行っているので、その変更はありません。

というわけで、同様の案内をサイボウズ社からいただいていて、ウェブブラウザーやOSのチェックでは問題ないと表示される方、
は本稿を参考にしてもらえれば幸いです。

もし必要であれば弊社宛にご連絡を頂戴できれば、ご相談にものらせていただきますよ。


2期目を迎え、2週間を経ての所感


4月に入ってから、ワークスタイルを少しだけ私の理想に近付けています。

Facebook静かな自宅では開発を集中して好きなだけ行い、一方では営業に商談に交流会にセミナーに自由に動き回る。といったような。もちろん、それにはリスクも覚悟も伴います。バランス感覚がより一層求められることは言うまでもなく。そんなわけで、今日から外回りの際はネクタイを締めることにしました。身だしなみ云々よりも自分のけじめのために。

今日は練馬のお客様の下へ訪問させて頂きました。社長様より経営哲学をじっくり聞かせて頂き、会社内も存分に見学させていただきました。実に見事な運営ぶりや、社員様やパート様との関係を実に上手に築いておられる姿に、感銘しました。アナログとITの長所が実にがっちりとかみ合っている運営を見るにつけ、非常に刺激になりました。ありがとうございました。

弊社も今後は人を雇って作業を分担していかねばなりません。すでに4月から一部の作業についてはお手伝いをお願いし始めています。でも人を自在に使いこなし、適材適所に相乗効果を発揮してもらうレベルに達するにはまだまだ私自身の精進が必要です。努力すべきと思っています。

4月に入ってからは、私自身の開発者としてのスキルをもう一度磨き直すべく、あえてタイトな納期のLAMP案件を請け、CUIでの操作も含めた開発・テストに没頭しています。一方ではkintoneエバンジェリストとしての動きを活発にしています。kintone Café 埼玉 vol.1の事務局を担当し、さらには別のkintone Caféの準備メンバーとしても動いています。そういった動きを重ねることで、人前でしゃべる技術を磨くという私自身の課題も少しずつ前に進んでいるように思います。今後もそのような場で登壇の場数を重ねるようにして、少しずつ経営者としての実践を身に付けて行きたいと思っています。

kintone案件のコンサル兼開発担当も2月から請けさせて頂いているのですが、そちらでも追加開発のご用命を頂きました。さらには今日、ありがたいことに別のお客様からもkintone案件をご用命頂きました。何とかkintoneエバンジェリストとしての顔でも、少しだけ見せられるようになってきたかな、と思っています。

ただ、私自身のマンパワーも限られています。開発とテストと営業と商談に加え、常駐先での作業もまだ続きます。トイロハさんへの記事アップは2週間ほど出稿出来ていません。トイロハさんは今後も続けていきたいと思っております。そんなわけで、交流会の出席はかなり厳選しなければ、と思っています。今日お話を聞かせて頂いた経営者の方も、交流会は厳選して参加されておられるとか。私も少し見直す時期が来ているのかもしれません。

でも、お蔭さまでなんとか4月からのワークスタイル変更もいい滑り出しを見せています。これも皆さまのお力添えの賜物と感謝しております。その場限りの儲け目当てではなく、お互いが永きにわたって良い関係を得られるよう、引き続き努力していきたいと思っております。

今後ともよろしくお願い申し上げます。


年賀状の宛名書きをkintoneにお願いする。WordとAccessを和えて


kintone Advent Calendarへの参加表明をしたはいいですが、ネタ選びで思惑が狂ってしまいました。ネタにしようと目論んでいたSansan API(eightではなくてSansanの方)とkintoneの連携。ところがSansan様よりAPIの技術情報をブログ等で出すことなかれ、と言われてしまいまして。
え、あと私の担当日まで3日しかないのに、どうしよう・・飲み会も続くし、年賀状も書いてないのに。・・・ん?年賀状? と思いついたのがこちらです。はがきといえば差し込み印刷。Wordです。Wordからkintoneを読み込めないか、という浅い思いつきで調べてみました。 あまり実践的な内容とはいえませんが、ごらん頂ければ幸いです。

本記事のネタは 2015/12/11時点のkintoneとWord 2010とAccess 2010で構築しています。12/12に予定されていたkintoneアップデートは延期されてしまいました。アップデートの後も本記事にある技術は使えると思います。
あ、それとQiitaから頂いたリマインドメールにはブログにアップしたurlを連携してもよいと書かれていたので、当方のブログにアップしました。不都合があればQiitaに上げなおします。

 

Wordの差し込み印刷

さて、まずはWordを立ち上げて下さい。メニューの差し込み文書から、宛先の選択を選びます。

1

 

すると、新しいアドレス帳というダイアログが開き、情報が入れられます。いいですねぇ。ちなみに私、Wordの差し込み印刷はほとんど触ったことがありません。

2

 

で、入れ終わって閉じてみると、mdbの保存ダイアログが出てきました。ほう、Wordの裏側はAccessだったんですね。これは思惑が狂いました。調べてみたのですが直接Wordを触って差し込み印刷の値を設定する方法がわかりませんでした。でも、Accessとkintoneって近いようであまり連携例を見かけません。この機会に連携も試してみましょう。ではAccessを任意の場所に保存してみましょう。

3

 

Accessの設定

Accessが出来ました。Address.mdbというやつです。じゃあこれを元にしてkintoneに一つアプリを作ってみましょうか。作成されたAddress.mdbにあるテーブルは一つだけ。Office_Address_Listというテーブルです。これをエクスポートして、csvファイルとして保存し、そのcsvファイルをkintoneに備わっているcsvからアプリを作る機能で作れば何とかなりそうです。

4

 

テキストエクスポートウィザードで出してみます。

5

 

カンマ区切りですね。あとは先頭行をフィールドとして使うにチェックを入れてください。kintoneのアプリストアでExcel/CSVからのアプリ作成で、先頭行に見出し行が設定されている必要があります。

6

 

じゃあ、この場所にcsvを作ってみましょう。

7

 

そしてkintoneへ

いよいよkintoneの出番です。アプリストアから「Excel/CSVから作成」を選びましょう。そしてAccess.mdbから出力したcsvファイルは、以下の通りとなっています。これはWordのアドレス帳のテーブルです。各項目のフィールドタイプはホームページとメール以外は「文字列(一行)」で設定しましょう。

8

 

無事、アプリが作成できました。あとはここに住所を入れて行けば、念願のクラウドでの住所録管理は目の前です。あ、それと、kintoneのフォームで各フィールドのフィールドコードの設定を以下の通りでお願いします。

            ふりがな(姓) → FamilyName_Furigana
            姓 → FamilyName
            ふりがな (名) → FirstName_Furigana
            名 → FirstName
            敬称 → Title
            会社名 → CompanyName
            部署名 → DivisionName
            役職 → Position
            郵便番号 → ZipCode
            住所 1 → Address1
            住所 2 → Address2
            住所 3 → Address3
            勤務先電話番号 → OfficeTelNo
            勤務先 FAX 番号 → OfficeFaxNo
            自宅電話番号 → HomeTelNo
            自宅 FAX 番号 → HomeFaxNo
            連名 → JointSignature
            連名敬称 → JointSignatureTitle
            電子メール アドレス → MailAddress
            ホーム ページ → WebSiteUrl
            市区町村 → City
            都道府県 → Prefecture
            国/地域 → Country

9

 

いよいよAccessからkintoneへ

kintoneにアプリを作ったら、今度はAddress.mdbです。このAccessとkintoneが連携できればよいわけです。ここで、Visual Basic Editorを開いていただき、ツール(T)→参照設定をお願いします。

10

 

参照設定でなにをするかというと、連想配列を使うのでMicrosoft Scripting Runtimeにチェック。それとmdbへのテーブル操作にADOを入れてみましょう。Access 10であれば ADO 2.8が無難でしょうね。これもチェックをお願いします。

11

 

つづきまして、以下の標準モジュールとクラスモジュールを入れてください。ファイルのインポートで可能です。なお、それぞれのモジュールファイルは用意しました。ダウンロードしやすいようにzip化しておきました。解凍してお使い頂ければ。あ、怪しくないので大丈夫ですよ ^_^

JSON
Mod_CommonDeclaration
Mod_DataTrust
Mod_GetKintone

12

 

さて、インポート頂いたファイルですが、ほとんどはそのままで使えるようにしていますが、皆様の環境に応じて若干の変更が必要です。そうしないと動きません。例えば、Mod_GetKintoneのファイルの19行目。ここで
Set objJSON = GetJSON("/k/v1/records.json?app=**") という記載があります。このアスタリスクの2文字は、先にkintoneに作ったアプリのUrlにある数字を入れてください。

12-3

 

続いて Mod_DataTrustです。ここは7行目。
Private Const TARGET_URL As String = "https://*******.cybozu.com"
という記述の****** の部分に、ご契約されているkintoneのドメイン名を当ててください。

12-1

 

さらに Mod_DataTrustです。ここは76、77、78、79行目に情報を入れて頂きます。
76行目はベーシック認証の情報です。BAsic_IDの部分にIDを。Basic_PWの部分にパスワードをお願いします。
77行目はご契約されているkintoneのドメイン名を****の部分に入力してください。
78行目はBAsic_IDの部分にIDを。Basic_PWの部分にパスワードをお願いします。
79行目はkintone内で使用するユーザーのIDとパスワードです。IDの部分にIDを。PWの部分にPWをお願いします。
12-2

さて、ここまででAccess.mdbとkintoneの連携は出来ました。AccessからMod_GetKintoneの中にあるgS_CmdDataRequeryClickを実行すれば、kintoneのデータを反映するのがわかります。

 

再びWord

さて、Wordに戻りましょう。Wordの文書は新たに作成して結構です。作成したら、差し込み文書を選びます。さらにはがき印刷を選んでください。

13

 

さらに、宛名面の作成を選びましょう。

14

 

するとはがき宛名面印刷ウィザードが開きます。

15

 

種類は、年賀/暑中見舞いを選びます。

16

 

さらに様式を選びましょう。好みで縦書きでも横書きでも結構です。

17

 

フォントも選びましょう。

18

 

差出人の情報を選びます。差出人を印刷するにチェックを入れると差出人情報が印刷されます。

19

 

既存の住所録ファイルにラジオにチェックして下さい。さらに、住所録ファイル名は、先ほどAccessのmdbを保存したパスを入力して下さい。

20

 

これではがき宛名面印刷ウィザードは完了です。

21

 

すると、年賀状のイメージと各項目のフィールドに、kintoneに入力した情報が出てきます。この時点で出ているのは、当初アドレス帳に入力して、Address.mdbに入った情報です。

22

 

違う宛先を表示させるには、差し込み文書からメニューの右にある矢印をクリックすればそれぞれの情報にアクセスできます。

23

 

それではこのWord文書を保存して閉じましょう。そして、もう一度開き直すと、以下のようなメッセージが表示されます。これは毎回Address.mdbの情報を見に行き、SQL文を発行してAccess.mdbの情報を取得していることを意味します。

25

 

さて、ここまで出来ているのであれば、もう一段階進んでみましょうか。
先程Accessにモジュールを組み込んで頂いた際に、「AccessからMod_GetKintoneの中にあるgS_CmdDataRequeryClickを実行すれば、kintoneからデータを取りに行ってくれます」と書きました。では、これをWordから行えるようにすれば、いちいちAccessを起動せずに済みますよね。
そんなわけで、GetKintoneFromWordをご用意いたしました。こちらもzip化しています。解凍したらGetKintoneFromWord.basが現れます。これをWordのVisual Basic Editorからファイルのインポートで取り込んで頂ければ。すると、Wordからkintoneの取り込みを行い、その結果をAccess.mdbに取り込むことが出来ます。なお、mdbの場所は C:¥kintone¥Address.mdb にある前提で作成しています。26

 

では、組み込んだマクロを実行してみましょう。メニューの表示からマクロ→マクロの表示を選びます。

26

 

一覧が出るので「アドレス帳最新化」を選んで実行を押してください。

27

 

そうするとkintoneで変更した内容がwordにも反映されます。これで完成です♪
どうでしょう? 今ならまだ年賀状に間に合いますよ♪

29

 

 


cybozu.com conference 2015に参加して


先日、cybozu.com conference 2015 Tokyoに行ってまいりました。https://cybozuconf.com/
私にとってcybozu.com conferenceは3年半前に参加して以来となります。

その時のcybozu.com conference 2012では「世界に通用する日本の企業クラウドを作る」という青野社長の基調講演から、各種セミナーにいたるまでじっくりと参加できました。その少し前に行われたcybozu.comのプレス発表会にも参加させて頂いた私は、海外製品が幅を利かせる現状にサイボウズさんが風穴を開けてくれることに期待し、心地良い期待感と昂揚感に胸躍らせたことを覚えています。

以来、cybozu.comの成長は素晴らしいものがあります。まさに日本発のソフトウェア企業として、有数の会社になっていると思います。私もその波に乗りたかったのですが、cybozu.com conference 2012の直後から参画したプロジェクトに忙殺され、まったくユーザー会にもカンファレンスにもお伺いできずじまい。忸怩たる思いをずっと抱えていました。Kintoneの正式リリース前からkintoneβ版で開発し、正式リリースとほぼ変わらぬ時期にお客様に販売管理システムを無事納品し、以来ずっと運用している自負を持つだけになおさら。

でも、現状をどうこう言っていても仕方ありません。現状をいかにして将来に役立てるかが重要です。今回のサイボウズカンファレンス2015ではセミナー2つのみの参加でした。しかし、その2つのセミナーこそが、今私が参画しているプロジェクトでの経験を将来に役立てるにあたり、ためになると思ったので参加しました。

「サイボウズで実現するNotesマイグレーション~事例に学ぶーNotesマイグレーション成功の鍵~」
「ただしく恐れるクラウドのセキュリティ」

最初のNotesセミナーですが、私にとって現状を活かすには最適なセミナーでした。今のプロジェクト行っている諸作業のうち、Notes Designerを使ってのNotesDB作成があります。正直なところクラウド全盛の今にあって、クライアント/サーバー(クラサバ型)システムによるNotesグループウェアが通用するかは微妙です。しかし、NotesはNotesで今まで広く使われていただけに、慣れた人には使いやすいのでしょう。私にしてもNotes Designerを使い始めたのはこの1、2年ですが、拡張性や機能自体は悪くないと思います。ただ、Lotus Scriptのデバッグやエディタなどがイライラするほどユーザーに優しくないことを除けば、やれないことはあまりないといってもよいと思います。しかし、それにはスキルがいります。NotesはNotesでクラウド化を推進していると聞きますが、多くの現場では依然としてクラサバ型の構成が主流でしょう。特定のNotes技術者を育成せねばならぬところ、開発者向けの環境充実にあまり力を入れていないところからすると、衰退も致し方ないのかもしれません。

しかも、Notes Designerを触って思ったのが、テーブルいらずの発想が、クラウド製品に似通っていること。普通はテーブル設計から入り、Create文を作ってストアド・プロシージャ―などを流してDBを作り込みます。しかし、Notesはいきなりフォーム設計から入ります。フォーム設計でフィールドとして項目を埋め込んでいくことで、それがテーブルとなっていく。この構成は実は、kintoneやSalesForce.comといったクラウドの代表的なアプリにも見えます。ここらに、両者に共通する点を見つけることができました。セミナーでうかがったのですが、青野社長がサイボウズ社を創業するにあたり、Notesの管理者をされていたことがベースにあったとか。これは知らなかった。

サイボウズさんでは、Notesマイグレーション事業を行うにあたり、ターゲットとなる製品をGaroonとkintoneで想定していらっしゃるようです。そして私はkintoneユーザーかつエバンジェリストの端くれでもあります。Notes Designerの現役開発者であり、かつ、kintoneの開発者。ここに私には現状を将来に向けて活かせる突破口を見出したのです。

また、私はkintoneには慣れていますが、Garoonはまだ触ったこともない状態。今回はGaroonに取り組むきっかけになるのでは、とも思いました。例えばNotes上の文書に貼り付ける添付ファイルの位置、これもGaroon上でほぼ再現できるそうです。これは大きいセールスポイントです。

ここ3年ほど、私の人生にとって、技術力の成長面では停滞していたといわれても仕方ありません。エバンジェリストにして頂いたり、法人化を成し遂げたり、診療所を開設したり、自治会で総務部長を完遂したりと個人的にはかなりの成長を遂げることができた3年ですが、最新技術の吸収ではかなり遅れを取っています。危機感も相当持っています。今回のセミナーは、そういった意味でも勉強になりましたし、今後の自分のスキルとして営業に加えていこうと考えています。

また続いてのセキュリティセミナー。これもまた重要です。実は今までもサイボウズさんのセキュリティに関するセミナーには参加させてもらっています。しかしやはり現役の運用本部長からのお話は、紙やディスプレイで読むのと違い、頭に入ります。クラウドに興味を持っていただくお客様の第一の要件は、業務を遂行することができるか、ということです。ただ、お客様自身、あまり口には出しませんが、クラウドのセキュリティへの不安はお持ちではないかと思うことがあります。それはまた、2016年1月からのマイナンバー導入を目前にした今、ますます注視されていることでしょう。私にとって、改めてサイボウズさんの施している万全のセキュリティに対する体制を伺わせて頂いたことは、良かったと思います。今参画しているプロジェクトでも、セキュリティ遵守に関する話は統括部門だけに当然耳に入ってきます。また、運用本部長の山本氏のセキュリティについての謙虚な姿勢も良かったです。100%安全ですというセキュリティに対する態度程危ないものはないと思っています。常に努力と改善しかないのだろうな、と思います。

今回のセキュリティセミナーの受講で新たになった私の知識は、今後のクラウド提案に活かせると思っています。会場でも配布していましたが、セキュリティ&運用基盤というパンフレットは常に最新版を持ち歩いていたいと思いました。説明を求められた際には対応できるように。

現場の作業の都合もあり、青野社長をはじめとした豪華ゲストの方々による基調講演は行かれず。また、夜の池上彰氏による特別講演も聞かれませんでした。しかし、たとえわずかな時間であっても、昨年は参加すら出来なかったことに比べたら状況は回復の途中にあるといえます。私も、このまま回復基調を維持し、次なるキャリアステップに役立てたいと思います。


kintoneのレコード詳細ページからAmazon商品データを参照するテクニック(3/3)


3.kintoneからのJavaScript呼び出しとJavaScript内でのAjaxの記載について

 ここでは、kintoneへのJavaScriptの適用方法その他の説明は割愛します。はじめよう kintone JavaScript API の記述が大変参考になります。私も最初はここから勉強しました^^

 ただし、本稿でもまた、前書きを書かねばなりません。恐縮ですm(_ _)m

 それは、kintoneからAmazon API呼び出しのタイミングについてです。冒頭でも挙げたように、本稿ではレコードの詳細画面でAmazon情報を呼び出すのが目的です。しかし、レコード詳細画面の編集時画面での実装については、本稿では触れていません。できなくもないのですが、現在の2014年9月時点のkintone JavaScript APIでは、レコード詳細画面のテキストボックスの値変更時にJavaScriptを呼び出すことができません。そのため、編集時画面でAmazonからの情報を表示させる意味がないと判断し、記載から外させて頂きました。ご了承下さい。ただし、レコード編集画面のフィールド値変更時イベントについては、2015/07/12の定期メンテナンスにおけるkintone API更新で実装されました。が、本稿ではまだ検証に至っておらず、掲載しておりません。ご了承ください。

 また、Amazonからの呼び出し自体にも、全く問題がない訳ではありません。Amazon側にリクエストを続けて投げると、Http/1.1 Service Unavailableのエラーが返ってくる場合があります。なので、リクエストはほどほどに・・・ということなのでしょう。このエラー対策については、jQueryのAjaxで、Errorハンドラから再帰呼び出しを試したのですが、calleeとuse strictが両立できていません。なので、時間をおいて再実行ということでご了承ください。

 最後にクロスドメイン制約についてです。本稿の目指す実装は、*****.cybozu.com から ****.jp/ または *****.co.jpといった違ったドメインのサーバーにAjaxリクエストを発行します。が、そのような別ドメインへのリクエストは、セキュリティ上脆弱になりかねないため、環境によっては簡単に通信ができないようになっています。本稿ではphpからkintoneへ渡すデータ形式をXMLにしました。JSONPを使えばクロスドメイン制約についても解決できるようです。が、今回はXMLを選択しました。ご了承頂ければと思います。随所にそのあたりについて

 あと、jQueryの呼び出しも必要となりますので、kintoneのアプリ設定画面から、JavaScript登録を忘れずに。

 さて、気を取り直して実装開始^^

 以下は、AmazonCallBookInfoDetail.jsの内容です。200行あるすべてのコードを開示しながら、適宜解説を加えていきたいと思います。

  4行目     jQuery.support.cors = true; はクロスドメイン制約をかいくぐるための呪文の一つです。

  5行目     kintone.events.on('app.record.detail.show', function(event){
は詳細レコード呼び出し時に書く呪文です。

  6行目     実行しているブラウザの種別をここで取得しています。呼び出し先のURLがhttpsから始まっても、画像はhttpが返されることがあり、それを防ぐために一部ブラウザによって画像のアドレスを変更しています。

  8行目     表示したkintone詳細レコードのISBNフィールド(フィールド名はF_Isbn)の値を取得しています。

  9行目     8行目で取得したIsbnから – ハイフンを除去しています。

  10行目     8行目で取得したIsbnから  ISBN を除去しています。

  11行目     kintoneフォーム上で事前に作成しておいたスペースをここで取得しています。このスペース内にAmazon からの情報を書き込んでいきます。

  12行目     kintoneフォーム上で事前に作成しておいたスペースをここで取得しています。このスペース内にAmazon からの大きな画像を表示させます。

  15-17行目 本稿で実装する内容では、ユーザ入力データからのサニタイズ処理はあまりありません。唯一この場所で実施しています。数値がどうかをチェックしていますが、ISBNの末尾一桁のチェックディジット結果が X となる場合があり、その場合のみ許可しています。

  19行目     12行目で取得した画像表示用イメージの横幅を指定しています。

  20行目     11行目で取得したAmazonデータ表示用スペースにユーザを飽きさせないために表示しております。

 

 

  24行目     先に紹介しましたが、codeとIsbnという2つのパラメーター以外に、nowというパラメーターも追加しています。実はnowパラメーターは以下の処理では使われていません。投げるリクエストURLを常に変化させないとInternet Explorer上でうまく値が戻ってこないという情報から、このような記載を設けています。

  25行目     ここは xml を指定しています。おそらくはjsonpでもjsonでもうまくいくことでしょう。

  26行目     キャッシュをOFFにしないとInternet Explorerで正常に戻ってこないという情報を基に追加しました。

  27行目     非同期処理をONにしています。ここをOFFまたは記載なしにすると、何らかのエラーがAjaxで発生した場合、ブラウザがフリーズしてしまいます。

  28行目     10000ミリ秒、つまり10秒間だけphpからの戻り値を待ちましょう。という指定です。

  29行目     正常にデータが戻ってきた場合の処理を30行目以下に記載します。関数の引数dataが、成功した場合に受け取るデータが格納された変数です。

 

 

  30-79行目     商品情報に関するすべてのAmazonからの戻り値を変数として宣言してあります。正直、ここまでやる必要はありません。ありませんとも。

  82行目以下は、実際のXMLの内容を基に、DOMの森を探索することになります。なので、以下ではXMLを実際に表示させてみます。

  この中で実際に表示させるのに必要なのは、32行目の<Item>タグ以下のデータとなります。

  53-101行目までは各種商品イメージの情報が記載されています。この中で53行目から67行目までのSmallImageとMediumImageとLargeImageは。その下の68行目から101行目までの間に同一の情報があります。なので、この部分は取得を省略してもよいでしょう。

  102行目以下は主要な情報が並んでいます。これらはほぼ取得すべきところです。ただし、データの種類によってはタグ自体がないものもあります。以下の例でいうと、104行目の<Creator>タグは小説のように著者がある本には登場しません。著者がある本は<Author>タグが替わりをつとめます。また、112行から117行の<Languages>タグや135行目の<ReleaseDate>タグがないデータがあることも確認しています。

 もう一件、AmazonからのXMLデータで気を付けるべき点があります。それは書籍とkindle書籍の場合です。kindle書籍はISBN項目を持っていないのですが、Amazonの仕様でISBNに対してデータ検索結果にkindleデータも含まれます。XML上では、2つの<Item>タグが紐付いています。その場合は、JavaScript側で<Item>タグ内をループさせるのですが、<ISBN>タグの有無で判別しています。

 大体のXMLデータの構造がおわかり頂けましたでしょうか。先に紹介したAmazonのガイドですが、項目の位置はこちらでガイドされています。ただし、各項目がどのように商品データに関連付いているかはわかりません。なので、上に挙げた項目以外にも存在することもあるかと思います。ご容赦ください。

  82行目     jQueryの文法に則り、XMLのデータが格納された変数dataに対し、<item>タグの要素を検索しています。その結果をループしたのが82行目の記述となります。

  83行目     条件文が真の場合、84行以下の処理を実行しています。条件文の頭に$(this)とありますが、これは 82行目でループを開始した<item>タグの要素データを指します。<item>タグ内でさらに<ItemAttributes>タグを検索し、さらにその中で<ISBN>タグを検索しています。この構造については、3ページ前のXMLをご参照ください。<ISBN>タグの要素内のテキストを取得しているのが text()です。
つまり、ISBNの要素の有無によって判定を分けています。これは先にXMLの構造を説明する際にも書きましたが、kindle書籍データが含まれている場合の退避処理となります。

  84-85行目     <item>タグの要素データ内の<ASIN>タグや<DetailPageURL>タグの要素内データを取得しています。

  86行目     82行目で始めた<item>タグ内ループの中で、さらに<ItemLink>タグを検索し、さらに<Description>タグを検索し、それをループしています。

  87行目     86行目で始めた<Description>タグ内ループの中で、要素内の文字列が Add to Wish Listに等しいかを尋ねています。

  88行目     87行目で条件に合致した場合、<Description>タグと同じレベルの隣のタグ<URL>タグの要素内のデータを取得しています。

  90-98行目 87、88行目と同様です。それぞれの<URL>タグの内容を取得しています。

  99行目     86行目で始めたループを閉じています。

 

  100行目     82行目で始めた<item>タグ内ループの中で、さらに<ImageSet>タグを検索し、それをループしています。

 

  101-118行目    100行目で始めた<ImageSet>タグ内ループの中で、それぞれの要素タグの内容を取得しています。

  119行目     100行目で始めたループを閉じています。

 ここまで来たら大分要領がつかめてきたのではないでしょうか。あともう少しで終わりです。お疲れのことでしょう。
私も大分疲れてきました・・・(^^;)

 120-146行目     それぞれの要素内データを取得しています。もう説明は不要ですよね。ただ、121行目ですが、Attrという見慣れない記述があります。この部分に対応しているのは XMLの104行目に当ります。<Creator>タグ内に Role="監修"というRole属性の値を取得するために、Attrという命令を発行しています。これを末尾につなげることで、 サイボウズ式編集部 監修 という文字列を組み立てているわけです。

 

  147行目     83行目の<ISBN>タグの有無を判定したif文の終わりです。

 

  148行目     82行目で始めたループを閉じています。

  149行目     aws_ASINの値が真かどうかを判定しています。cybozu.com developer networkのTipsの中に(小技)undefined と空文字のスマートな if 文判定がありますが、そちらの記述通り、文字列の取得結果がnull または undefined以外のデータの場合、150行目以下の処理を実行します。

  150-173行目   11行目で取得したスペースに対し、innerHTMLを使用して書き込むべき文字列を生成しています。今回の実装では、一つのスペース内に改行で区切ってAmazonからの情報を羅列する手法を採りました。冒頭に記載しましたが、編集時画面に実装し、Isbnの値の変更時に各フィールドに自動入力するほうが望ましいという考えもあります。なお、169行目から173行目は取得したURL文字列を<a>タグの中に含めています。

  174行目     6行目でブラウザの種別を取得しました。取得したブラウザ文字列にfirefoxが含まれているかどうか、要は実行しているブラウザの種類を問うているのがこの行です。

  175行目     使用ブラウザがfirefoxの場合、12行目で取得したスペースのinnerHTMLに対して書き込む画像の http: を 空白文字 にしています。Firefoxだけが、https:// から https:// の画像を読み込もうとするとエラーが出るため、このような処置を取っています。

  176-178行目     使用ブラウザがFirefox以外の場合、12行目で取得したスペースのinnerHTMLに対して書き込む画像のURLをそのまま使用しています

  179行目     149行目で判定したaws_ASINの値が偽だった場合に、180~184行目の処理が動作します。なお、Amazon側に渡したISBNコードが不正な値だった場合、XMLは違う種類のメッセージを返します。ほとんどの部分は一緒なのですが、<Items>タグの中の<Item>タグが<Errors>タグに置き換えられています。以下にエラー時のXMLを提示します。

 

  180-184行目     11行目で取得したスペースにエラー情報を含めます。上の図にあるとおりのエラー構造に基づいて、適宜整形して頂ければと思います。

 

  185行目     149行目で判定した条件式の終わりの部分です。

  186行目     29行目で判定したAmazonから正常な値が戻ってきたかどうかの判定式の終わりの括弧です。

  187行目     Ajax通信に何らかのエラーが生じた場合、処理はこちらのステートメント内で行われます。関数の引数として、XMLHttpRequest、textStatus、errorThrownといった値が返されますので、その値に応じてメッセージを整形されるとよいでしょう。

  193-195行目     errorThrownは場所の数だけ配列として戻ってきますので、193~195行では配列の要素数だけ処理を行っています。

  196行目     ここでは取得したエラー情報をアラートとしてポップアップ表示しています。Amazon情報を表示するスペースに表示するのもよいでしょう。

  197行目     187行目からのエラー時に実行される関数の終わり部分です。

  198行目     22行目からの$.ajaxの関数の終わりの括弧です。

  200行目     5行目からのkintone詳細レコード表示時に実行される関数を終えるにあたり、戻り値を返しています。

  201行目     5行目からのkintone詳細レコード表示時に実行される関数の終わりの括弧です。

  203行目     本JavaScript全体の無名関数の終わりの括弧です。

これでJavaScriptの実装は終了です。お疲れ様でした。お互い・・・

 さて、JavaScriptを完成させ、あとはこれをkintoneのアプリ管理画面からアップすれば完成!

 のはずですが・・・・・実は先ほどのJavaScriptの仕組みでは動かないブラウザがあります。それは。。。。Internet Explorerです。jQueryのAjax関数では、Internet Explorer 9より前のバージョンに対応しておらず、別の仕組みを経由してAjaxを実装する必要があります。なんてこった!またまたクロスドメインではまることになりました。

 Googleで検索を行うと、いくつかInternet Explorer 9以前のバージョンでのクロスドメイン実装についてのJavaScriptが存在します。私もいくつか試しましたが、私の環境ではそのうち動いたのが以下の1つだけでした。

 MoonScript/jQuery-ajaxTransport-XDomainRequest

 なので、今回はこちらのJavaScriptも使わせて頂きました。ありがとうございます。

 結果として、kintone上にアップするJavaScriptファイルは3種類となりました。最初にjQuery本体。次にIE9以前対応のクロスドメイン制約対応のJavaScript。最後に今回作成したJavaScriptです。

 これで完成です。

 と。このように表示が出来ましたでしょうか。レコードを移動すると新たなレコード先でAmazonから呼び出されたデータが表示されます。ただし、あまり短い時間で呼び出すとAmazonからの応答が途絶えてしまうことがあります。その場合は F5でリロードを行って頂ければと思います。

 外部認証が必要な複雑なAPIを呼び出す場合、このような方法があるよ、ということを理解頂ければ、さまざまなケースで実装が可能になり、kintoneをより一層ご活用頂くことができることでしょう。ぜひ、お試しください。ただしくれぐれも過度なAmazonへのリクエストはお控えください。1時間当たりのリクエスト回数も上限が定められているようですし・・・・

 本件について、文責は全て私、合同会社アクアビットの長井にあります。もし内容についての御質問などございましたら、お寄せいただければ、時間の許す限り、回答させて頂くようにいたします。


kintoneのレコード詳細ページからAmazon商品データを参照するテクニック(2/3)


2.phpを利用したAmazonへのパラメーターの生成方法について

 最初に一言申し添えておかねばなりません。なぜkintoneからAmazonの情報を取得するのにphpを介さなければならないか、について。

 cybozu.com developer networkをご覧の皆様は、すでに有識者の皆様が執筆された、秀逸なTipsの数々にも目を通されたことでしょう。Tipsの中には

  外部APIの同期処理をつかってみよう!

  他サービスのAPIからデータを取得してkintoneに表示してみよう

 という2つのkintoneから外部APIに接続するTipsも挙げられています。私も本稿を書くにあたって参考にしました。ありがとうございますm(_ _)m。

 これらのTipsはkintoneのJavaScript APIを使用しています。JavaScript内に外部APIのURLを直接書き込み、その結果を表示させる方法です。一方、Amazon APIを呼び出す際のURLですが、その中には全章で取得したAmazonの「Access Key ID」「Secret Access Key」「トラッキングID」の3種類から生成した署名文字列を含める必要があります。つまり、署名文字列を生成するにあたっては、JavaScript内に「Access Key ID」「Secret Access Key」「トラッキングID」という機密情報を埋め込まねばなりません。それを避けるために、外部のphpに署名文字列の生成や機密情報の扱いを任せようというのが狙いです。

 能書きが長くなりました。それでは実装に移らせて頂きます。

 まず、kintoneに埋め込むJavaScript内で呼び出すAPIのURLを記します。

パラメーターは ?code=ISBN&no='+Isbn としています。Isbnとあるのは、kintoneのフィールドに入力された本のIsbnを変数化したものです。Amazon認証情報に関する情報の一切はJavaScriptからは省かれています。

 次にパラメーターを受け取ったAmazonBookInfo.phpの中身を示します。

 JavaScriptに値を返すための各種ヘッダの記述と、Amazonから情報を取得する本体のphpであるSetBookByAmazon.phpの呼び出しが書かれています。ヘッダについてはGoogle ChromeとFirefox、そしてInternet Explorer 9で正常にデータが渡せることが確認できています。これは環境によって違いますので、工夫してみてください。特に4番目のヘッダで application/xml となっています。これはJavaScriptに対して渡すデータの種類を xmlとして規定する部分ですから、重要です。

 最後にパラメーターを受け取ったSetBookByAmazon.phpの中身を示します。

 これで動くはずです。あ、Access Key IDとSecret Access Key IDとアソシエイトTagはこんなふざけたものではなく、きちんとAmazonで取得した文字列を使って下さいね^^。

  define('AssociateTag','anatanoiddesu-99'); の赤字の部分を前章で取得した「トラッキングID」で置き換えてください。

  define('AccessKeyId','ICHIBANSAISHINNOAKUSESUKII');  の赤字の部分を前章で取得した「Access Key ID」で置き換えてください。

  define('SecretAccessKey','DAIJINADAIJINASIIKURETTOAKUSESUKIIDESUNENN');   の赤字の部分を前章で取得した「Secret Access Key」で置き換えてください。

 それ以外は、この内容でデータが取得できるはずです。このphpをサーバーにアップし、URLをアドレス欄に打ち込んでみてください。例えば以下のように。

 どうでしょう。XML形式でデータが表示されたのではないでしょうか。そう、あの本の情報が\(^o^)/

 なお、渡すパラメータや取得できる情報の種類はかなり多岐に亘っています。本稿ではISBNのみに焦点を当て、ISBNから取得可能な書籍情報のみについて紹介したいと思います。上に挙げたphpの内容もISBNによる取得に対応しています。Amazon Products Advertising APIの単一商品取得の ItemLookUpはこちらのリンクが公式情報となっています。

 上のphpの内容に ・・・・・・あ といった記述があります。くれぐれもこの部分はphpファイルからは除いてくださいね。動かなくなってしまいますので^^。

 以下は、その あ~か についての説明を記します。

 ・・・・あ その上の行と共にJavaScriptから渡ってきた変数が格納される場所です。

 ・・・・い Amazonから取得する情報の種類を指定します。ItemAttributesは商品詳細、Imagesは画像情報。このように複数の種類を指定する場合 , カンマでつなげます。

 ・・・・う 指定した各種パラメーターをそのパラメーター名称で並び替えます。署名作成時に必要な処理です。

 ・・・・え 指定した各種パラメーターをエンコードします。RFC3986で定められた方法に準じています。

 ・・・・お 署名文字列の生成部分です。ここで初めてSecret Access Keyの活躍の場が与えられます。ハッシュ関数によって、容易には結果から元々の文字列が推測できないような文字列に変換されます。

 ・・・・か 組み立てられたUrlから取得した文字列を取得します。結果文字列はXML形式で帰ってきています。

 ・・・・き 帰ってきた文字列を表示します。この表示処理によって、呼び出し元へ文字列が戻されます。

 これで、Access Key ID と Secret Access Key、トラッキングIDの3種類の情報とISBNからAmazonの書籍情報がデータ取得できました。あとはこれをJavaScriptで加工するだけです。あと一息です!もうしばらくご辛抱ください!!

 最後に、こちらで作成した SetBookByAmazon.php と AmazonBookInfo.php ですが、サーバーにアップする際、https://の領域ではなく、https:// の方にアップされたほうが良いかと存じます。

なぜかというと、呼び出し元となるkintoneのURLは https://******.cybozu.com/k/12/ とhttps://から始まります。これに対し、上記2つのphpファイルが https:// から始まる領域から始まると、ブラウザによっては好ましからざる動きをするからです。


kintoneのレコード詳細ページからAmazon商品データを参照するテクニック(1/3)


はじめに

kintone アプリで資産管理を行いたい、という要望はよく聞きます。その際、既存サイトのデータが参照できたら便利ですよね。例えば商品データを参照したい場合、Amazon APIを通して取得したいといった要件は十分考えられます。本稿では、kintoneの詳細レコード上に、Amazon APIから取得したデータを表示する方法について紹介します。

少々長くなってしまうので、簡単に本稿の構成を書いておきます。

1.AmazonのAccess Key ID と Secret Access Key、トラッキングIDの取得方法について

2.phpを利用したAmazonへのパラメーターの生成方法について

3.kintoneからのJavaScript呼び出しとJavaScript内でのAjaxの記載について

その前に・・・本件を実現するために必要なものがあります。それは例えば・・・

  1. パソコン。ノートでもデスクトップでもよいですが、スマートフォンや俗にいうガラパゴス携帯、またタブレットからの利用は考慮していません。
  2. パソコンのブラウザ。本件ではGoogle Chrome、Firefox、Internet Explorer9、11で検証しています。その他のブラウザでは予測不可能な動作に遭遇するかもしれません。もちろん、他のブラウザでも動けばそれに越したことはありません。ノウハウはぜひ教えて頂ければ^^;)・・・。とりあえずこの4種でお願いします。
  3. phpの動作するサーバー。今回の検証で使用したレンタルサーバーは、php.iniも変更できず、pearなどのライブラリも利用できません。SSLも専用でははく共用SSLを使用しています。phpネイティブだけを頼りに構築しているので、サーバー間の動作差異はあまりないと思えます。なお、レンタルサーバのOSはUnix系で、Apache 2.2で動いているそうです。phpのバージョンは5.3.2です。
  4. kintoneの画面ですが、最低限3つのフィールドが必要です。Isbnを入力するためのフィールド(名称:F_Isbn)を追加し、Amazon情報表示用のスペース(名称:Space_Image)と画像表示用のスペース(名称:Large_Image)を追加してください。
    表にまとめると以下のような感じとなります。

    フィールドタイプ フィールド名 フィールドコード 説明
    文字列(1行) ISBNコード F_Isbn ISBNコード用
    スペース Space_Image Amazon情報表示用スペース
    スペース Large_Image 画像表示用スペース

    (※以下の結果画面は違うフィールドも多数入っていますが)

    それらを満たし、かつ、kintoneにカスタマイズを加えると、以下のように詳細画面にAmazonからの呼び出し結果が表示できるようになります♪

最後になりますが、本稿は2014/10月に書かれました。その時期のkintoneとAWSの仕様に合わせています。cybozu.com developers networkへの掲載に合わせ、サイボウズ社のご担当者のチェックを経て、2015/2月のkintoneのバージョンアップ内容と本稿の記載を照合させた上で投稿しています。2015/7現在、まだ同じ仕組みで動作しているので、本稿の内容に問題ないとは思われますが、今後のkintoneおよびAWSのバージョンアップによっては本稿の内容が正常に動作しない恐れもあります。ご承知の上、ご参考になさって下さい。

1.AmazonのAccess Key ID と Secret Access Key、トラッキングIDの取得方法について

 https://www.amazon.co.jpにアクセスしましょう。まずはここから。

 次にアカウントを既にお持ちの方はログインし、まだの方はアカウントの新規作成をお願いします。

 アカウントでログインすると、下の図の赤枠が こんにちは ○○○さん という表示になっています。

 その赤枠部分をクリックすると、アカウントの管理画面に移動します。

管理画面では、アフィリエイトの報酬結果を見たい気持ちをこらえ、ページの下端まで移動してください。すると下図のような記載が見つかると思います。その中のアソシエイト(アフィリエイト)をクリックしてください。

 下図は、アソシエイト・セントラルの画面です。左上にIDのようなものが書かれています(赤枠囲み)。これがkintoneからAmazonへアクセスする際に必要な情報の一つ「トラッキングID」です。是非φ(..)メモしておいてください。

 続いて、上図のメニューバナーの中に Product Advertising APIというのがあります。そのバナーをクリックしてください。Product Advertising APIのページに移動します。

 Product Advertising API のページ内にはAmazonへのAPIアクセスの奥義が沢山書かれています。是非 お読み頂ければと思います。が、ここは先を急ぎましょう。

 ページの中ほどに、下図のようなリソースというコーナーが設けられており、その中にアカウント設定というリンクがあります。これをクリックします。

 アカウントサービスのページに移動しました(下図)。アクセスキー情報という枠の中に、こちらのリンクというリンクがあります(下図の赤枠です)。これをクリックしてください。

 ダッシュボードのページに移ったと同時に、ダイアログボックスが登場します(下図)。Amazonでは、アカウント管理についてはIAMというサービスを推奨しています。きめ細やかな各種権限設定が可能になっているようです。しかし、今回のAmazonへのAPIに当っては、最初にAmazonにログインした際に使用したアカウント(IAMではルートIDと呼んでいるようです)の認証情報を使用する必要があります。なので、先にContinue to Security Credentialsの方をクリックして下さい。

 下図のようなYour Security Credentialsの画面が表示されました。右側の + Access Keys(Access Key ID and Secret Access Key)をクリックして下さい。

 下図のようにメニューが開きました。Important Changeとメッセージがあります。これはSecret Access Keyの取扱いが変更になり、以前から持っていたAmazonアカウントに紐付いたAccess Key IDでは、再取得を行わないとSecret Access Keyが使用できないという意味です。なので、新たなAccess Key IDを作成しました。新たなAccess Key IDの作成は下図のCreate New Access Keyをクリックすると発行できます。発行が終われば、前のAccess Key IDはDeleteしてください。

新たなAccess Key IDを発行した画面が下図です。Show Access Keyをクリックすると、今回kintoneからAmazon APIへアクセスする際に必要な情報の二つ「Access Key ID」「Secret Access Key」が表示されます。是非φ(..)メモしておいてください。Download Key Fileをクリックすると、csvファイルで2つのIDが保存できます。

 これで、Access Key ID と Secret Access Key、トラッキングIDの3種類の情報が取得できました。3種類とも是非φ(..)メモしておいてください。 図が多くて見にくかったことでしょう。お疲れ様でした。