はじめに
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