D12 - 如何用 Apps Script 寄出客制化的表单并搜集分散在 Google Sheet 中的回应?(二)大幅度客制你的 Google Form

今天的目标

很多时候我们会需要搜集些不同的资料。像是 Marketing 在做大规模但针对不同组织的调查问卷。如果只是三份、五份的问卷要做客制化、统整算还好;但如果是一百份、甚至上千份时,总不能一个个复制了吧。此时就会遇到个问题——

  1. 要如何复制客制化 Google 表单?
  2. 要如何集中很多表单中的资料(回应)?

因为篇幅关系,这边会拆成三篇来写,第一篇与第二篇回应 Q1;第三篇回应 Q2。今天这篇是针对 Q1 的第二篇,昨天我们讲了怎麽样复制与简单客制 Google 表单。而今天来到了第十二天,我们来到目前最进阶的操作,用 GAS 完成超·客制表单。一样先讲结论,如果你很急着用,可以直接使用这份 Add-On: Form Publisher,功能非常强大。自己写的好处是,如果你一天突然要做些高度客制化,那此篇会有帮助。这篇的定位比较像是字典、工具,在你需要用的时候可以来参照。那就让我们开始吧!


Q1. 我要如何复制客制化 Google 表单?

复制表单有两种方式。一种是比较简单的「复制范本」,简单来说就是针对一个表单复制,然後再改期中的元素。另一种是「从零制作」,这种就比较复杂,因为会是透过 GAS 完整制作表单,会需要比较熟悉 GAS。我们昨天讲完方法一,今天则着重在方法二。

方式二:用 GAS 制造表单

如果是比较复杂的表单与设定,举例来说,有些人需要第一题,有些人第五题要用填空,那要怎麽处理?这个时候可以用 GAS 来制作每份表单。虽然这样客制化程度很高,但相对制作时间会比前一次更久。果然是充满取舍的人生。

也给大家看一下这次预计生成的参数们,左边绿色是我们输入的参数,右边橘色是我们预计输出的参数。

补充:校稿时有朋友问说那个「勾勾」怎麽做,多录一支影片给大家看

Step 1 开启 Google Sheet,并串起 GAS

设定步骤跟之前一一样,从 Google Sheet 中进入 GAS。

Step 2 用 GAS 中生成我们表单

昨天我们有示范如何操作既存的表单,今天我们来看看怎麽生出新的表单。方式极其简单,就是用 FormApp.create() ,并在括号中输入表单名称即可。

function createNewForm(){
  let new_form = FormApp.create('New Form');
}

但,这样创造有个小问题,就是创造的位置会在根目录,也就是一开始打开 Google Drive 的位置。那要怎麽移动?目前是需要比较阳春的透过写 DriveAppmoveTo(folder) 才行。也就是以下示范的程序码,改编自 JLMosher 的回应

function moveFile(fileId, destinationFolderId) {
  let destinationFolder = DriveApp.getFolderById(destinationFolderId);
  DriveApp.getFileById(fileId).moveTo(destinationFolder);
}

所以原本的表单,使用上就变成了——

function createNewForm(){
  let new_form = FormApp.create('New Form');
  let new_form_id = new_form.getId()
  let destinationFolderId = "your_folder_id_here"
  moveFile(new_form_id, destinationFolderId)
}

那这边是简易生成一张表单的方式,接着我们要对每一张表单进行细节的操作,开始罗!

Step 3 用 FormApp 来生出表单的问题

我们要如何在 GAS 内生出问题们?我做了一个简单方法对照表。

那实际上怎麽用呢,这边先给大家看完整的程序码,接着一个个说明。我们以一份要约 onsite interview 的表单为例。

function addNameText(form){
    let text_name = form.addTextItem().setTitle('Name').setRequired(true);
    return text_name
}

function addLunchList(form){
    let list_item = form.addListItem();
    list_item.setTitle('Option for the Lunch')
             .setChoices([
                 list_item.createChoice('Meat'),
                 list_item.createChoice('Vegetarian')
              ]);
    return list_item
}

function addTrafficChoices(form){
     let multipleChoice_item = form.addMultipleChoiceItem();
      multipleChoice_item.setTitle('How do you come to our office?')
                         .setChoices([
                           multipleChoice_item.createChoice('Train'),
                           multipleChoice_item.createChoice('Bus'),
                           multipleChoice_item.createChoice('Drive'),
                           multipleChoice_item.createChoice('Walk')
                         ])
                         .showOtherOption(true);
     return multipleChoice_item
}

function addInterestGridWithValidation(form){
      let grid_item = form.addGridItem();
      grid_item.setTitle('Rate your interests')
               .setRows(['SDE', 'Test Engineer', 'Project Manager'])
               .setColumns([5, 4, 3,2,1])
               .setHelpText("It won't affect your scores in interviewing.");

      let gridValidation = FormApp.createGridValidation()
                                  .setHelpText("Select one item per column.")
                                  .requireLimitOneResponsePerColumn()
                                  .build();

      grid_item.setValidation(gridValidation);
      return grid_item
}

function addSkillCheckbox(form){
      let checkbox_item = form.addCheckboxItem();
      checkbox_item.setTitle('What are your technical skillsets')
                   .setChoices([
                        checkbox_item.createChoice('Python'),
                        checkbox_item.createChoice('JavaScript'),
                        checkbox_item.createChoice('HTML5'),
                        checkbox_item.createChoice('CSS3')
                  ])
                  .showOtherOption(true);
      return checkbox_item
}

function addAvailableDateTime(form){
      let date_time_item = form.addDateTimeItem();
      date_time_item.setTitle('When is your availability?');
      return date_time_item
}

function addSuggestionParagraphText(form){
      let paragraph_text_item = form.addParagraphTextItem();
      paragraph_text_item.setTitle('Any question or suggestion?');
      return paragraph_text_item
}

function addRateScale(form){
      let scale_item = form.addScaleItem();
      scale_item.setTitle('Rate this form')
                .setBounds(1, 5);
      return scale_item
}

function writeForm(curr_form){
      curr_form.setTitle('D12 Form').setDescription('Description of form \nTest for new line');

       let text_name = addNameText(curr_form);
       let list_item = addLunchList(curr_form);
       let multipleChoice_item = addTrafficChoices(curr_form);
       let grid_item = addInterestGridWithValidation(curr_form);
       let checkbox_item = addSkillCheckbox(curr_form);
       let date_time_item = addAvailableDateTime(curr_form);
       let paragraph_text_item = addSuggestionParagraphText(curr_form);
       let scale_item = addRateScale(curr_form);
            
       return curr_form
}

好,那我们一个个来讲。顺序依照最上面图的顺序。

设定表单标题与叙述(Title / Description)

这边应该算好理解,针对 form 本身用 setTitle() 设定标题,也透过 setDescription()设定标题下的叙述。

form.setTitle('D12 Form').setDescription('Description of form \n Test for new line');

眼尖的朋友应该有看到我有加入一个 '\n' 在 Description,这是「换行符号」,也就是在叙述段落如 description 时,可以透过加上这符号进行换行。换句话说,如果输入

// 会出现连续的 123
.setDescription('123')
123

// 会出现分成三行的 1, 2, 3 
.setDescription('1\n2\n3')
1
2
3

补充的是,这边有三个功能是可以加上的,分别是收到回应的确认讯息、是否允许编辑与是否接受重复回应。

form.setConfirmationMessage('Thanks for responding!')
    .setAllowResponseEdits(true)
    .setAcceptingResponses(false);

对应的回应关系图如下——

这张表是用中文版 Google Form 在新增问题时的顺序,也是我们接下来列点介绍的顺序。

设定简答与段论问题(Text / Paragraph Text)

这边很简单地用了 setTextItem() 作为了设定问题的方式,并且针对这个新增的问题用 setTitle() 来给予叙述,并且用 setRequired 来设定必填。

let text_name = form.addTextItem().setTitle('Name').setRequired(true);

而对应的段落也是用 .addParagraphTextItem() 即可。

let paragraph_text_item = form.addParagraphTextItem();
paragraph_text_item.setTitle('Any question or suggestion?');

但如果有时候我们想要加上一些限制,像是至少输入 100 字,那要怎麽做?这时就要用到 createParagraphTextValidation 来执行。范例程序码如下——

let paragraphtextValidation = FormApp.createParagraphTextValidation()
                                     .setHelpText(“Answer must be more than 100 characters.”)
                                     .requireTextLengthGreatherThan(100);
paragraph_text_item.setValidation(paragraphtextValidation);

而其总共有六种模式可以设定,分别是...

  • 设定回应需要含有以下 Pattern requireTextContainsPattern(pattern):通常是开头、结尾需要是特定格式。 e.g. email 的信箱位址。
  • 设定回应不得含有以下 Pattern requireTextDoesNotContainPattern(pattern)
  • 设定回应需要吻合以下 Pattern requireTextMatchesPattern(pattern):通常是字句中需要含有特定关键字、模式。 e.g. 含有三码邮递区号数字後接上文字
  • 设定回应不得吻合以下 Pattern requireTextDoesNotMatchPattern(pattern)
  • 设定回应长度大於或等於 数字requireTextLengthGreaterThanOrEqualTo(number):e.g. 地址文字中不得含有数字(需用国字中文之类)
  • 设定回应长度小於或等於 数字 requireTextLengthLessThanOrEqualTo(number)

上面的功能中所提到的 Pattern,其实就是 Regex(Regular Expression 正规表示式)。这边给一个使用的范例。

let paragraphtextValidation = FormApp.createParagraphTextValidation()
                                     .requireTextContainsPattern('[a-zA-Z]')                                     
paragraph_text_item.setValidation(paragraphtextValidation);

上面所写的这个范例就是,检查输入的内容只能是英文大写 [A-Z] 或小写 [a-z] 。更详细 Regex 可以到 regexonelearn regex 学,很详尽。

设定选择题(Multiple Choice)

下面这段程序码,主要是先用 addMultipleChoiceItem() 创造一个 object,并接着用 setChoices()createChoice)_ 来创造选项们,最後设定 showOtherOption() 来让填答人可以自行输入「其他」。

let multipleChoice_item = form.addMultipleChoiceItem();
  multipleChoice_item.setTitle('How do you come to our office?')
                     .setChoices([
                       multipleChoice_item.createChoice('Train'),
                       multipleChoice_item.createChoice('Bus'),
                       multipleChoice_item.createChoice('Drive'),
                       multipleChoice_item.createChoice('Walk')
                     ])
                     .showOtherOption(true);

设定核取方块(Checkbox)

方式跟设定选择题几乎一样,只差在是用 addCheckboxItem() 来建造。

let checkbox_item = form.addCheckboxItem();
checkbox_item.setTitle('What are your technical skillsets')
             .setChoices([
                    checkbox_item.createChoice('Python'),
                    checkbox_item.createChoice('JavaScript'),
                    checkbox_item.createChoice('HTML5'),
                    checkbox_item.createChoice('CSS3')
             ])
             .showOtherOption(true);

额外有 CheckboxValidationBuilder 可以建造验证程序,方式包括

  • 至少填入几个选项 requireSelectAtLeast()
  • 至多填入几个选项 requireSelectAtMost()
  • 刚好填入几个选项 requireSelectExactly()

提供官方范例给大家参考~

var checkBoxValidation = FormApp.createCheckboxValidation()
                                .setHelpText(“Select two condiments.”)
                                .requireSelectExactly(2)
                                .build();
checkBoxItem.setValidation(checkBoxValidation);

设定下拉式选单(List)

透过 addListItem() 来建置即可,比较没看到特别好玩的部分

 let list_item = form.addListItem();
  list_item.setTitle('Option for the Lunch')
           .setChoices([
                list_item.createChoice('Meat'),
                list_item.createChoice('Vegetarian')
            ]);

设定线性刻度(Scale)

透过 addScaleItem() 来建置。基本上是输入数值。那身为中文使用者,会很想问说,那怎麽样输入中文?这时就要透过 setLabels('Bad', 'Good')的方式。

let scale_item = form.addScaleItem();
scale_item.setTitle('Rate this form')
          .setBounds(1, 5)
          .setBounds('Bad', 'Good');

设定单选方格(Grid) 和 核取方块格(Checkbox Grid)

藉由 addGridItem 来新增。

let grid_item = form.addGridItem();
  grid_item.setTitle('Rate your interests')
           .setRows(['SDE', 'Test Engineer', 'Project Manager'])
           .setColumns([5, 4, 3,2,1])
           .setHelpText("It won't affect your scores in interviewing.");

且一样有 GridValidationBuilder 可以建立。但方式只有一种,也就是限制每一直栏都只能有一个被填入:requireLimitOneResponsePerColumn()。程序码如下。

let gridValidation = FormApp.createGridValidation()
                              .setHelpText("Select one item per column.")
                              .requireLimitOneResponsePerColumn()
                              .build();

  grid_item.setValidation(gridValidation);

那会想问,如果我想设定的是每个横的行,都必须要填入一个选项呢?就单纯用 .setRequired(true) 即可做到了。

那至於「核取方块格」呢?因为方式都一样,所以可以单纯地把 addGridItem 换成 addCheckboxGridItem。两者的 validation method 也都都是只有一种,但核取方块格需要将 GridValidationBuilder 换成 CheckboxGridValidationBuilder() 就是。

设定日期与时间(addDateTimeItem)

日期与时间也相对单纯,用 addDateTimeItem() 即可。

let date_time_item = form.addDateTimeItem();
date_time_item.setTitle('When is your availability?');

Step 3 读取 Google Sheet 里面的资料来客制 Google Form

好,但我们的重点是客制化表单,要怎麽样将原本的做表格变客制化呢?这边先用个简单的方式。

function writeForm(){
  let data = readData();
  let new_forms_id_arr = [];
  for(row_data of data){
   let form_name = row_data[0];
   let form_description = row_data[1];
   
   let curr_form = FormApp.create(form_name);
   let curr_form_id = curr_form.getId()
   new_forms_id_arr.push([curr_form_id]);
   moveFile(curr_form_id, target_folder_ID)
   curr_form.setTitle(form_name).setDescription(form_description);
   
   let question_function_list = [addNameText,
                                 addLunchList,
                                 addTrafficChoices,        
                                 addInterestGridWithValidation,
                                 addSkillCheckbox,
                                 addAvailableDateTime,
                                 addSuggestionParagraphText,
                                 ]
     
    for(let i = 2; i< row_data.length; i++){
        if(row_data[i] == true){
            question_function_list[i-2](curr_form);
        }
    }
    
   // add general item
   addRateScale(curr_form);
  }
  writeData(new_forms_id_arr)
}

里头要用到的功能上方都有写,可以直接复制喔!

Step 4 将创造後的表单 ID 写回 Google Sheet

其实已经偷偷写在上面的 Code 里面了,请看 Step 3 最後 writeData() 的部分。

完整执行画面——

回对我们的任务表,确认 Ben 表单中是没有「下拉选单」午餐编号的。

任务完成!


好,我们总算完成了(落泪),虽然昨日 D11 介绍的第一种方式比较简单,但实际上想弹性运用,我们会需要很多 D12 的内容。但,如果我们今天真的生了 100 份表单,那要怎麽样统一回应?总不能慢慢搜集吧。这时候就会需要看我们的 D13 了。

不知不觉就写了一整天...Orz,希望大家喜欢。一样提醒,用FormApp.create()来创造表单是有 Quota 限制——每天不超过 250 份。如果还有问题,透过留言之外,也可以到 Facebook Group,想开很久这次铁人赛才真的开起来哈哈哈,欢迎来当 Founding Member。如果不想错过可以订阅按赞小铃铛(?),也欢迎留言跟我说你还想知道什麽做法/主题。我们明天见。


<<:  登录档结构和物理位置--一颗四处散落的tree

>>:  [Angular] Day12. Template variables

DAY 5 html 基础网页

在昨天将 index 成功推上 Github 後,今天该让他有点东西了。 打开 VSCODE 後 按...

Day 24 开发者福音无服务器运算

随着资讯技术普及与推陈布新,基础设施及服务(IaaS)、平台即服务(PaaS)、软件即服务(Saa...

进入主题-建置本地PYTHON API环境

前面10天测试完大概的API功能後, 今天本来要开始建立API方法, 但用Anaconda建置API...

Day 16 - UML x Interface — TextField

今天的 TextField 和明天的 FormControl 都是在介绍跟表单有关的介面和元件,而...

EP 05 - [TDD] HashID 计算

Youtube 频道:https://www.youtube.com/c/kaochenlong ...