LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

GoogleAppsScript(GAS)でGoogleフォームを魔改造した

こんにちは。木村 修平@LIFULLジンジニア(エンジニア人事) (@kimkimniyans) | Twitter です。

昨今エンジニア経験者が開発部門以外のポジションで仕事することも増えてますね。
そんなエンジニアあるあるで、

ちょっとスクリプト書くだけで周りから魔法使いか神扱いされる

っていうのがありまして。
例に漏れずそんなことがあって大変うれしいのですが、悦に浸るだけではもったいないので何したか吐き出しておきます。 GAS初学者の方にもお役に立てれば。
 
 
お題は、『インフルエンザ予防接種の希望日程を申し込むフォーム』です。 これまでは希望に応じて日程ごとに振り分けを手作業でやっていたそうで、すごい時間かかっていたようです。
   

画面イメージとざっくり仕様

 
f:id:LIFULL-kimuras:20181105173610p:plain

  • 回答送信時に、希望日程ごとの定員上限数と突き合わせして定員上限に達していればフォームから選択肢削除。
    例)11/1(木)15:00-16:00、11/1(木)16:00-17:00、11/1(木)17:00-18:00、11/2(金)15:00-16:00、11/2(金)16:00-17:00、11/2(金)17:00-18:00
  • 設問は日程の選択肢プルダウンとその他リクエストのテキストで両方非必須。
  • 選択肢から削除された場合、削除された旨をchatwork(専用のグループチャット)に通知。
  • すべての選択肢が削除された場合、全日程満員である旨と次の設問にリクエストを記載してほしい旨の選択肢を追加。その選択肢の定員数上限はもちろんなし。

今回は送信タイミングでの回答数チェックはしていません。なので厳密には上限を超えることがあります。

FormApp使ってガシガシっと

いくつかの.gsファイルに分割したのでそれごとにコメントで説明します。
片手間タスクだったのでレビューも受けてないし雑でごめんなさい。

公式リファレンスはこちらです。

main.gs

グローバル変数である必要ないけどまとめて記載。global領域だとconstで定数宣言できない(?)みたいです。

var ROOM_MAXIM = 40;  // 日程ごとの最大人数
var ARY_CHOICE_VAL = ["11/1(木)15:00-16:00", "11/1(木)16:00-17:00", "11/1(木)17:00-18:00", "11/2(金)15:00-16:00", "11/2(金)16:00-17:00", "11/2(金)17:00-18:00"];  // 選択肢VALUEを配列に確保
var CHOICE_LENGTH = ARY_CHOICE_VAL.length;  // 選択肢の数
var TMP_NEW_CHOICE_VAL = "全日程満員です。次の設問で希望の日程を入力してください";  // 通常選択肢が全て無くなった後に追加する選択肢VALUE
var STR_TARGET_TITLE = "希望日程";  // 処理の対象とする質問タイトル名

/**
 * フォーム送信時イベント(初期処理)
 */
function onSend(e) {
  reBuildItem(e.response.getItemResponses());  //functionの引数eからイベント発生時の回答時のレスポンス取得して受け渡し
}

onSendメソッドをプロジェクトのトリガー(フォーム送信時イベント)に設定しています。
編集タブ→「現在のプロジェクトのトリガー」で設定できます。

ui.gs

回答データとの突き合わせとUI項目の操作。前者は別メソッドにしたいっちゃしたいけどまぁいいです。回答データの取得もここでコールしてます。

/**
 * 選択肢の再構築
 * 
 * @param {array} 選択肢ごとの回答カウントデータ配列
 */ 
function reBuildItem(rAnswer) {
  var form = FormApp.getActiveForm(); //アクティブなformオブジェクトを取得
  var items = form.getItems();  // フォームのUI項目を取得
  var allAnswer = getAnswer();  // 全回答の取得
  // 今回の回答を取得(※プルダウンUIで未選択の場合は回答データに含まれない模様です※)
  var nowAnswerVal = rAnswer[0].getResponse();
  var nowAnswerTitle = rAnswer[0].getItem().getTitle();
  
  if (nowAnswerTitle == STR_TARGET_TITLE) {  // 「希望日程」プルダウンで未選択の場合は何もしない(項目タイトルで判断)

    for (var i = 0; i < items.length; i++) {  // UI項目分ループ
      var item = items[i];
      var itemTitle = String(item.getTitle());
      var itemType = String(item.getType());  // UI項目のタイプ

      if (itemType == "LIST" && itemTitle == STR_TARGET_TITLE) {   // リストボックスかつ項目タイトルが対象の場合
        var choiceArray = [];
        var arrayCount = 0
        var flgChoiceNone = false;  // 回答突き合わせ後選択肢が残るか否かのフラグ

        for (var j = 0; j < CHOICE_LENGTH; j++) {  // リスト選択肢分ループ
          var choiceVal = ARY_CHOICE_VAL[j];
          var flgSet = false;  // 上限内か否かのフラグ
          
          if (flgChoiceNone == false) {  // 選択肢がまだ残っている場合
            if (j == 0 && nowAnswerVal == TMP_NEW_CHOICE_VAL) {  // 選択肢が全て無くなった後の追加選択肢のチェックはスルーする
              flgChoiceNone = true;
            } else {
              if (allAnswer[j] < ROOM_MAXIM) {  // 回答上限チェック
                flgSet = true;
              }      
            }
          }

          if (flgSet == true) {  // 回答上限に達してない選択肢は退避
            choiceArray[arrayCount] = choiceVal;
            arrayCount++;
          } else {
            if (choiceVal == nowAnswerVal) {  // 今回の回答だった場合
              sendChatwork(nowAnswerVal);  // 満枠になったことをchatwork通知する
            }
          }
        }

        if (arrayCount == 0) {  // 選択肢が全てなくなった場合
          choiceArray[arrayCount] = TMP_NEW_CHOICE_VAL;  // 追加選択肢をセット
        }
        // リストボックス作り直し
        item.asListItem().setChoiceValues(choiceArray);
      }
    }
  }
}

 
ちなみに、''Logger.log(hoge);''でログ出力できます。デバッグに必須です。
実行後、表示タブ→ログから確認できます。

answer.gs

回答データの全取得と選択肢ごとにカウントして返します。

/**
 * 回答データの取得
 * 回答データが取得できない(例外の場合)エラースロー
 * 
 * @return {array} 選択肢ごとの回答カウントデータ配列 
 */
function getAnswer() {
  var form = FormApp.getActiveForm();
  var formResponses = form.getResponses();  // 全回答内容を取得
  var aryAnswerCount = [];   // 選択肢分配列定義
  for (var k = 0; k < CHOICE_LENGTH; k++) {  // 配列をゼロで初期化(fillメソッド使えないので)
    aryAnswerCount[k] = 0;
  }
  var flgAnswer = false;
  
  for (var i = 0; i < formResponses.length; i++) {
    flgAnswer = true;
    var formResponse = formResponses[i];  // 回答ひとつ分を取得
    var itemResponses = formResponse.getItemResponses();  // 質問項目を取得
    
    for (var j = 0; j < itemResponses.length; j++) { // 回答内容をひとつずつチェック
      var itemResponse = itemResponses[j];
      var question = itemResponse.getItem().getTitle();
      var answer = itemResponse.getResponse();

      if (question == STR_TARGET_TITLE) {  // 項目タイトルが対象の場合
        var x = ARY_CHOICE_VAL.indexOf(answer);
        if (x != -1) {  // 選択肢ごとに回答数カウント
          aryAnswerCount[x]++;
        }
      }
    }
  }
  
  if (flgAnswer == false) {  // 例外:回答がまだなければエラー吐いて終了
    throw new Error("not Answer");    
  } else {
    return aryAnswerCount;
  }
}

chatwork.gs

バックオフィスも含めて弊社公式に採用しているchatworkのとあるグループチャットに満枠になったら通知するメソッドです。
chatwork通知の方法はググったらすぐ出てくるので詳細は割愛します。

f:id:LIFULL-kimuras:20181105201523p:plain
chatwork通知例

/**
 * chatwork通知処理
 * 
 * @param {string} 回答した選択肢タイトル 
 */ 
function sendChatwork(rChoiceVal) {
  
  const CW_TOKEN = "hogehoge";  //チャットワークトークン
  const CW_ROOM = 1111111;   // 通知先のチャットルームID

  var client = ChatWorkClient.factory({token: CW_TOKEN });  // チャットワークAPI
  
  var strBody = "[info][title]Googleフォームから通知[/title]インフルエンザ予防接種で以下の日程が満席になりました。\n" +
  rChoiceVal + "\n" +
  "[編集用URLでも記載するといいと思います]" +
  "[/info]";

  client.sendMessage({   //チャットワークに送る
      room_id: CW_ROOM,
      body: strBody
  });
}

 

結論 色々できそうでできなかったり

もっとちゃんと考えればやれることとか違うやり方とかあるんでしょうけど、GASでのフォーム操作は結構なパワープレイな感じでオススメしません。
要件がシンプルな場合はいいかと思います。
とはいえ、特に今回のようなバックオフィスタスクの負荷軽減には役に立つと思うのでちょっとのjsスキルと利他の気持ちでパパっとやれるのはいいですね。
LIFULLでは当たり前のようにこういった周囲へのサポートがあります。

よかったら参考にしてください。