そのうち誰かの役に立つ

もしくは誰の役にも立たない

Google Apps Script(GAS)とLINE Messaging APIでユーザアカウント不要のイベント受付システムを作る -3- Googleドキュメントでチケットを作成する

続きもの。

  1. 準備
  2. Googleフォームの回答と編集URLを取得する
  3. Googleドキュメントでチケットを作成する ←今ココ
  4. Gmailでメールを送信したりLINEにPushメッセージを送ったりする (2019/11/01更新)
  5. まとめ (2019/11/05更新)

Googleドキュメントでチケットを作成し、管理する。

用語

以降、特に説明なくこれらの用語を使う。

オブジェクト

Googleドライブの中身の識別単位。 「どういった種類か」ということを意識しないので、その実体はGoogleフォーム、GoogleスプレッドシートGoogleドキュメント、その他のファイルなど多様だが、 とにかく「そこにある1つのもの」として認識される。

オブジェクトID

Googleドライブに格納したオブジェクトを識別するID。 直感的にはブラウザで個々のオブジェクトを表示したときのURLのこの部分(下記 {} で括られた部分)と覚えておけばよい。

https://docs.google.com/forms/d/e/{0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_}/viewform

フォルダID

Googleドライブのフォルダを識別するID。 直感的にはブラウザで各フォルダを表示したときのURLのこの部分(下記 {} で括られた部分)と覚えておけばよい。

https://drive.google.com/drive/u/1/folders/{0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_}

チケット格納フォルダの作成

Googleドキュメントで作成したチケットをユーザに提示する場合、ユーザには閲覧権限だけを渡して編集権限を渡さないようにする必要がある。 個々のドキュメントに対してそのように権限設定をすることも可能だが、オブジェクトは特に指定がない限り配置フォルダの共有設定に従う性質を利用して、 ゲストに対して閲覧権限だけ付与したフォルダ配下にドキュメントを置く方法がある。

フォルダの共有設定を開く方法は複数ある(開くものは同一)。

  • フォルダに対して右クリックメニュー
  • フォルダを選択して右上の 共有 アイコン
  • フォルダを表示してドロップダウンメニュー

共有メニューを開いたら 詳細設定 から アクセスできるユーザー の変更をクリックし、 リンクを知っている全員閲覧のみ可 となるように設定する(下図)。

Shared Link
共有設定

チケットデザイン

今回作るチケットのデザインだが、シンプルに

  • ヘッダ画像
  • ユーザ情報(名前、来場者数など)
  • フッタ画像

が表示されるものとする。

チケット管理

ユーザにより予約の変更があったときには新しいチケットを発行し、古いチケットを無効化する必要がある。 そのようなチケット管理を実現するためには同じユーザからの回答であることを識別する必要があるが、 今回はそれを「回答編集URLが同一であること」と定義する。

1つの回答編集URLに対し1つの(チケットの)オブジェクトIDを紐付けるようなことができればいいのでKey-ValueなDBがあればよいが、 今回はそれをGoogleスプレッドシートで実現する。 スプレッドシートに回答編集URLと作成したチケットのオブジェクトIDのペアを追記していくトランザクションログもどきを作成し、 追記する際にログを一通り見て同一回答編集URLのレコードがあった場合は、そのレコードに記録されているオブジェクトIDを持つチケットを無効化する処理をする。 これは追記に現在のレコード数だけ処理が必要で、最終的なレコード数  N に対し  O(N^2) となるような割と頭の悪い処理をしているが、 会場キャパからしてせいぜい数百件オーダーの予約しか来ないと判断し特に問題なしとしている。

スクリプト

Googleドキュメントでチケットを作成するスクリプトは下記。 前記事までにフォームの回答 answers と回答編集URL edit_url が取得できているので、それらを用いる

// チケット格納フォルダID
// 共有設定でリンクを知っている人のみが閲覧可能にしておく
var ticket_folder_id = PropertiesService.getScriptProperties().getProperty("FOLDER_ID") || "";
// チケットヘッダ画像オブジェクトID
var ticket_header_image_id = PropertiesService.getScriptProperties().getProperty("HEADER_ID") || "";
// チケットフッタ画像オブジェクトID
var ticket_footer_image_id = PropertiesService.getScriptProperties().getProperty("FOOTER_ID") || "";
// チケット管理用スプレッドシートオブジェクトID
var ticket_sheet_id = PropertiesService.getScriptProperties().getProperty("TICKET_SHEET_ID") || "";

function createTicket(answers, edit_url) {
  // チケット用Googleドキュメントの作成
  var title = answers["お名前"] + "様チケット"
  var tmp_doc = DocumentApp.create(title);
  var ticket_id = DriveApp.getFileById(tmp_doc.getId()).makeCopy(DriveApp.getFolderById(ticket_folder_id)).getId();
  DriveApp.removeFile(DriveApp.getFileById(tmp_doc.getId()));
  var ticket_doc = DocumentApp.openById(ticket_id);
  ticket_doc.setName(title);
    
  var ticket_body = ticket_doc.getBody();
  
  // ヘッダ画像挿入
  if (ticket_header_image_id != "") {
    var ticket_header_image = UrlFetchApp.fetch("https://drive.google.com/uc?export=view&id=" + ticket_header_image_id).getBlob();
    ticket_body.insertImage(0, ticket_header_image);
  }
  
  // ユーザ情報記入
  var ticket_txt = ticket_body.editAsText();
  ticket_txt.appendText(answers["お名前"] + " 様\n");
  if (answers["ご来場予定者数"] != "キャンセル") {
    ticket_txt.appendText("ご来場予定者数: " + answers["ご来場予定者数"] + "名");
  } else {
    ticket_txt.appendText("ご来場予定者数: 0名");
  }
  ticket_txt.setFontSize(24);
  
  // フッタ画像挿入
  if (ticket_footer_image_id != "") {
    var ticket_footer_image = UrlFetchApp.fetch("https://drive.google.com/uc?export=view&id=" + ticket_footer_image_id).getBlob();
    ticket_body.appendImage(ticket_footer_image);
  }

  // チケット管理
  if (ticket_sheet_id != "") {
    // チケット管理スプレッドシートの取得
    var sheet = SpreadsheetApp.openById(ticket_sheet_id).getSheets()[0];

    // チケット一覧を一段下げる
    var logs = 1000;
    var upper_row = sheet.getRange(2, 1, logs, 2);
    var lower_row = sheet.getRange(3, 1, logs, 2);
    lower_row.setValues(upper_row.getValues());

    // 同一edit_urlの古いチケットドキュメントを管理表から削除する
    var old_transaction = lower_row.getValues();
    for (row = 0; row < logs; row++) {
      if (old_transaction[row][0] == edit_url && old_transaction[row][1] != "") {
        DriveApp.getFolderById(ticket_folder_id).removeFile(DriveApp.getFileById(old_transaction[row][1]));
        sheet.getRange(3 + row, 2, 1, 1).setValues([[""]]);
      }
    }

    // チケットリストの最上段にデータを挿入する
    var top_row = sheet.getRange(2, 1, 1, 2);
    top_row.setValues([[edit_url, ticket_id]]);
  }

  var ticket_url = ticket_doc.getUrl();
  return {"id": ticket_id, "url": ticket_url}
}

リファレンスは全部貼るととんでもなく多くなるので一部だけ出すと、 こことか こことか こことか こことか

いくつか要点を書いていく。

  • PropertiesService.getScriptProperties().getProperty() 関数はスクリプトのプロパティ変数を取得することができる。 どこにも出さないならハードコーディングでもいいが環境変数のように使えるので何かと便利。 プロパティ変数はスクリプトエディタで ファイル > プロジェクトのプロパティ から スクリプトのプロパティ で編集できる。
  • DocumentApp.create() 関数はオブジェクトをルートフォルダに作る仕様なので、それを格納フォルダにコピーする必要がある。
  • ヘッダ画像の挿入は insertImage() 関数で、フッタ画像の挿入は appendImage() 関数を使ったのは挿入時に段落の変更が発生するかの違いだった気がする。
  • チケット管理でレコードの追記を一番上にするのは、過去に getLastRow() 関数で最終行を取得してその下に追記というスクリプトを書いたらなんのバグか同じ行を延々と更新し続けてしまった失敗があったため。 そのあたりのケアを考えるくらいなら適当な行数をまとめて一段下げて一番上に追記した方が楽と判断した。

最後に

このシリーズで作成したシステムが動いている演奏会はこちらです(宣伝)

2019年11月30日(土)開場:13時00分  開演:13時30分
すみだトリフォニーホール 小ホールにて(東京都墨田区)
【アクセス】JR「錦糸町駅」北口より徒歩5分/東京メトロ「錦糸町駅」3番出口より徒歩5分
入場無料 事前予約制