新しいGoogle Formを業務に活用する – その②
G Suiteの主要なアプリケーションはMicrosoft365とは違い、Google Apps Scriptで開発をし、機能を強化する事が可能です。このGoogle Formも同じです。オリジナルにはない機能を付け加える事で、更にG Suiteが活用出来るばかりではなく、全自動で人間が行っていた作業をやらせる事が出来るので、利用者が多ければ多いほど、削減出来る労力や時間・コストは大きいです。
今回は、前回の活用するその①に引き続き、さらにGoogle Apps Scriptで強化し、色々な作業をやらせてみたいと思います。このテクニックはFormだけでなく、自作のフォーム類や他のスクリプトでも応用の出来るものです。
目次
今回使用するファイル
本フォームは自動応答機能付きGoogle Formとしてサイトでリリースされているものと同じものです。フォーム自体の使い方や、細かな設定関係や事前準備に関してはそちらのページを参照してください。
送信時スクリプトを使う為には
トリガーの設置
今回紹介しているスクリプト類を使う為には、必ずトリガーの設置が必要になっています。トリガー自体に関してはまた別のエントリーで詳しく紹介する予定ですが、ここでは必要最低限これだけはというトリガーの設置に関する解説をしてみます。
今回自動作業させる関数はsendForms()という関数で作成しています。以下の手順でスクリプト作成後にトリガーを設置してください。
ここで注意しなければならないのは、このトリガー設置はこのフォームの管理者だけが行って下さい。他の方もやってしまうと二重にトリガーが設置されてしまいます。フォームは他の人からは他の人のトリガーが見えない仕様なので、トリガー削除もその人でなければ出来ません。気を付けましょう。
- フォームの右上の「︙」をクリックし、「スクリプトエディタ」を開きます。
- メニューより、「編集」⇒「現在のプロジェクトのトリガー」を開きます。
- トリガーが空なので、リンクをクリックし、実行に「sendforms」、イベントに「フォームから送信時」にてセットします。
図:スクリプトトリガーがなければ動作しません
二重送信されると言われるケースについて
ここ数年QAサイト等で「二重にメールが送信されたり、内容が入れ違って送信される・・・」という方がを見かけます。その方々のコードが「スプレッドシート側で記述し、データが入ってきたら処理をする」といったコードを書かれていますが、推奨しません。
理由は
- Formを同時に利用した場合に、複数の処理が止まることなくスプレッドシートに入ってくる。
- 排他制御をしていない為、例えばスクリプトプロパティなどの値が上書きされて、入れ違いが生じる。
- 次々に入ってくるので、参照すべきレコードが想定したレコードではなく、一番最後に入ってきたレコードになっている。
- そもそも、スプレッドシート側のForm送信時は自動応答メールを送るのに適していない。
- Formに記述する手法と違い非常にコードが煩雑になる(FormAppはレコードの位置など気にする必要がない)
FormAppは送ったデータを確実に取得し、メールを送ってからスプレッドシートに書き込みをしています。それぞれのプロセスは独立しています。しかし、スプレッドシート側にしてしまうと並列していくつものの処理が同時に入ってくる(プロセスは独立していない)ので、想定外の動きになります。本コードを装備するならば、FormAppで実装しましょう。
FormApp.getActiveForm を呼び出す権限がありません
最近また仕様が変わったせいなのか?次項の自動応答の機能をつけたフォームを作成したものの、フォームを送信しても自動応答が作動せずに「FormApp.getActiveForm を呼び出す権限がありません」というエラーとともに、Google Apps Scriptがエラーを吐いて停止するというケースに遭遇しました。
コードを見ればわかるのですが、FormAppに関しての記述は元々ありません。以前はこれでも問題なく動作していたのですが、原因を究明してみると、どうやらFormAppは使っていないものの、FormAppの権限が無いと動かないという。実は似たようなケースに過去Google Pickerのアップロード実装でも遭遇しています。
対応策としては、コメントアウトの形でダミーで「//FormApp.getActiveForm()」というコードをいれて、実行し権限承認し直せばOK。Pickerの場合も//DriveApp.addFile("test")といった書き込み権限のダミーコードを含めることで、トラブルを回避しています。
搭載している機能
自動応答メール機能
現在のGoogle Formには、アンケート送信時に「回答のコピーを送信」する機能が備わっています。以前はなかった機能ですが、これで自動応答メール的な役割は簡易的には可能です。しかし、あくまで簡易機能であり、その中のメッセージであったり、デザイン等をカスタマイズ出来るわけでもないので、便利なものではありません。
そこで装備したいのが「しっかりした自動応答メール送信機能」です。尚、回答のコピーを送信で行わせる場合には以下の手順でスクリプト無しで装備が可能です。
- 右上の歯車をクリックする
- 全般にて「メールアドレスを収集する」をチェックする
- 「回答のコピーを送信する」をチェックする
図:ただこの機能はちょっと不便なんですよね・・・
自動応答メールは、HTMLメールの形式で作成をするわけですが、その為にはHTMLタグで改行を加えつつメール本文を構築したり、またテーブルタグで綺麗に整形したり(またこの場合、レスポンシブメールにすると非常に綺麗になります)して、申請者や管理者宛に自動でメールを送信します。メール本文に署名や挨拶文などを加える事が可能になるので、業務やお問い合わせフォームなどでは必須の機能になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
function sendForms(e){ //変数を宣言する var user = ""; //メールアドレス格納用 var body =""; //本文用変数 //フォームデータを取得する var formdata = e.response.getItemResponses(); //ループでメアド取得&入力値を本文に加える for (var j = 0; j < formdata.length; j++){ //入力項目名を取得する var title = formdata[j].getItem().getTitle(); //メールアドレスを取得する if(title == "メールアドレス"){ user = formdata[j].getResponse(); } //入力項目を入れる(タイトルと入力値) body += "【"+formdata[j].getItem().getTitle()+"】<br>"; body += formdata[j].getResponse()+"<br><br>"; } //本文以外の項目を作成する var footer = '<br>' + "本件に関わるお問い合わせは、○○○部(電話番号:03-1259-96414)までご連絡ください。<br>"; var top = "「○○○」を受け付けました。大変アリガトウございます。<br>" + "申請内容を確認・承認後、改めてご連絡致します。<br><br>" + "【連絡事項】<br>" + "・本メールにて返信する場合は、‟全員に返信”にて送信ください。<br><br><br>"; //メール内容をつなぎ合わせる body = top + body + footer; //メールを送信する MailApp.sendEmail({ to: user, subject: "お問い合わせ確認", htmlBody: body, name:"自動応答メール", }); } |
メールのSubjectやName,メッセージ内容はスクリプト内に直接書くのも良いのですが、メンテナンスフリーにする為に、このあたりもUIを自分で実装して誰でも変更出来るようにすると尚良いでしょうね(担当者が変わった程度でソースを直さないと行けないのは不便ですから)。
書類自動生成機能
例えば申請フォームなどの場合、このままではスプレッドシートに1行申請データが追加されるだけで、ペーパーレスならばこれでも良いのですが、会社内で印刷物で判子が必要といった場合には、データから別に申請書を作成しなければなりません。この作業を人間が行うのは非生産的以外の何者でもないので、予め用意して於いたテンプレートをコピーして、そこにデータを書き込ませる事で書類も自動生成させてしまうのが良いでしょう。
この後の項目のPDF化をするにも必須です。Googleドキュメントでも良いのですが、Googleスプレッドシートのほうがきめ細かなレイアウトを作成し易いので、ここではスプレッドシートにて諸類を自動生成させてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//親フォルダのIDを返すラッピング関数 function driveman(id){ var parentsfolder = DriveApp.getFileById(id).getParents(); while (parentsfolder.hasNext()) { var folder = parentsfolder.next(); var idval = folder.getId(); return idval; } } //フォーム入力内容を拾って、ドキュメントを生成し、指定フォルダに格納するルーチン function docsgenerator(array) { //テンプレートファイルのURLを取得 var docurl = "ここにテンプレートのURLを/editまで含めて入れる"; //書類格納先フォルダを指定する var targetfolder = "ここに格納先フォルダのIDを入れる"; //各種変数の宣言と格納 var templatedocs = SpreadsheetApp.openByUrl(docurl).getId(); var movetarget = ""; var movefiles = ""; //テンプレートファイルをコピーしてIDを取得する var files = DriveApp.getFileById(templatedocs).makeCopy("申請書_temp"); var filesId = files.getId(); var parentsfolder = driveman(filesId); var tempparents = DriveApp.getFolderById(parentsfolder); //作成したファイルにフォームデータをdataシートへ書き込む var sheet = SpreadsheetApp.openById(filesId); sheet.getSheetByName("data").appendRow(array); //移動先を変数に格納 movetarget = DriveApp.getFolderById(targetfolder); movefiles = DriveApp.getFileById(filesId); tempparents.removeFile(movefiles); //元のフォルダからは削除 movetarget.addFile(movefiles); //格納先にファイルを追加 //作成したドキュメントのIDを返してあげる return filesId; } |
予め用意しておいたGoogle Driveのフォルダにファイルを生成します。データはコピーした申請書テンプレートの1つずつのセルに書き込むのではなく、arrayで受け取ったデータを別のシートにappendRowで一気に書き込み、申請書シートからは数式でそれを参照する形にしています。この方が高速に書き込みが可能です。
ファイルの移動はちょっとややこしいですが、一度生成したファイルの親フォルダからはremoveFileし、格納先フォルダにaddFileする事が必要です。生成したファイルの親フォルダを知る為に、driveman()という関数を別に用意してあります。最期にfilesIdを本ルーチンに返して上げています。
自動PDF作成機能
自動生成された書類はスプレッドシートなどのデータです。しかし改ざんの可能性もあったり、即時に印刷をしたい場合には少々不便です。この書類をPDF化してしまいましょう。PDF化させる為のルーチンの詳しい解説はまた別のエントリーで紹介します。ここではPDF化する為にはこういうルーチンを組むという事だけ紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//PDFを格納するフォルダのIDを取得しておく var folid = "ここにGoogle DriveのフォルダのIDを入れる"; //Access Tokenを取得する var token = ScriptApp.getOAuthToken(); //生成したスプレッドシートのIDを取得しておく var fileid = "ここに生成したスプレッドシートのIDを入れる"; //生成前に対象ファイルのデータ存在確認チェックと無限ループ var chksheet = SpreadsheetApp.openById(fileId); var rangechk = chksheet.getSheetByName("申請書").getRange("チェックするセル番地").getValue(); while (rangechk == ""){ rangechk = chksheet.getSheetByName("申請書").getRange("チェックするセル番地").getValue(); } //申請書シートのシートIDを取得する var gid = SpreadsheetApp.openById(fileId).getSheetByName("申請書").getSheetId(); //PDF生成URLを組み立てる var url = "https://docs.google.com/spreadsheets/d/" + fileId + "/export?format=pdf&gid=" + gid + "&portrait=true&size=A4&gridlines=false&fitw=true"; //PDFデータを受け取る var pdf = UrlFetchApp.fetch(url, {headers: {'Authorization': 'Bearer ' + token} }).getBlob().setName("申請書類.pdf"); //Google Driveの指定フォルダにファイルを生成する DriveApp.getFolderById(folid).createFile(pdf); |
PDF生成は、特別なURLに対して、UrlFetchAppクラスを使ってアクセスさせ、Blob形式でPDFとして取得します。その際にsetNameでファイルに名前を付けています。また、この受け取ったPDFデータをDriveAppクラスを使ってドライブ内にPDFファイルも生成させています。
途中のrangechkですが、時々データ書き込み完了前にPDF生成にルーチンが移動してしまって、空のままPDFが作成されてしまうことがあるので、ここで書き込みチェックをする無限ループを設けてあります。チェックするセル番地は一番最期に書き込まれる所を指定すると良いでしょう。
メールに添付する時には、以下のように受け取ったpdfをattachmentに加えれば良いです。複数のファイルを添付する時には配列で渡してあげればOKです。
1 2 3 4 5 6 |
MailApp.sendEmail({ to: user, subject: subject, htmlBody: body, attachments:pdf, //PDFを添付する。 }); |
自前でスプレッドシートに書き込む
Google Formは自動でスプレッドシートへ書き込む機能が搭載されているのですが、書き込まれる情報はフォーム上の情報に限られ、また書き込まれる列の並び順なども固定です。これではスプレッドシートの二次利用をする上では少々困るケースがあります(ワークフロー的なシステムや、お問い合わせ番号で作業をするようなケース)
そこで、書き込み自体Google Formにやらせるのではなく、自前でスプレッドシートに書き込む機能を装備する事で、さらに高度なGoogle Formの活用をする事が可能です。但し、送信日付に関してはフォーム送信内容からは取得が出来ないので、new Date()で自分で生成してあげる必要があります。
スプレッドシート書き込み
自前で書き込みするといっても、さして難しいものではありません。取得したフォームのデータにユニークIDや自動生成した書類のURL、また処理ステータスの初期値などを自分の場合書き込ませてますが、基本は配列を用意して流し込むだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function sendForms(e){ //書き込み用配列を用意する var array = []; //書き込み先スプレッドシートを取得する var sheet = SpreadsheetApp.openById("値を書き込先スプレッドシートID"); var ss = sheet.getSheetByName("値を書き込み先シート名"); //フォームデータを取得する var formdata = e.response.getItemResponses(); //ループでデータをpushする for (var j = 0; j < formdata.length; j++){ //入力項目を取得しpushする array.push(formdata[j].getResponse()); } //uidを一番はじめに追加する array.unshift(uid); //日付を配列の2番目に追加する array.splice(1, 0, new Date()); //初期ステータスを追加する array.push("未処理"); //シートへデータを書き込みする ss.appendRow(array); } |
今回はこの次の項目で紹介するユニークIDを配列の一番最初に、申請日を配列の2番目に、最期に初期ステータスとして「未処理」を追加するようにしています。こうする事で、自前で自由自在にデータを書き込みさせる事が可能になります。
ユニークIDを割り振ってあげる
ユニークなIDとは、絶対にかぶらない連番の事で、例えばお問い合わせ番号を発行してこの番号を元に、双方で仕事をしたり、またワークフロー的な仕組みを構築する時に、そのレコードを特定するにはユニークなIDが必要になります。このユニークIDは通常は1列目に書き込みを行わせます(vlookup関数でその後参照させたりする事もありますから)。
このユニークIDを発行する仕組みには、プロジェクトプロパティを使用しますが、同時に送信されるケースも考慮してLock Serviceを使い排他処理も装備する必要があります。排他処理自体はまた別の項目で紹介しています。ここではユニークID発行の仕組みだけを紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//ロックサービスの宣言 var lock = LockService.getPublicLock(); try{ //30秒間のロックを開始 lock.waitLock(30000); //uidの現在地を取得する var Properties = PropertiesService.getScriptProperties(); var uid = Properties.getProperty("uid"); //新しいuidを作る uid = Number(uid) + 1; //uidを書き戻す Properties.setProperty("uid",uid); //LockServiceを開放する lock.releaseLock(); }catch(e){ //ここにエラー発生時の処理を記述する } |
原理は簡単で
- LockServiceで排他処理を開始
- プロジェクトプロパティのuidの値を取得し、1を加算する
- プロジェクトプロパティにuidの値を書き戻す
- LockSercviceを開放する
後はスプレッドシートに書き込む為の配列の一番最初にこのuidの値をpushもしくはunshiftしてあげれば良いです。
1 2 3 4 5 6 7 8 9 10 |
//普通に最初に追加する時 var array = []; array.push(uid); //後から1番目に追加する時 var array = []; ・・・ 途中で色々pushしてる ・・・ array.unshift(uid); |
自動でCC追加機能
業務で使用する場合、自動で上司や関連部署の人にCCで通知も送りたい事があります。その場合別のシートにその一覧を用意しておき、それを参照し、MailAppのccにはめ込めば良いでしょう。別のシートには、申請者のメアド、CC送信先の2列で用意すればOKです。シート名は「CC送信先」と付けています。データは2行目から入っています。
この時、ユーザのメアドは自動で取得させるほうが楽ですが、今回はFormのメールアドレス欄で入力されたものを使ってCCの送信先を探索させる方法をあえて取ってみます。CC送信先はカンマ区切りでセルに入力しておけば、複数送れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//変数を用意する var user = ""; //ユーザのメアドを格納する var cc = ""; //CC送信先を格納する //スプレッドシートのCC送信先シートを取得する var sheet = SpreadsheetApp.openById("ここにシートのIDを入れる"); var ss = sheet.getSheetByName("CC送信先").getRange("A2:B").getValues(); //フォーム送信データを取得する var formdata = e.response.getItemResponses(); //ループでメールアドレスを取得する for (var j = 0; j < formdata.length; j++){ //入力項目名を取得する var title = formdata[j].getItem().getTitle(); //メールアドレスを取得する if(title == "メールアドレス"){ user = formdata[j].getResponse(); } } //CC送信先情報を探索する for(var i = 0;i<ss.length;i++){ //CC送信先があれば、ccにそのアドレスを格納しループを抜ける if(user == ss[i][0]){ cc = ss[i][1]; break; } } |
この処理では、メールアドレス入力欄が何番目であっても、メールアドレスという名称の入力欄の値を取得してくれます。また、そのメアドを元にCC送信先シートから一致するメアド、それに対応するCC送信先の値を変数ccに格納しています。cc送信先がみつかったら、break;で抜けて、後は、MailAppにて、ccにこの値を加えれば良いです。
1 2 3 4 5 6 |
MailApp.sendEmail({ to: user, cc:cc, //ここにccをはめ込む subject: title, htmlBody: body, }); |
フォームの回答を削除する
自前で書き込みをさせる時に、フォームに回答データを残しておきたくない場合があります。その時には、ルーチンの最期でフォームレスポンスデータを全て削除するコードを入れて入れておけば良いでしょう。
※ただし、同時に利用者がいる場合、その人のデータまで削除される恐れがあるので、夜中などの利用者ゼロの時間にトリガーで実行させるほうが望ましいです。
1 2 |
//フォームに蓄積されてるデータをすべて削除する FormApp.openById("ここにフォームのIDを入れる").deleteAllResponses(); |