首页 > 解决方案 > 当用户发送或编辑时,如何制作通过电子邮件发送 PDF 的脚本

问题描述

这是上一个问题的附录:Problems generate PDFs from Google Forms after it is submit

我正在创建一个谷歌脚本,它根据谷歌表单的字段填充谷歌文档模板,并通过电子邮件将生成的 PDF 发送给用户。现在我需要实现用户在按下“发送表单”后可以编辑其提交的可能性。

这是文档模板示例:

文件模板

这就是电子表格的样子(我为列枚举添加了第二行):

时间戳 电子纠错方向 模型 生日 (1) 汽车 (1) 添加新行? 生日 (2) 汽车 (2) 添加新行? 生日 (3) 汽车 (3) 什么时候
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
2021 年 2 月 4 日 20:30:53 example1@gmail.com 一个 F1 2005年 Z 2008 年 3 月 15 日 毫米 1996 年 12 月 12 日 - ñ 2009 年 3 月 5 日
2021 年 2 月 4 日 20:31:05 example2@gmail.com F2 2006年 X 2005 年 3 月 2 日 万维网 ñ 2000 年 8 月 24 日
2021 年 3 月 4 日 21:40:04 example3@gmail.com C F3 2018 - TT 2004 年 3 月 3 日 54 - 43 2019 年 12 月 24 日

感谢鲁本的回答,我可以处理一些领域。到目前为止,这是我的代码:

function onSubmit(e) { // Function taken from https://medium.com/swlh/hacking-it-generate-pdfs-from-google-forms-3ca4fcc5a0aa
  const rg = e.range;
  const sh = rg.getSheet();

  const values =  e.range.getValues().flat();

  const cEmail = e.namedValues['Dirección de correo electrónico'][0] ? e.namedValues['Dirección de correo electrónico'][0] : values[1];
  const cSurname = e.namedValues['Surname'][0] ? e.namedValues['Surname'][0] : values[2];
  const cFirstName = e.namedValues['First name'][0] ? e.namedValues['First name'][0] : values[3];
  const cYear = e.namedValues['Year'][0] ? e.namedValues['Year'][0] : values[4];
  const cModel = e.namedValues['Model'][0] ? e.namedValues['Model'][0] : values[5];

  // EXTRACT DAY, MONTH AND YEAR from https://stackoverflow.com/a/66909780/11617040

  const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

  // Birthday 1
  var dateStrBirthday1 = e.namedValues['Birthday (1)'][0] ? e.namedValues['Birthday (1)'][0] : values[6];
  var dateArrBirthday1 = dateStrBirthday1.split("/");
  var newDateBirthday1 = dateArrBirthday1[1] + "/" + dateArrBirthday1[0] + "/" + dateArrBirthday1[2];
  var dateBirthday1 = new Date(newDateBirthday1);
  var cBDay1 = dateBirthday1.getDate();
  var cBMonth1 = dateBirthday1.getMonth(); // To use month with letters write monthNames[cMonth]
  var cBYear1 = dateBirthday1.getFullYear();
  // Birthday 2
  var dateStrBirthday2 = e.namedValues['Birthday (2)'][0] ? e.namedValues['Birthday (2)'][0] : values[9];
  var dateArrBirthday2 = dateStrBirthday2.split("/");
  var newDateBirthday2 = dateArrBirthday2[1] + "/" + dateArrBirthday2[0] + "/" + dateArrBirthday2[2];
  var dateBirthday2 = new Date(newDateBirthday2);
  var cBDay2 = dateBirthday2.getDate();
  var cBMonth2 = dateBirthday2.getMonth(); // To use month with letters write monthNames[cMonth]
  var cBYear2 = dateBirthday2.getFullYear();
  // Birthday 3
  var dateStrBirthday3 = e.namedValues['Birthday (3)'][0] ? e.namedValues['Birthday (3)'][0] : values[12];
  var dateArrBirthday3 = dateStrBirthday3.split("/");
  var newDateBirthday3 = dateArrBirthday3[1] + "/" + dateArrBirthday3[0] + "/" + dateArrBirthday3[2];
  var dateBirthday3 = new Date(newDateBirthday3);
  var cBDay3 = dateBirthday3.getDate();
  var cBMonth3 = dateBirthday3.getMonth(); // To use month with letters write monthNames[cMonth]
  var cBYear3 = dateBirthday3.getFullYear();
  // ^ This code should be done with a for loop


  const cCar = [];

  for (var i=1; i<=3; i++) {
    // Here I should do the same but for Birthday (i)
    cCar.push(e.namedValues['Car (' + i + ')'][0] ? e.namedValues['Car (' + i + ')'][0] : values[7+(i-1)*3]);
  }

  const cWhen = e.namedValues['When'][0] ? e.namedValues['When'][0] : values[14];

  //Build a new invoice from the file
  //Folder and file IDs
  const folderAnswersID = '........';
  const folderAnswers = DriveApp.getFolderById(folderAnswersID);

  // Add actual time to the file name
  var today = new Date();
  const newFileName = 'Example - ' + cSurname + ' ' + cFirstName + ' ' + today.getDate() + ' ' + (today.getMonth()+1) + ' ' + today.getFullYear() + ' ' + today.getHours() + '_' + today.getMinutes();

  const temapleFileID = '...';
  
  //Make a copy of the template file
  const newAnswersFileID = DriveApp.getFileById(temapleFileID).makeCopy(newFileName, folderAnswers).getId();
  
  //Get the invoice body into a variable
  var document = DocumentApp.openById(newAnswersFileID);
  var body = document.getBody();

  //Replace all the < > text in the invoice body
  body.replaceText('<Surname>', cSurname);
  body.replaceText('<FirstName>', cFirstName);
  body.replaceText('<Year>', cYear);
  body.replaceText('<Model>', cModel);

  body.replaceText('<BDay1>', cBDay1);
  body.replaceText('<BDay2>', cBDay2);
  body.replaceText('<BDay3>', cBDay3);
  body.replaceText('<BMonth1>', cBMonth1);
  body.replaceText('<BMonth2>', cBMonth2);
  body.replaceText('<BMonth3>', cBMonth3);
  body.replaceText('<BYear1>', cBYear1);
  body.replaceText('<BYear2>', cBYear2);
  body.replaceText('<BYear3>', cBYear3);

  for (var i=1; i<=3; i++) {
    body.replaceText('<Car' + i + '>', cCar[i-1]);
  }
  
  body.replaceText('<When>', cWhen);

  document.saveAndClose();
  
  // From https://stackoverflow.com/a/66862676/11617040
  var docblob=document.getAs('application/pdf').setName(newFileName + '.pdf');
  var dupdocs=folderAnswers.getFilesByName(newFileName);
  while(dupdocs.hasNext()) {
    dupdocs.next().setTrashed(true);
  }
  var file=folderAnswers.createFile(docblob);

  var emailTitle = 'Your answer was submitted!';
  var emailBody = 'Dear <b>' + cFirstName + '</b>:<br><br>The document <b>' + newFileName + '</b> was submitted correctly. Please see the attachment.';

  var attachment = DriveApp.getFileById(newAnswersFileID);
  GmailApp.sendEmail(
    cEmail,
    emailTitle,
    "",
    {
      htmlBody: emailBody,
      attachments: [attachment.getAs(MimeType.PDF)]
    }
  );
}

我也有 2 个触发器:

我在代码上标记了 2 个问题。我需要关心两种类型的“字段”:

  1. “非重复字段”:姓氏、名字、年份、型号、时间。这些字段不需要循环for,因此代码不会那么长:
    • “非日期字段”:姓、名、年。它们并不特别。
    • “日期字段”:WHEN。这是 Google 表单上的日期字段,但没有什么特别之处。
  2. “重复字段”:日、月、年、车。这些字段确实需要一个for循环,因此代码不会那么长:
    • “非日期字段”:Car。我for为这个字段创建了一个循环。
    • “日期字段”:日、月、年。这些字段是日期字段,但它们需要分开,所以我分别有天、月和年。这已在上一个问题中解决,但我不知道如何for为这些问题创建一个循环。

我的问题是:我们如何使用表单,用户可以在按下“发送表单”后编辑其提交,而不是对列标题进行硬编码,也不关心分解日期字段并for以奇特的方式使用循环?

如果for循环问题太难回答,你可以忘记它,我需要尽快实现用户所做的编辑。

标签: google-apps-scriptgoogle-sheetstriggersgoogle-forms

解决方案


方法一

为了让接收 PDF 的用户能够编辑他们之前提交的表单,您可以使用该getEditResponseUrl()方法包含为他们的表单响应生成的编辑 URL。

例如,如果您要修改文档以包含标签,则可以按如下方式插入 URL:

function onSubmit(e){
  /*
  snip
  */

  var myFormId = "1234567890abcdefghijklmn"; // replace with your Form ID
  var allResponses = FormApp.openById(myFormId).getResponses();
  var lastResponse = allResps[allResps.length-1];

  var editUrl = lastResponse.getEditResponseUrl();
  
  body.replaceText('<editURL>', editUrl);
  
  /*
   snip
  */    

}

代码故意冗长以使步骤清晰;您可以将其合并为 2 行。

上面的代码获取最后一个响应的编辑 URL,这可能是一个问题......

方法二

如果你在短时间内有很多提交,有可能最后一次提交不是触发代码的提交。那么方法 1 不是推荐的方法。

相反,您必须使用一些标识符(例如时间戳,甚至可能是用户名)来获取与事件对象中的信息相关的特定响应。

您可能还需要在格式化时间戳方面做一些工作以便比较它们。

例如,

function onSubmit(e){
  /*
  snip
  */

  var myFormId = "1234567890abcdefghijklmn"; // replace with your Form ID

  // handle the timestamp
  var timestampParts = e.namedValues.Timestamp.toString().split(" ");
  var tParts = timestampParts[1].split(":");
  var dParts = timestampParts[0].split("/");
  var tstamp = new Date(dParts[2], dParts[1]-1, dParts[0], tParts[0], tParts[1], tParts[2]);
  var tEarlier = new Date(tstamp);
  
  // get the time 3 seconds earlier, tEarlier
  tEarlier.setSeconds(tstamp.getSeconds() - 3); // tune "3" to appropriate value

  // get all responses since tEarlier
  var allResponses = FormApp.openById(myFormId).getResponses(tEarlier);

  // loop over the responses and check the event object timestamp and username match the response item timestamp and username
  for (let i=0;i<allResponses.length;i++){
    if( Date.parse(allResponses[i].getTimestamp()) === Date.parse(tstamp) 
        && e.namedValues["Email address"] === allResponses[i].getRespondentEmail()
    ){
      // Match! Grab the edit URL
      var editUrl = allResps[i].getEditResponseUrl();    
    }else{
      // Not a match! Something went wrong (e.g. event object missing, triggered twice, etc.)
      var editUrl = "Not found";
      // log somewhere and investigate 
    } 
  }

body.replaceText('<editURL>', editUrl);

/*
snip
*/

考虑点:

  • 如果您不小心,时间戳可能会很麻烦。我必须执行上述操作,因为时间戳来自 dd/mm/yy 格式的表单,但 JS 使用 yy/mm/dd(至少对我而言 - 它可能因您的语言环境而异)。
  • 第二个标识符(如电子邮件地址)不是必需的,但建议使用。我之前看到过表单的奇怪行为,其中事件触发了两次,或者被延迟了等,因此进行此检查对于确保某人不会获得其他人的编辑 URL 非常重要,以防出现您无法控制的问题。

一些参考

其他方法可以从表单提交中获取最后响应

有关这些方法的更多信息(和示例),请参阅FormResponse API 参考


推荐阅读