はじめに
Googleフォームで候補者の中から1名を選ぶ匿名投票に近いものを作れないかと調べてみました。
環境
Google Workspaceを使いました。
- Google Workspace - Google App Script(GAS) - Google Forms - アンケートフォーム - Google Sheets - メールアドレス - 回答データ - 候補者一覧
欲しい機能
・誰が投票したか分かる。
・重複投稿は認めない。
・回答データと投票者が紐づけできないこと。
・不公平をなくすために、候補者の順番を毎回シャッフルする。
処理の流れ
スプレッドシートに書き込む場合は、GASのスクリプトロックを使って排他制御します。
フォームの標準機能として、プルダウンやラジオボタンには「選択肢の順序をシャッフルする」があります。
候補者が少ない場合はフォームに直接入力して、この機能を使えば良いので、スプレッドシートで準備する必要はないと考えます。
その場合は該当場所をコメントしてください。
大まかな処理は以下のようになります。
- フォームから回答データが送られてくる。
- 候補者をシャッフルしてフォームの項目に設定する。(次回のフォーム表示時に反映される。)
- メールアドレスの重複があれば中止し、フォームに保存された情報を削除する。
- メールアドレスをスプレッドシートに記録する。
- スプレッドシートに記録したメールアドレスをシャッフルする。
- アンケート内容をスプレッドシートに記録する。
- フォームに保存された情報を削除する。
メールアドレス重複の有無で送信後のメッセージを変更する
これが可能なら投稿の重複をユーザーに通知できたのですが、GASの仕様から無理でした。
メールアドレスの重複の有無に関わらずメッセージが同じになるため、ユーザー側からは処理されたように見えます。
実際には、メールアドレスが重複した場合は処理を中断します。
以下にその記載があります。
また、フォームの表示時にメールアドレスの重複をチェックできないかと考えたのですが、設定できるトリガーが意図したものではありませんでした。
上記には、実行順序は以下とあります。
- フォームの値をスプレッドシートに送信する。
- 送信後のフォームUIで確認メッセージを表示する。
- トリガー「フォーム送信時」に設定したGASを実行する。
まとめると以下のようになります。
- フォーム送信時にsetConfirmationMessageを実行しても即時反映されない。(次のフォーム表示時に反映)
- フォーム送信時に実行するGASで、送信後のフォームUIを取得できないため、メッセージダイアログを表示することはできない。
- フォームで設定する「起動時」のトリガーは、フォーム「作成者」がフォーム「編集画面」を開いたときに発火するトリガーのこと。
フォームの回答をGASで書き込む方法
以下を使わせていただきました。
呼び出し元の関数でロックを行うので、ロック部はコメントしています。
フォームの設定
フォームを作成して、候補者を表示する項目(プルダウン、またはラジオボタン)を作成します。
設定タブ
メールアドレスの重複はGASで判別します。
送信後に「別の回答をする」でなく「回答を記録しました」を表示させるため、「回答を1回に制限する」を有効にします。
「メールアドレスを収集する」をチェックしない場合、フォームにメールアドレスの項目が表示されます。
この場合、作成したGASではログインしたメールアドレスと、入力されたメールアドレスを比較して異なっている場合は、処理を中止します。
回答タブ
自前で作成したスプレッドシートを使うのでリンクはしません。
スプレッドシートの作成
メールアドレスと回答データ、候補者一覧を保存するための3個のスプレッドシートを作成します。
メールアドレスと回答データは、空のスプレッドシートです。
候補者一覧のスプレッドシートは、1列目にフォームに表示するための候補者を入力します。
GASの設定
フォームのスクリプトエディタを開いて、下記のGASを貼り付けます。
GASにある「spreadsheetEmail」、「spreadsheetAnswer」、「spreadsheetCA 」の変数に各スプレッドシートのIDを入力します。
スプレッドシートのIDは、スプレッドシートを開いてURLの「・・・/d/xxxxx/edit#gid=・・・」の部分を探し、dとeditに挟まれた文字列(xxxxxの部分)です。
「トリガー」を設定してアクセス権限を付与します。
フォームに作成した候補者の項目にスプレッドシートに入力したデータを反映させるために、スクリプトエディタの「実行」をクリックしてGASを実行します。
フォームの情報が取得できずにエラーになりますが、問題ありません。
「トリガー」の設定時に承認できていないものがあれば、認証画面が表示されるのでアクセス権限を付与します。
テスト
Googleフォーム、3個のスプレッドシートを開いて、実際にフォームから送信して正常に動作することを確認します。
GASソース
作成したGASのソースです。
function Submit(e) { // メールアドレスを記録するスプレッドシートのID //「https://docs.google.com/spreadsheets/d/xxx/edit」のxxxを入力する。 var spreadsheetEmail = 'xxxxxxxxxxe1ctqR3cxuFR5XTcnpmV_TbPxxxxxxxxxx'; // 回答データを記録するスプレッドシートのID //「https://docs.google.com/spreadsheets/d/xxx/edit」のxxxを入力する。 var spreadsheetAnswer = 'xxxxxxxxxx2nQT3RIX7N2hLL1zYTqrlwppxxxxxxxxxx'; // 候補者を記入したスプレッドシートのID //「https://docs.google.com/spreadsheets/d/xxx/edit」のxxxを入力する。 var spreadsheetCA = 'xxxxxxxxxxJDSIP65sn_GC0Pc2UrYjXGyKxxxxxxxxxx'; // 候補者の項目は1番目とする。 var questionNo = 1; // 候補者の選択項目の種類(true:ラジオボタン false:プルダウン) // 予めフォームで項目を作成しておく。 flgRadio = false; // // 以下、触らない。 // var ssEmail = SpreadsheetApp.openById(spreadsheetEmail); var sheetEmail = ssEmail.getActiveSheet(); var ssData = SpreadsheetApp.openById(spreadsheetAnswer); var sheetData = ssData.getActiveSheet(); var ssCA = SpreadsheetApp.openById(spreadsheetCA); var sheetCA = ssCA.getActiveSheet(); var range; // フォームの取得 const form = FormApp.getActiveForm(); ///////////////////////////////////// // フォームに表示する候補者をシャッフルする。 // 次回の表示時に反映される。 ///////////////////////////////////// // 候補者リストをシャッフルして取得する。 var lastRowCA = sheetCA.getLastRow(); range = sheetCA.getRange(1, 1, lastRowCA); range.randomize(); var listCA = range = sheetCA.getRange(1, 1, lastRowCA).getValues(); // 全ての質問項目を取得する。 var items = form.getItems(); // questionNoで指定した質問項目を指定する。 var item = items[questionNo - 1]; // フォームに反映する。 if ( flgRadio ) { // ラジオボタン item.asMultipleChoiceItem().setChoiceValues(listCA).showOtherOption(false); } else { // プルダウン item.asListItem().setChoiceValues(listCA); } // Googleフォームの「メールアドレスを収集する」を使用してメールアドレスを取得する。 var email = e.response.getRespondentEmail(); // フォームの設定で「信頼できる組織のユーザーに限定する」がOFFの場合に、 // 入力されたメールアドレスとログインしているメールアドレスが不一致ならば中止する。 if ( form.requiresLogin() == false ) { var emailAvtive = Session.getActiveUser().getEmail(); if (email != emailAvtive) { console.log('×:メールアドレスの不一致:' + emailAvtive + ' ' + email); return; } } // // 排他制御の設定(スクリプトロック) // const lock = LockService.getScriptLock(); // ロックの取得を試みる時間:ミリ秒 // https://developers.google.com/apps-script/reference/lock?hl=ja const tryTime = 10 * 1000; // 入力されたメールアドレスがスプレッドシートに記録されているか検索する。 var textObject = sheetEmail.createTextFinder(email); var results = textObject.findAll(); // 入力されたメールアドレスが既に記録されていたら処理を中止する。 if ( results.length ) { // Googleフォームからデータを削除する。 form.deleteAllResponses(); // 送信後にGoogleフォームに表示するメッセージを上書きする。 //form.setConfirmationMessage('既に回答済みです。'); //var ui = FormApp.getUi(); //var response = ui.alert('既に回答済みです。'); console.log('メールアドレス一致数:' + results.length); Logger.log('メールアドレスが一致したので停止。'); return; } // ロックの取得を試みる。 if ( ! lock.tryLock(tryTime) ) { console.log(tryTime + '秒待機してもロックできなかったので処理中止。'); return; } //////////////////// // メールアドレスの処理 //////////////////// // 1列目の最終行に記録する。 var lastRowEmail = sheetEmail.getLastRow(); sheetEmail.getRange(lastRowEmail + 1, 1).setValue(email); //SpreadsheetApp.flush(); // 1列目をシャッフルする。 range = sheetEmail.getRange(1, 1, lastRowEmail + 1); range.randomize(); //SpreadsheetApp.flush(); //////////////// // 回答データの処理 //////////////// // データをスプレッドシートに書き込む const Writer = new AnswerInfoWriter(); const Answer = e.response.getItemResponses(); Writer.Write(sheetData, Answer); // スプレッドシートの修正を即時反映する。 SpreadsheetApp.flush(); // ロック開放 lock.releaseLock(); // Googleフォームからデータを削除する。 form.deleteAllResponses(); // 送信後にGoogleフォームに表示するメッセージを上書きする。 //form.setConfirmationMessage('ご回答いただき、ありがとうございました。'); } // // 回答をスプレッドシートに書き込む関数 // https://risaki-masa.com/write-form-answer-to-spreadsheet/ // // 既に呼び出し元の関数でlockしているので、LOCKの箇所をコメントする。 // AnswerInfoWriter = function() { function _Write( sheet, item_and_answer_pairs ) { const LOCK = LockService.getScriptLock(); /** if ( ! LOCK.tryLock( 60 * 1000 ) ) { console.log(tryTime + '秒待機してもロックできなかったので処理中止。'); return; } */ // 毎回ヘッダを書き込むのが無駄だが仕方なし。 _WriteHeader( sheet, item_and_answer_pairs ); _WriteData ( sheet, item_and_answer_pairs ); //SpreadsheetApp.flush(); //LOCK.releaseLock(); } function _WriteHeader( sheet, item_and_answer_pairs ) { const TITLES = item_and_answer_pairs.map( pair => pair.getItem().getTitle() ); TITLES.forEach( ( title, index ) => { const CELL = sheet.getRange( 1, index + 1 ); CELL.setValue( title ); } ); } function _WriteData( sheet, item_and_answer_pairs ) { const ROW_NUMBER = sheet.getLastRow() + 1; const ANSWERS = item_and_answer_pairs.map( pair => pair.getResponse() ); ANSWERS.forEach( ( answer, index ) => { const CELL = sheet.getRange( ROW_NUMBER, index + 1 ); CELL.setValue( answer ); } ); } return { Write: _Write, }; }
Comments