ぼくがかんがえたさいきょうの"定員付きフォーム"

今回は、GASで定員付きのGoogleフォームを作りました。

# 背景
定員付きのGoogleフォームを作成する記事は他にもありますが、どれも回答数が一定のラインに達したらフォームを閉じるだけのもので、「残りの枠数の表示」などの実務で使う上では必要な機能は搭載されていません。

また、そのようなサイトで紹介されているコードは、どんなに小さな変更(定員を変える等)でもコードをいじらなければならないことがほとんどです。これでは保守管理ができる人が限られてしまいます。この点も実務で使う上では不便だと感じました。

# そこで、つくった

実務で差し支えなく使用できるよう、以下のような機能を搭載しました。

- フォームの説明欄に残り人数を表示できる
- 枠が残り少なくなったときにだけ、フォームの説明欄に残り人数を表示するようにできる
- ITに詳しくなくても、GUIで保守・管理ができる

この記事は、開発する上で心がけたことの話が中心となります。導入方法や使い方などの実際に使う上での情報が欲しいという方は、下記のブログをご覧ください。


https://r-40021.github.io/blog/google/2021/09/16/form-limit.html

# 使用言語

Google Apps Script

普段からJavaScriptに慣れているので、すんなりと馴染めました。ES6の記法にも対応していて便利です。

# ソースコード
汎用性や保守のしやすさを重視した結果、140行近くに上りました(笑)

ここに全体を示します。

```js
`use strict`

const propaty_display_limit = PropertiesService.getScriptProperties().getProperty("DISPLAY_LIMIT");
const propaty_max = Math.floor(PropertiesService.getScriptProperties().getProperty("MAX"));
const propaty_old = PropertiesService.getScriptProperties().getProperty("old");
let LIMIT_COUNT = propaty_max || propaty_max === 0 ? propaty_max : 5;//定員
let old_dis = propaty_old ? propaty_old : "";//古い定員の残り人数通知
let display_limit = Math.floor(propaty_display_limit || propaty_display_limit === 0 ? formatLimit(propaty_display_limit) : -1);//残りの枠を表示するしきい値
const kaigyo = "\n\n---------------------------------\n\n";//残り枠数のあとに追加される文字列


function endFormCheck(changed) {
  const form = FormApp.getActiveForm();//アクティブなフォーム
  const remaining = LIMIT_COUNT - form.getResponses().length;//残り人数
  const displayRemaining = remaining < 0 ? 0 : remaining;//残り人数が負の場合は0にする
  if ((display_limit === -1 || remaining <= display_limit) && Number(display_limit !== 0)) {
    const description = `定員${LIMIT_COUNT}名のところ、これまでに${form.getResponses().length}名が申し込みました。\n残りは${displayRemaining}枠です。` + (remaining < 0 ? `(超過${form.getResponses().length - LIMIT_COUNT})` : "");//定員があと何人か
    if (old_dis && form.getDescription().indexOf(old_dis) !== -1) {
      form.setDescription(form.getDescription().replace(old_dis, description));//概要文を書き換え
    } else {
      form.setDescription(description + kaigyo + form.getDescription());//新規
    }
    PropertiesService.getScriptProperties().setProperty("old", description);
  } else if (old_dis && form.getDescription().indexOf(old_dis) !== -1) {
    form.setDescription(form.getDescription().replace(old_dis + kaigyo, ""));//概要文を書き換え
  }
  if (form.getResponses().length >= LIMIT_COUNT) {
    if (changed === "changed" && form.isAcceptingResponses()) {
      FormApp.getUi().alert(`回答を締め切りました。\n現在の定員は${LIMIT_COUNT}名です`);//定員の変更によってフォームが閉鎖された場合に、ポップアップで通知
    }
    form.setAcceptingResponses(false);
  } else if (changed === "changed") {
    if (!form.isAcceptingResponses()) {
      /* 定員に余裕がある場合には、回答の収集を再開することを提案 */
      const ui = FormApp.getUi();
      const alert = ui.alert("回答の収集を再開", `回答の収集を再開しますか？\n現在の定員は${LIMIT_COUNT}人、残りは${remaining}枠です。`, ui.ButtonSet.YES_NO);
      if (alert === ui.Button.YES) {
        form.setAcceptingResponses(true);
      }
    } else if ((display_limit !== -1 && remaining > display_limit) || display_limit === 0) {
    /*フォームの説明欄に残りの人数が記述されない場合は、ポップアップでお知らせ*/
      FormApp.getUi().alert(`定員${LIMIT_COUNT}名\n残り${LIMIT_COUNT - form.getResponses().length}枠`);
    }

  }
}

function onOpen() {
  const ui = FormApp.getUi(); // Uiクラスを取得する
  const menu = ui.createMenu('定員制御');  // Uiクラスからメニューを作成する
  menu.addItem('定員を変更', 'setMax');   // メニューにアイテムを追加する
  menu.addItem('残りの枠数を表示するしきい値を変更', 'setDisplayLimit');
  menu.addItem('最新の状態に更新', 'refresh');   // メニューにアイテムを追加する
  menu.addToUi();// メニューをUiクラスに追加する
}

function refresh() {
  endFormCheck("changed");//更新用
}

function setMax() {
  /* 定員を変更 */
  const form = FormApp.getActiveForm();//アクティブなフォーム
  const ui = FormApp.getUi();
  const title = '定員の設定';
  const body = `半角数字で定員を入力してください。\n現在の定員は${LIMIT_COUNT}人で、残りは${LIMIT_COUNT - form.getResponses().length}枠です。`
  const prompt = ui.prompt(title, body, ui.ButtonSet.OK_CANCEL);//プロンプトを表示
  const text = prompt.getResponseText();
  if (prompt.getSelectedButton() === ui.Button.OK) {
    if (!isNaN(text) && Number(text) >= 0) {
    /*数字であることが確認された場合の処理*/
      if (!text) return;//空欄だった場合
      LIMIT_COUNT = Math.floor(Number(text));
      PropertiesService.getScriptProperties().setProperty("MAX", Math.floor(Number(text)));
      display_limit = Math.floor(propaty_display_limit || propaty_display_limit === 0 ? formatLimit(propaty_display_limit) : 0);
      refresh();
      return;
    } else {
      /* 数字じゃなかったら */
      const ui2 = FormApp.getUi();
      const title2 = 'むむ？';
      const body2 = Number(text) < 0 ? '0以上の数を半角で入力してください。' : '半角数字で入力してください。'
      const alert2 = ui2.alert(title2, body2, ui2.ButtonSet.OK);
      if (alert2 === ui2.Button.OK) setMax();
    }
  }
}

function setDisplayLimit() {
  /* しきい値を変更 */
  const form = FormApp.getActiveForm();//アクティブなフォーム
  const ui = FormApp.getUi();
  const title = '残りの枠数を表示するしきい値を設定';
  const body = `残りの枠数がいくつ以下になったら、回答者数・残りの枠数をフォームの概要文に表示するかを半角数字で入力してください。\n例）定員が100人で、残り5枠以下になったら残りの枠数をフォームの概要文に表示したい場合→「5」と入力する\n\nまた、残りの枠数が定員の○%以下になったら、というように設定することもできます。\n例）残りの枠数が定員の20%以下になったら残りの枠数をフォームの概要文に表示したい場合→「20%」と入力する\n\n概要文に常に残りの枠数を表示したい場合は、「-1」と入力してください。\n概要文に残りの枠数を表示させない場合は、「0」と入力してください。\n\n現在の設定は「${/%/.test(propaty_display_limit) ? propaty_display_limit : Math.floor(Number(propaty_display_limit))}」です。\n現在の定員は${LIMIT_COUNT}人、残りは${LIMIT_COUNT - form.getResponses().length}枠です。\n\n`;
  const prompt = ui.prompt(title, body, ui.ButtonSet.OK_CANCEL);//プロンプトを表示
  const text = prompt.getResponseText();
  if (prompt.getSelectedButton() === ui.Button.OK) {
    if (/%/.test(text)) {
      const onlyNum = text.replace("%", "");
      if (!isNaN(onlyNum) && Number(onlyNum) >= 0 && Number(onlyNum <= 100)) {
      /*パーセントで入力された場合*/
        if (!onlyNum) return;
        display_limit = formatLimit(text);//しきい値を変更
        PropertiesService.getScriptProperties().setProperty("DISPLAY_LIMIT", text);//しきい値を保存
        refresh();
        return
      }
    } else if (!isNaN(text) && Number(text) >= -1) {
      if (!text) return;//空欄だった場合
      display_limit = formatLimit(Number(text));
      PropertiesService.getScriptProperties().setProperty("DISPLAY_LIMIT", Math.floor(Number(text)));
      endFormCheck();
      return;
    } else {
      /* 数字じゃなかったら */
      const ui2 = FormApp.getUi();
      const title2 = 'むむ？';
      const body2 = "数字または割合(%)を半角で正確に入力してください。"
      const alert2 = ui2.alert(title2, body2, ui2.ButtonSet.OK);
      if (alert2 === ui2.Button.OK) setDisplayLimit();
    }
  }
}

function formatLimit(int) {
  if (/%/.test(int)) {
  /*％を実数に*/
    return Math.floor(LIMIT_COUNT * (Number(int.replace("%", "")) / 100));
  } else {
  /*%でなければ、小数点以下を切り捨てて戻す*/
    return Math.floor(int);
  }
}
```

# 工夫したところ
## アラートやプロンプトなど、GUI要素をふんだんに取り入れた
誰でも保守管理ができるよう、設定はすべてGUIで完結するようにしました。

最初にこのコードをコピペして必要なトリガーをセットすれば、あとはすべてGUIで操作できます。

![ポップアップメニュー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1172197/eeb1d64c-7c77-099d-4c97-edb6ba318a87.png)

![GUIで操作可能](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1172197/39da0c41-04b7-0ae0-ab7a-1095611cb25b.png)

## とにかく高機能

残りの人数を概要文に表示する機能を搭載しました。これ、ないと不便ですよね。

また、あまりにも申込者数が少ないと申し込みをためらってしまう人が多いのではと考え、申込者数が増えてきてから残りの人数を表示できる機能も追加しました。

## 汎用性を高くした
既存のフォームにもすぐに取り入れられるような設計になっています。もちろん新規のフォームでも問題ありません。前述したとおり、残りの人数を概要文に表示する機能を搭載したのですが、それがすでに概要欄に記述してある文章と統合されるようにしました。コードの量は増えますが、処理の中身は簡単です。

概要欄に表示されている「残り○人」のカウントダウンの文言(これをAとします)をプロパティに保存し、次にフォームが送信された際に、概要文中のAを、新しい残りの人数を含むカウントダウンの文言に置き換える、そしてその新しい文言もプロパティに保存する、.....(以下、無限ループ)という処理を行っています。

# まとめ
プログラミングを使って、Googleのサービスに様々な付加価値を加えていくのはとても楽しいですし、達成感も大きいです。

今後もちょこちょこGASを使っていこうと思います。

最後まで読んでいただきありがとうございました！

# 関連サイト
今回ご紹介したGASの導入方法・使い方について

https://r-40021.github.io/blog/google/2021/09/16/form-limit.html

ソースコード

https://github.com/r-40021/form_limit