-
Notifications
You must be signed in to change notification settings - Fork 2
/
templates.js
766 lines (665 loc) · 34.5 KB
/
templates.js
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
/** Template generation is as follows:
*
* open up the configuring sheet
* read the configuring sheet. It tells us which templates exist, and a little bit about those templates.
* filter the templates, excluding all those which are not suitable for the current configuration.
*
* create a new folder
* for each suitable template, load the source HTML
* fill in the HTML template
* convert the HTMLOutput into Google Docs native
* put the google docs document into the new folder
*
*/
// ---------------------------------------------------------------------------------------------------------------- desiredTemplates_
var currentTemplate;
function desiredTemplates_(config) {
var toreturn = [];
if (! config.templates) return toreturn;
for (var i in config.templates.tree) {
var field = asvar_(i);
toreturn.push(field);
}
return toreturn;
}
function suitableTemplates(readRows_, parties) {
var availables = readRows_.availableTemplates;
// teLog(["suitableTemplates: available templates are %s", availables.map(function(aT){return aT.name})],8);
var desireds = desiredTemplates_(readRows_.config);
// teLog(["suitableTemplates: desired templates are %s", desireds.map(function(aT){return aT})],8);
var suitables = intersect_(desireds, availables); // the order of these two arguments matters -- we want to preserve the sequence in the spreadsheet of the templates.
// TODO: this is slightly buggy. kissing, kissing1, kissing2, didn't work
suitables = suitables.filter(function (aT) {
if (! aT.requires || ! aT.requires.length) {
return true;
} else {
if (aT.requires.filter(function (rP){
teLog("suitableTemplates filtering: %s requires %s; there are %s such parties", aT.name, rP, parties[rP] ? parties[rP].length : "no");
return (parties[rP] && parties[rP].length);
}).length == aT.requires.length) {
teLog("suitableTemplates filtering: requirement for %s is met; including in suitables.", aT.name);
return true;
}
else {
teLog("suitableTemplates filtering: requirement for %s is not met; excluding from suitables.", aT.name);
return false;
}
}
});
teLog("filtered suitables = %s", suitables.map(function(e){return e.url}).join(", "));
return suitables;
}
// ---------------------------------------------------------------------------------------------------------------- filenameFor
// create a canonical filename for a given sourceTemplate,entity pair
function filenameFor(sourceTemplate, entity) {
var sequence = sourceTemplate.sequence;
if (sequence == undefined || sourceTemplate.sequence_length < 10) { sequence = "" } else { sequence = (sequence < 10 ? "0" : "") + sequence + " - " }
if (entity) return sequence + sourceTemplate.title + " for " + firstline_(entity.name) +
(entity.email ? (" " + firstline_(entity.email)) : "");
else return sequence + sourceTemplate.title;
};
// ---------------------------------------------------------------------------------------------------------------- obtainTemplate_
// obtainTemplate
// we can pull a generic HTML template from somewhere else,
// or it can be one of the project's HTML files.
function obtainTemplate_(url, nocache, readmeDoc) {
// teLog("obtainTemplate_(%s) called", url);
// we're actually running within a single script invocation so maybe we should find a more intelligent way to cache within a single session.
// otherwise this risks not picking up changes
if (url.match(/^http/)) {
if (nocache != true) {
var cache = CacheService.getDocumentCache();
var cached = cache.get(url);
if (cached != null) {
teLog(["nocache is false; using cached copy of %s", url],7)
return HtmlService.createTemplate(cached);
}
else {
teLog(["nocache is false but cache miss for %s", url],7)
}
}
else {
teLog(["nocache requested, forcing retrieve of %s", url],7);
}
try {
var result = UrlFetchApp.fetch(url, { headers: { "Accept-Encoding": "identity" } } );
} catch (e) {
teLog(["ERROR: caught error (%s) while fetching %s", e, url],3);
}
if (result == undefined) {
try {
result = UrlFetchApp.fetch(url, { headers: { "Accept-Encoding": "identity" } } );
} catch (e) {
teLog("ERROR: caught error (%s) while fetching %s for the second time!", e, url);
throw ("during obtainTemplate_(" + url + "): " + e);
}
}
// by default the good people at Github Pages will gzip compress if we don't explicitly set this
var contents = result.getContentText();
if (result.getResponseCode() != 200) {
if (readmeDoc) { readmeDoc.getBody().appendParagraph("obtainTemplate(" + url + ") returned response code " + result.getResponseCode()); }
result = UrlFetchApp.fetch(url, { headers: { "Accept-Encoding": "identity" } } );
contents = result.getContentText();
if (readmeDoc) { readmeDoc.getBody().appendParagraph("obtainTemplate(" + url + ") second try returned response code " + result.getResponseCode()); }
}
if (! contents || ! contents.length) {
if (readmeDoc) {
readmeDoc.getBody().appendParagraph("obtainTemplate(" + url + ") returned no contents"); teLog("obtainTemplate(" + url + ") returned no contents");
readmeDoc.getBody().appendParagraph(JSON.stringify(result.getAllHeaders())); teLog("obtainTemplate(" + url + ") headers: " + result.getAllHeaders());
}
throw("received zero-length content when fetching " + url);
}
// the cache service can only store keys of up to 250 characters and content of up to 100k, so over that, we don't cache.
if (nocache != true && contents.length < 100000 && url.length < 250) {
cache.put(url, contents, 300);
// a run of the google script may take up to 5 minutes, so cache for that time.
// if you're hot and heavy with the updates, set nocache:true in the sourceTemplate properties.
}
// teLog("obtained template %s, length %s bytes", url, contents.length);
return HtmlService.createTemplate(contents);
}
// TODO: find a way to expose the original script context to this section ... otherwise the add-on tries to satisfy createTemplateFromFile()
// out of the add-on library's script environment, which kinda defeats the purpose.
// this is tricky. it gets called from include() and it gets called from fillTemplates().
else return HtmlService.createTemplateFromFile(url);
}
// see documentation in notes-to-self.org
var docsetEmails = function (sheet, readRows_, parties, suitables) {
this.sheet = sheet;
this.readRows = readRows_;
this.parties = parties;
this.suitables = suitables;
var that = this;
teLog("docsetEmails(%s): running", this.sheetname);
this.sequence;
if (this.suitables.length > 1) { this.sequence = 1; } // each sourcetemplate gets a sequence ID. exploded templates all share the same sequence id.
this.esNumForTemplate = { };
// teLog("docsetEmails(%s): now I will figure out who gets which PDFs.", sheet.getSheetName());
// teLog("docsetEmails(%s): incoming readRows has entitiesByName = %s",
// sheet.getSheetName(),
// readRows.entitiesByName
// );
// populate rcpts
this._rcpts = { exploders: { }, normals: { } };
this._parties = { exploders: { }, normals: { } };
for (var i in suitables) {
var sourceTemplate = suitables[i];
if (this.sequence) { sourceTemplate.sequence = this.sequence++; this.sequence_length = suitables.length; }
var to_list = [], cc_list = [];
var to_parties = { }; // { director: [ Entity1, Entity2 ], company: [Company] }
var cc_parties = { };
var ex_parties = { }; // { new_investor: EntityX }
var nullIsOK = false;
for (var mailtype in sourceTemplate.parties) {
// teLog("docsetEmails: sourceTemplate %s: expanding mailtype \"%s\"", sourceTemplate.name, mailtype);
for (var mti in sourceTemplate.parties[mailtype]) { // to | cc
var partytype = sourceTemplate.parties[mailtype][mti]; // company, director, shareholder, etc
if (partytype == "") {
// teLog("docsetEmails: %s mailtype %s has blank partytypes. skipping.", sourceTemplate.name, mailtype);
continue;
}
if (partytype.toLowerCase() == "null") {
// teLog("docsetEmails: %s mailtype %s has deliberately blank partytypes. skipping.", sourceTemplate.name, mailtype);
nullIsOK = true;
continue;
}
// teLog("docsetEmails: discovered %s: will mail to %s", mailtype, partytype);
var mailindex = null;
// sometimes partytype is "director"
// sometimes partytype is "director[0]" indicating that it would be sufficient to use just the first director in the list.
// so we pull the 0 out into the mailindex variable
// and we reset partytype from "director[0]" to "director".
if (partytype.match(/\[(\d)\]$/)) { mailindex = partytype.match(/\[(\d)\]$/)[1];
partytype = partytype.replace(/\[\d\]/, "");
// teLog("docsetEmails: matched mailindex %s, simplified partytype to %s", mailindex, partytype);
}
if (mailtype == "to") { // teLog("docsetEmails: initializing to_parties[%s] as array", partytype);
to_parties[partytype] = to_parties[partytype] || [];
}
else cc_parties[partytype] = cc_parties[partytype] || [];
if (readRows_.principal.roles[partytype] == undefined) {
// teLog("docsetEmails: principal does not possess a defined %s role! skipping.", partytype);
continue;
}
for (var j in parties[partytype]) {
var entity = parties[partytype][j];
if (mailindex != undefined) {
if (j == mailindex) {
// teLog("docsetEmails: matched mailindex %s == %s, chosen %s", mailindex, j, entity.name);
}
else {
// teLog("docsetEmails: matched mailindex %s != %s, skipping %s", mailindex, j, entity.name);
continue;
}
}
// teLog("docsetEmails: what to do with %s entity %s?", partytype, entity.name);
if (mailtype == "to") {
to_list.push(entity.name);
to_parties[partytype].push(entity);
} else { // mailtype == "cc"
cc_list.push(entity.name);
cc_parties[partytype].push(entity);
}
}
}
}
if (sourceTemplate.explode == "") {
this._rcpts .normals[sourceTemplate.title]={to:to_list, cc:cc_list};
this._parties.normals[sourceTemplate.title]={to:to_parties, cc:cc_parties};
// teLog("docsetEmails: defining this._rcpts.normals[%s].to=%s",sourceTemplate.title, to_list);
// teLog("docsetEmails: defining this._rcpts.normals[%s].cc=%s",sourceTemplate.title, cc_list);
// teLog("docsetEmails: defining this._parties.normals[%s].to=%s",sourceTemplate.title,Object.keys(to_parties));
} else { // explode first and then set this._rcpts.exploders
// teLog("docsetEmails(): will explode %s", sourceTemplate.explode);
var primary_to_list = to_list; // probably unnecessary
for (var j in this.parties[sourceTemplate.explode]) {
var entity = parties[sourceTemplate.explode][j];
// we step through the desired {investor,company}.* arrays.
// we set the singular as we step through.
ex_parties[sourceTemplate.explode] = entity;
var mytitle = filenameFor(sourceTemplate, entity);
// teLog("docsetEmails(): preparing %s exploded %s", sourceTemplate.explode, mytitle);
var exploder_to_list = primary_to_list.concat([entity.name]);
// TODO: if the exploder's email is multiline there needs to be a way for it to append to the cc_list.
var exploder_to_parties = {};
for (var pp in to_parties) { exploder_to_parties[pp] = to_parties[pp] }
exploder_to_parties[sourceTemplate.explode] = [ entity ];
this._rcpts .exploders[mytitle] = {to:exploder_to_list, cc:cc_list};
this._parties.exploders[mytitle] = {to:exploder_to_parties,cc:cc_parties};
// teLog("docsetEmails: defining this._rcpts.exploders[%s].to=%s",mytitle,exploder_to_list);
// teLog("docsetEmails: defining this._rcpts.exploders[%s].cc=%s",mytitle,cc_list);
// teLog("docsetEmails: defining this._parties.exploders[%s].to=%s",mytitle,Object.keys(exploder_to_parties));
}
}
// teLog("docsetEmails: testing: does %s have To+CC/Explode? to_list=\"%s\"; explode=\"%s\"",
// sourceTemplate.name, to_list, sourceTemplate.explode);
if (to_list.length == 0 && sourceTemplate.explode=="" && ! nullIsOK) {
throw("in the Templates sheet, does " + sourceTemplate.name + " define To and CC parties? and are those parties defined?");
// TODO: sometimes the template does define to and cc, but the Entities/Roles neglect to define such.
// this error is misleading in those cases.
// test for those cases and throw a different, more instructive error.
}
else {
// teLog("docsetEmails: Template %s passed To+CC test: to_list=\"%s\"; explode=\"%s\"",
// sourceTemplate.name, to_list, sourceTemplate.explode);
}
}
// return to_cc for a given set of sourceTemplates
this.Rcpts = function(sourceTemplates, explodeEntity) { // explodeEntity may be null -- that's OK, just means we're not exploding.
// clear es_nums in entities
for (var e in this.readRows.entitiesByName) { this.readRows.entitiesByName[e]._es_num = null; this.readRows.entitiesByName[e]._to_email = null; }
var sourceTemplateNames = sourceTemplates.map(function(st){return st.name});
teLog("docsetEmails.Rcpts(%s), %s", sourceTemplateNames, explodeEntity);
// pull up all the entities relevant to this particular set of sourceTemplates
// this should be easy, we've already done the hard work above.
var all_to = [], all_cc = [];
var to_parties = {}, cc_parties = {}, explode_party = {};
for (var st in sourceTemplates) {
var sourceTemplate = sourceTemplates[st];
if (explodeEntity) {
var mytitle = filenameFor(sourceTemplate, explodeEntity);
all_to = all_to.concat(this._rcpts.exploders[mytitle].to);
all_cc = all_cc.concat(this._rcpts.exploders[mytitle].cc);
to_parties = this._parties.exploders[mytitle].to;
cc_parties = this._parties.exploders[mytitle].cc;
} else {
all_to = all_to.concat(this._rcpts.normals[sourceTemplate.title].to);
all_cc = all_cc.concat(this._rcpts.normals[sourceTemplate.title].cc);
to_parties = this._parties.normals[sourceTemplate.title].to;
cc_parties = this._parties.normals[sourceTemplate.title].cc;
}
}
// sometimes a person has to sign twice on the same document. maybe they are wearing different hats.
// there are smarts in the signature engine which properly consolidate duplicate rcpts To: addresses, to a single esnum.
all_to = uniq_(all_to);
all_cc = uniq_(all_cc);
teLog("docsetEmails.Rcpts(%s): all_to=%s", sourceTemplateNames, all_to);
teLog("docsetEmails.Rcpts(%s): all_cc=%s", sourceTemplateNames, all_cc);
var to_emails = [], cc_emails = [];
var es_num = 1;
for (var ti in all_to) {
var entityName = all_to[ti];
var entity = this.readRows.entitiesByName[entityName];
if (! entity) { teLog(["Rcpts: XXX tried to find %s in entitiesByName but couldn't!",
entityName],4); }
if (this.readRows.config.email_override && this.readRows.config.email_override.values[0]
&&
email_to_cc(entity.email)[0] && email_to_cc(entity.email)[0]) {
entity._to_email = plusNum(es_num, this.readRows.config.email_override.values[0]);
teLog("INFO: email override in effect for %s", entity._to_email);
}
else {
var email_to_cc_ = email_to_cc(entity.email);
entity._to_email = email_to_cc_[0];
// teLog("DEBUG: given entity %s, entity.email is %s and _to_email is %s", entityName, entity.email, entity._to_email);
cc_emails = cc_emails.concat(email_to_cc_[1]);
}
if (entity._to_email) {
to_emails.push(entity._to_email);
entity._es_num = es_num++;
entity._unmailed = true;
}
}
for (var ti in all_cc) {
var entityName = all_cc[ti]; var entity = this.readRows.entitiesByName[entityName];
var email_to_cc_ = email_to_cc(entity.email);
cc_emails = cc_emails.concat(email_to_cc_[0]).concat(email_to_cc_[1]); // both top and subsequent will go to CC
}
if (this.readRows.config.email_override && this.readRows.config.email_override.values[0]) {
cc_emails = [this.readRows.config.email_override.values[0]];
}
return [to_emails, cc_emails, to_parties, cc_parties];
};
// callback framework for doing things to do with normal sourceTemplates, for both concatenate_pdfs modes
this.normal = function(individual_callback, group_callback) {
var normals = suitables.filter(function(t){return ! t.explode});
teLog("docsetEmails.normal(): concatenateMode %s, templates=%s",
this.readRows.config.concatenate_pdfs && this.readRows.config.concatenate_pdfs.values[0] == true,
normals.map(function(t){return t.name}));
if (this.readRows.config.concatenate_pdfs && this.readRows.config.concatenate_pdfs.values[0] == true) {
var rcpts = this.Rcpts(normals);
for (var ni in normals) { individual_callback([normals[ni]], null, rcpts); }
if (group_callback) { group_callback(normals, null, rcpts); }
} else {
for (var ni in normals) { var rcpts = this.Rcpts([normals[ni]]); individual_callback([normals[ni]], null, rcpts); }
}
};
// callback framework for doing things to do with exploded sourceTemplates
this.explode = function(callback) {
var exploders = this.suitables.filter(function(t){return t.explode});
teLog("docsetEmails.explode(): templates=%s",
exploders.map(function(t){return t.name}));
for (var explode_i in exploders) {
var sourceTemplate = exploders[explode_i];
var partytype = sourceTemplate.explode;
teLog("template %s will explode = %s", sourceTemplate.name, partytype);
// teLog("parties[partytype] = %s", parties[partytype]);
for (var parties_k in parties[partytype]) {
var entity = this.readRows.entitiesByName[parties[partytype][parties_k].name];
if (! entity) { teLog(["docsetEmails.explode(): XXX tried to find %s in entitiesByName but couldn't!",
parties[partytype][parties_k].name],4); }
teLog(["docsetEmails.explode(): working with %s %s %s", partytype, entity.name, sourceTemplate.name],4);
if (entity.legalese_status
&& entity.legalese_status.match(/skip\s+explo\w+\s+[^;]+/) // skip exploding / skip exploder
&& entity.legalese_status.match(/skip\s+explo\w+\s+([^;]+)/)[1].match(sourceTemplate.name) // add \b, i think
) {
teLog(["docsetEmails.explode(%s): SKIPPING because legalese status says %s", entity.name, entity.legalese_status],4);
continue;
}
var rcpts = this.Rcpts([sourceTemplate], entity);
callback([sourceTemplate], entity, rcpts,
{ explodee:entity, partytype:sourceTemplate.explode, explodees:parties[partytype] } // details of the explosion
);
}
}
};
};
// ---------------------------------------------------------------------------------------------------------------- fillTemplates
function fillTemplates(sheet) {
var sheetPassedIn = ! (sheet == undefined);
if (! sheetPassedIn && (SpreadsheetApp.getActiveSpreadsheet().getName().toLowerCase() == "legalese controller"
||
SpreadsheetApp.getActiveSheet().getSheetName().toLowerCase() == "controller")
) {
teLog("in controller mode, switching to fillOtherTemplates()");
fillOtherTemplates_();
return;
}
sheet = sheet || SpreadsheetApp.getActiveSheet();
var sheetname = sheet.getSheetName();
var sheet_id = sheet.getParent().getId();
teLog(["fillTemplates(%s) called; will call readRows(%s)", sheetname, sheetname], 6);
var entitiesByName = {};
var readRows_ = new readRows(sheet, entitiesByName,0);
var templatedata = readRows_.terms;
var config = readRows_.config;
templatedata.clauses = {};
templatedata._config = config;
templatedata._availableTemplates = readRows_.availableTemplates;
templatedata._todays_date = Utilities.formatDate(new Date(), sheet.getParent().getSpreadsheetTimeZone(), "d MMMM YYYY");
templatedata._todays_date_wdmy = Utilities.formatDate(new Date(), sheet.getParent().getSpreadsheetTimeZone(), "EEEE d MMMM YYYY");
templatedata.sheet_id = sheet_id.substr(sheet_id.length-8);
// if the person is running this in Demo Mode, and there is no User entity defined, then we create one for them.
// then we have to reload.
if (createDemoUser_(sheet, readRows_, templatedata, config)) {
teLog(["reloading for demo mode: will call readRows(%s)", sheetname], 6);
readRows_ = new readRows(sheet, entitiesByName,0);
templatedata = readRows_.terms;
config = readRows_.config;
templatedata._config = config;
templatedata._availableTemplates = readRows_.availableTemplates;
}
var entityNames = []; for (var eN in readRows_.entitiesByName) { entityNames.push(eN) }
teLog("fillTemplates(%s): got back readRows_.entitiesByName=%s",
sheetname,
entityNames);
if (config.templates == undefined) {
throw("sheet doesn't specify any templates ... are you on the right sheet?");
return;
}
var uniq = uniqueKey(sheet);
// in the future we will probably need several subfolders, one for each template family.
// and when that time comes we won't want to just send all the PDFs -- we'll need a more structured way to let the user decide which PDFs to send to echosign.
var folder = createFolder_(sheet); var readmeDoc = createReadme_(folder, config, sheet);
PropertiesService.getDocumentProperties().setProperty("legalese."+uniq+".folder.id", JSON.stringify(folder.getId()));
PropertiesService.getDocumentProperties().setProperty("legalese."+uniq+".folder.name", JSON.stringify(folder.getName()));
PropertiesService.getDocumentProperties().setProperty("legalese.templateActiveSheetId", sheet.getSheetId());
teLog("fillTemplates: property set legalese.%s.folder.id = %s", uniq, folder.getId());
teLog("fillTemplates: property set legalese.%s.templateActiveSheetId = %s", uniq, sheet.getSheetId());
var cell = sheet.getRange("E6");
// let's insert the Drive version not the Docs version of the folder url
cell.setValue("=HYPERLINK(\"https://drive.google.com/drive/u/0/#folders/"+folder.getId()+"\",\""+folder.getName()+"\")");
teLog("I have set the value to =HYPERLINK(\"https://drive.google.com/drive/u/0/#folders/"+folder.getId()+"\",\""+folder.getName()+"\")");
// hardcode some useful expressions
templatedata.xml_declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
templatedata.whitespace_handling_use_tags = '<?whitespace-handling use-tags?>';
templatedata.whitespace_handling_use_characters = '<?whitespace-handling use-characters?>';
templatedata._timezone = sheet.getParent().getSpreadsheetTimeZone();
// the parties{} for a given docset are always the same -- all the defined roles are available
var parties = roles2parties(readRows_);
teLog(["fillTemplates: looking for negate_role: %s", config.negate_role],4);
if (config.negate_role &&
config.negate_role.dict) {
teLog(["fillTemplates: handling negate_role exceptions"],4);
// specifically negated roles: in config,
// negate role: / shareholder / Alice
for (var negatePartyType in config.negate_role.dict) {
teLog(["fillTemplates: handling negate_role exceptions"],4);
config.negate_role.dict[negatePartyType].map(function(negatePartyName) {
teLog(["fillTemplates: negating party %s from the list of %s", negatePartyName, negatePartyType],5);
if (parties[negatePartyType] &&
parties[negatePartyType].indexOf(negatePartyName) > -1) {
teLog(["fillTemplates: found and removing."],5);
}
parties[negatePartyType] = parties[negatePartyType].filter(function(pname){return (pname != negatePartyName)});
});
teLog(["fillTemplates: after negation, parties %s are %s", negatePartyType, Object.keys(parties[negatePartyType])], 6);
}
}
templatedata.parties = parties;
teLog("FillTemplates: INFO: assigning templatedata.parties = %s", Object.getOwnPropertyNames(templatedata.parties));
for (var p in parties) {
teLog("FillTemplates: INFO: parties[%s] = %s", p, parties[p].map(function(pp){return pp.name}));
}
var suitables = suitableTemplates(readRows_, parties);
templatedata.company = parties.company[0];
templatedata._entitiesByName = readRows_.entitiesByName;
var docsetEmails_ = readRows_.docsetEmails;
// you will see the same pattern in uploadAgreement.
var buildTemplate = function(sourceTemplates, entity, rcpts, explosion) { // this is a callback run within the docsetEmails_ object.
if (sourceTemplates.length > 1) {
teLog(["buildTemplate(%s): WARNING: given %s sourceTemplates; potential collision error", sourceTemplates.length],3);
teLog(["buildTemplate(%s): WARNING: URLs are ", sourceTemplates.map(function(st){ return st.url }).join(", ")],3);
}
var sourceTemplate = sourceTemplates[0];
var newTemplate = obtainTemplate_(sourceTemplate.url, sourceTemplate.nocache, readmeDoc);
newTemplate.data = templatedata; // NOTE: this is the first global inside the XML context
newTemplate.data.sheet = sheet; // NOTE: this is the second global inside the XML context
if (explosion != undefined) {
newTemplate.explosion = explosion;
}
if (templatedata._origparties == undefined) {
templatedata._origparties = {};
for (var p in parties) { templatedata._origparties[p] = parties[p] }
// teLog("buildTemplate(%s): preserving original parties", sourceTemplate.name);
}
else {
for (var p in templatedata._origparties) { templatedata.parties[p] = templatedata._origparties[p] }
// teLog("buildTemplate(%s): restoring original parties", sourceTemplate.name);
}
// EXCEPTION SCENARIO -- party overrides
//
// it is possible that in the Templates: line of the config section, one or more party overrides are defined.
//
// for instance, Template: | foobar | company | [promoter]
// means that when filling the foobar template, we should set data.parties.company = data.parties.promoter
// and, due to the special case, also set data.company = promoter[0].
//
// Template: | foobar | thing | SomeValue Pte. Ltd.
// means that for the foobar template, data.parties.thing = the entity named SomeValue Pte. Ltd.
//
// TODO
// this is a huge mess because we now overload the syntax to allow "requires".
// if we are going to seriously support this mechanism in future we should prefix it with a "party_override" key
// and use dict2 instead of dict.
teLog("buildTemplate(%s): config.templates.dict is %s", sourceTemplate.name, config.templates.dict);
if (config.templates.dict[sourceTemplate.name] && config.templates.dict[sourceTemplate.name].length) {
var mydict = config.templates.dict[sourceTemplate.name];
teLog("buildTemplate(%s): WE CAN HAZ OVERRIDE! coping with %s", sourceTemplate.name, config.templates.dict[sourceTemplate.name]);
var keyvalues = {};
while (config.templates.dict[sourceTemplate.name].length) { keyvalues[mydict.shift()] = mydict.shift() }
teLog("buildTemplate(%s): keyvalues = %s", sourceTemplate.name, keyvalues);
for (var kk in keyvalues) {
teLog("buildTemplate(%s): dealing with %s : %s", sourceTemplate.name, kk, keyvalues[kk]);
if (kk == "requires") {
teLog("buildTemplate(%s): that's not meant to be an override; skipping.", sourceTemplate.name);
continue;
}
var matches; // there is similar code elsewhere in readRows() under ROLES
if (matches = keyvalues[kk].match(/^\[(.*)\]$/)) {
// company: [promoter]
// means we temporarily substitute promoter for company
var to_import = asvar_(matches[1]);
// TODO: sanity check so we don't do a reflexive assignment
teLog("buildTemplate(%s): substituting %s = %s", sourceTemplate.name, kk, to_import);
if (! (templatedata.company.roles[to_import] && templatedata.company.roles[to_import].length)) {
teLog("buildTemplate(%s): ERROR: substitute [%s] is useless to us", sourceTemplate.name, to_import);
continue;
}
else {
teLog("buildTemplate(%s): substituting: before, parties.%s = %s", sourceTemplate.name, kk, templatedata.company.roles[kk]);
templatedata.parties[kk] = templatedata.parties[to_import];
teLog("buildTemplate(%s): substituting: after setting to %s, parties.%s = %s", sourceTemplate.name, to_import, kk, templatedata.parties[kk][0].name);
}
if (kk == "company") {
templatedata.company = templatedata.parties.company[0];
teLog("buildTemplate(%s): final substitution: company = %s", sourceTemplate.name, templatedata.company.name);
}
}
}
}
// teLog("buildTemplate: assigning newTemplate.data = %s", templatedata);
// teLog("buildTemplate: newTemplate.data.parties has length = %s", templatedata.data.parties.length);
// teLog("FillTemplates: recv: templatedata.parties = %s", templatedata.parties);
// entity is only true here if we're running in an exploder context, so it's ok for entity to be undefined when not in an exploder context.
if (entity) { newTemplate.data.party = newTemplate.data.party || {};
newTemplate.data.party[sourceTemplate.explode] = entity; // do we really want this? it seems to clobber the previous array
newTemplate.data [sourceTemplate.explode] = entity;
teLog(["assigning newTemplate.data[%s] = %s", sourceTemplate.explode, entity],7);
}
newTemplate.rcpts = rcpts;
newTemplate.rcpts_to = rcpts[2];
newTemplate.rcpts_cc = rcpts[3];
teLog("buildTemplate(%s): newTemplate.rcpts_to = %s", sourceTemplate.name, Object.keys(newTemplate.rcpts_to));
teLog("buildTemplate(%s): newTemplate.rcpts[0] = %s", sourceTemplate.name, rcpts[0].join(", "));
fillTemplate_(newTemplate, sourceTemplate, filenameFor(sourceTemplate, entity), folder, config);
readmeDoc.getBody().appendParagraph(filenameFor(sourceTemplate, entity)).setHeading(DocumentApp.ParagraphHeading.HEADING2);
readmeDoc.getBody().appendParagraph("To: " + rcpts[0].join(", "));
if (rcpts[1].length) readmeDoc.getBody().appendParagraph("CC: " + rcpts[1].join(", "));
};
teLog("FillTemplates(): we do the non-exploded normal templates");
docsetEmails_.normal(buildTemplate);
teLog("FillTemplates(): we do the exploded templates");
docsetEmails_.explode(buildTemplate);
var ROBOT = '[email protected]';
teLog("fillTemplates(): sharing %s with %s", folder.getName(), ROBOT);
folder.addEditor(ROBOT);
if (config.add_to_folder) {
var folderValues = [];
for (var i in config.add_to_folder.tree) {
var matches;
if (matches = i.match(/folders.*\/([^\/\?]+)/)) { // we want the rightmost folderid
folderValues.push(matches[1]);
}
}
for (var i = 0; i<folderValues.length; i++) {
var addToFolder = DriveApp.getFolderById(folderValues[i]);
if (addToFolder) {
teLog("fillTemplates(): config says we should add the output folder to %s", addToFolder.getName());
try { addToFolder.addFolder(folder); }
catch (e) {
teLog("fillTemplates(): failed to do so. %s", e);
}
}
else {
teLog("fillTemplates(): ERROR: unable to getFolderById(%s)!", folderValues[i]);
}
}
}
teLog(["that's all folks!"],6);
}
// ---------------------------------------------------------------------------------------------------------------- fillTemplate_
// fill a single template -- inner-loop function for fillTemplates() above.
//
// it's possible that a template references another template.
// the Google Docs HTMLTemplate engine is pretty basic and has no concept
// of modular components.
//
// so, we define an include() function.
function fillTemplate_(newTemplate, sourceTemplate, mytitle, folder, config, to_parties, explode_party) {
// reset "globals"
clauseroot = [];
clausetext2num = {};
newTemplate.data.signature_comment = null;
newTemplate.data._templateName = sourceTemplate.name;
currentTemplate = sourceTemplate.name;
teLog(["fillTemplate: starting %s (%s)", currentTemplate, mytitle],6);
// make this handle templatespec etc correctly. see inc_plain_letterhead.
var xmlRootExtras = [];
if (config.save_indd && config.save_indd.value) { xmlRootExtras.push('saveIndd="true"') }
if (config.omit_date && config.omit_date.value) { xmlRootExtras.push('omitDate="true"') }
newTemplate.data.xmlRoot = function(someText) {
var aid = ['xmlns:aid="http://ns.adobe.com/AdobeInDesign/4.0/"',
'xmlns:aid5="http://ns.adobe.com/AdobeInDesign/5.0/"'];
if (someText == undefined) { someText = [] }
else if (someText.constructor.name != "Array") { someText = [ someText ] }
return '<Root ' + aid.concat(xmlRootExtras, someText).join(" ") + ">";
};
var filledHTML = newTemplate.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).getContent();
var xmlfile;
if (sourceTemplate.url.match(/[._]xml(\.html)?$/)) {
teLog("templates: trying to create %s in %s", sourceTemplate.name, folder.getName());
xmlfile = folder.createFile(mytitle+".xml", filledHTML, 'text/xml');
}
else {
teLog("we only support xml file types. i am not happy about %s", sourceTemplate.url);
}
teLog("finished " + mytitle);
}
// ---------------------------------------------------------------------------------------------------------------- include
// used inside <?= ?> and <? ?>
function include(name, data, _include, _include2) {
teLog("include(%s) running", name);
// teLog("include(%s) _include=%s, _include2=%s", name, _include, _include2);
var origInclude = data._include;
var origInclude2 = data._include2;
var filtered = data._availableTemplates.filter(function(t){return t.name == name});
for (var i=0; i<filtered.length; i++) {
teLog(["include(): found %s: %s", filtered[i].name, filtered[i].url]);
}
if (filtered.length > 1) {
teLog(["include(): found multiple (%s) %s templates; picking the last one.", filtered.length, name],5);
filtered = [filtered.pop()];
teLog(["include(): template url = %s", filtered[0].url],5);
}
if (filtered.length == 1) {
var template = filtered[0];
var childTemplate = obtainTemplate_(template.url, template.nocache);
childTemplate.data = data;
childTemplate.data._include = _include || {};
childTemplate.data._include2 = _include2 || {};
var filledHTML = childTemplate.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).getContent();
// teLog("include(%s) complete", name);
data._include = origInclude;
data._include2 = origInclude2;
return filledHTML;
}
teLog("include(): unable to find template named %s", name);
return;
}
// i suspect these aren't even used any more.
// todo: rethink all this to work with both controller and native sheet mode. now that we save the sheetid into the uniq'ed
function templateActiveSheetChanged_(sheet) {
var templateActiveSheetId = PropertiesService.getDocumentProperties().getProperty("legalese.templateActiveSheetId");
if (templateActiveSheetId == undefined) { return false }
if ( sheet == undefined) { return false }
teLog("templateActiveSheetChanged: comparing %s with %s, which is %s",
templateActiveSheetId, sheet.getSheetId(),
templateActiveSheetId == sheet.getSheetId()
);
return (templateActiveSheetId != sheet.getSheetId());
}
function muteTemplateActiveSheetWarnings_(setter) {
if (setter == undefined) { // getter
var myprop = PropertiesService.getDocumentProperties().getProperty("legalese.muteTemplateActiveSheetWarnings");
if (myprop != undefined) {
return JSON.parse(myprop);
}
else {
return false;
}
}
else {
PropertiesService.getDocumentProperties().setProperty("legalese.muteTemplateActiveSheetWarnings", JSON.stringify(setter));
}
}