如何用Script修改Google Form question包含条件,并增加"other"选项?

How to use Script to modify Google Form question that contains conditions, and add "other" option?

任务

我有一个 Google 表格(参见简化的 test form here) that sends the submissions to a Google Spreadsheet (see test sheet here)。每个多项选择题都包含一个“其他”选项。

将表单提交给 sheet 后,将触发脚本函数 updateGoogleForm()(在下面的代码测试 sheet 中停用),该函数会修改 Google使新的“其他”提及成为预定义响应选项的一部分。

三个挑战:

  1. 我无法修改带有“根据答案转到部分”条件的问题的回答选项(例如'Question 2' 在测试表格中)。使用 setChoiceValues(MY_VALUES) 尝试此操作将抛出 Exception: Invalid data updating form. 我的解决方法是完全删除并重建相应的问题以保留跳过逻辑(在下面的代码片段中针对问题 2 完成)。

  2. 但是,删除并重新添加相同的问题将在 sheet 中创建一个新列来接收响应(从列开始H of the test sheet),尽管使用了相同的问题wording/title,即它被视为一个全新的问题,使得难以分析结果。我可以避免这种情况吗?

  3. 我不能向包含跳过条件的问题添加“其他”选项,至少不能使用 showOtherOption(true),如下面的代码示例 - 它也会抛出异常 Invalid data updating form。有没有其他方法可以通过编程方式执行此操作?

例子

这里是删除和重建待修改问题的变通函数:

const updateGoogleForm = () => {
  
  const GOOGLE_SHEET_NAME = 'Choices_for_form'; // Name of sheet in the Google spreadsheet that this script is linked to
  const GOOGLE_FORM_ID = '<form_id>'; // ID of the Google Form
  
  const ss = SpreadsheetApp.getActiveSpreadsheet(); // Get active (this) Google SHEET that contains the survey responses and response options
  const form = FormApp.openById(GOOGLE_FORM_ID); // Get our Google FORM to be edited
  const formSections = form.getItems(FormApp.ItemType.PAGE_BREAK); // Get all page breaks in form
  var formSectionIDsByTitle = [];
  for (i in formSections) formSectionIDsByTitle[formSections[i].getTitle()] = formSections[i].getId(); // Make an associative array of page break IDs that can be accessed by the section title
  
  // Get all data of the spreadsheet that contains the response options (one question per column)
  const [header, ...data] = ss
    .getSheetByName(GOOGLE_SHEET_NAME)
    .getDataRange()
    .getDisplayValues();
  
  // Write response options to an array that has the question titles as its keys and the response options as values
  const choices = {};
  header.forEach((title, i) => {
    choices[title] = data.map((d) => d[i]).filter((e) => e); // For each question item, store response options (choices) in array
  });

  // Iterate over all question items and replace the response options in the form
  const formItems = form.getItems(); // Get all question items from form
  for (i = 0; i < formItems.length; i++) {
    
    var itemTitle = formItems[i].getTitle() || null; // Get question title (label)
    if (!itemTitle || (itemTitle && !choices.hasOwnProperty(itemTitle))) continue; // If we don't have values for this question in the spreadsheet, skip
    
    var valuesToUse = choices[itemTitle];
    var itemType = formItems[i].getType();
    
    Logger.log("Updating item ID=" + formItems[i].getId(), ', TYPE=' + itemType + ', item title="' + itemTitle + '"');
    
    // Replace response choices depending on the question type
    switch (itemType) {
        case itemType.CHECKBOX:
          formItems[i].asCheckboxItem().setChoiceValues(valuesToUse);
          break;
        case itemType.LIST:
          formItems[i].asListItem().setChoiceValues(valuesToUse);
          break;
        case itemType.MULTIPLE_CHOICE:
        
          // Simply setting new choices for questions that contain skip conditions ("Go to section based on answer")
          // will throw an exception ("Invalid data updating form."), therefore reconstructing this question entirely.
          if (itemTitle=='Question 2: What is your favorite animal?') { // This is the question that requires skipping sections based on answers
            Logger.log('Rebuilding the question directing conditional skips.');
            form.deleteItem(formItems[i].getIndex()); // Delete the current item and replace it with a new item
            var newItem = form.addMultipleChoiceItem().setTitle('Question 2: What is your favorite animal?'); // Recreate the question item with the added options and add conditional skips
            newItem.setHelpText('You can add multiple "other" options by separating them with a comma (,) each');
            var eventTypeChoices = [];
            for (var j = 0; j < valuesToUse.length; j++) {
              if (valuesToUse[j] == 'Cat') 
                  eventTypeChoices.push(newItem.createChoice(valuesToUse[j], form.getItemById(formSectionIDsByTitle['Questions about cats']).asPageBreakItem()));
                else if (valuesToUse[j] == 'Dog')
                  eventTypeChoices.push(newItem.createChoice(valuesToUse[j], form.getItemById(formSectionIDsByTitle['Questions about dogs']).asPageBreakItem()));
                else
                  eventTypeChoices.push(newItem.createChoice(valuesToUse[j], form.getItemById(formSectionIDsByTitle['Concluding section for everyone']).asPageBreakItem()));
            }
            // Assign response choices to the newly re-built question
            newItem.setChoices(eventTypeChoices).setRequired(true);
            //newItem.showOtherOption(true); // This doesn't work because the question contains choices that require "going to section based on answer" (conditional skips)
            form.moveItem(newItem.getIndex(), formSections[0].getIndex()); // Need move the question into its original place (end of first section, section index = 0)
          }
          else {
            // This is a "regular" multiple choice item without condition skips,
            // we can simply replace the choices
            formItems[i].asMultipleChoiceItem().setChoiceValues(valuesToUse);
          }
          break;
        default:
        // ignore other items
      }
  }
  Logger.log('Google Form updated successfully!');
};

例如解决方案

这是 Alessandro 为上面使用的代码示例提出的工作解决方案。这段代码修改了现有的问题项,而不是删除和重建问题。

作为先决条件,问题需要通过 UI 以 navigationType 的形式设置所有选项,包括“其他”选项。

诀窍是再次将问题作为多项选择项 var theQuestion = form.getItemById(formItems[i].getId()).asMultipleChoiceItem() 并使用它来创建和设置选项。

case itemType.MULTIPLE_CHOICE:
  if (itemTitle=='Question 2: What is your favorite animal?') { // This is the only question containing navigationType
    var theQuestion = form.getItemById(formItems[i].getId()).asMultipleChoiceItem();
    var newChoices = [];
    for (var j = 0; j < valuesToUse.length; j++) {
      if (valuesToUse[j] ==  'Cat') 
        newChoices.push(theQuestion.createChoice(valuesToUse[j], form.getItemById(formSectionIDsByTitle['Questions about cats']).asPageBreakItem()));
      else if (valuesToUse[j] == 'Dog')        
        newChoices.push(theQuestion.createChoice(valuesToUse[j], form.getItemById(formSectionIDsByTitle['Questions about dogs']).asPageBreakItem()));
      else 
        newChoices.push(theQuestion.createChoice(valuesToUse[j], form.getItemById(formSectionIDsByTitle['Concluding section for everyone']).asPageBreakItem()));
    }
    theQuestion.setChoices(newChoices);
  }
  else {             
     formItems[i].asMultipleChoiceItem().setChoiceValues(valuesToUse);
  }
  break;

我无法通过直接使用 formItems[i].asMultipleChoiceItem().createChoice()formItems[i].asMultipleChoiceItem().setChoices() 来完成这项工作。这引发了 Exception: Invalid conversion for item type: TEXT.

解决方案

1) 选择数组必须一致

您不能使用 setChoiceValues(MY_VALUES),因为您正在更新的问题需要 (value, navigationType).

类型的 Choice 实例

来自documentation

Choices that use page navigation cannot be combined in the same item with choices that do not use page navigation.

2) 用 .setChoices()

更新现有问题

您可以使用与创建新问题相同的代码更新您需要的问题。请务必构建一个包含 navigationType 参数的一致的 Choices 数组。

3) 添加“其他”选项

“其他”选项只能在没有 navigationType 参数的选项上以编程方式设置。这恰好是一个 bug:目前,您只能从 UI 为包含 navigationType 参数的选择创建此选项。

如果“其他”选项已设置为正确的 navigationType,您可以使用 setChoices(choices) 更新现有问题。

参考

createChoice(value, navigationType)