Skip to content

Commit c17d2c9

Browse files
wescpyasrivas
authored andcommitted
add support for >1 row of data (googleworkspace#95)
* add Docs API Mail Merge Python sample * pylint & eval fixes * lint fixes * final(?) lint fixes * PR fixes and code cleanup * more cleanup & fixed brokenness from last commit * unit test fixes * README missing one more reference to Sheets API * oops * added Docs API video to README * updated app to spuport >1 row of data; README cleanup * update unit tests to support new functionality; README cleanup too * README tweaks * pylint fixes
1 parent e864f3b commit c17d2c9

3 files changed

Lines changed: 91 additions & 60 deletions

File tree

docs/mail-merge/README.md

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,15 @@
33
## Prerequisites
44

55
- Access to the internet and a web browser
6-
- A Google account (G Suite accounts may require administrator approval)
6+
- A Google or Gmail account (G Suite accounts may require administrator approval)
77
- Basic Python skills (2.x or 3.x)
88
- Google APIs project with the [Drive](https://developers.google.com/drive/), [Sheets](https://developers.google.com/sheets/), and [Docs](https://developers.google.com/docs/) APIs enabled
99

1010
## Description
1111

12-
Before getting started, create a new project (or reuse an existing one) at <https://console.developers.google.com> with Google Drive, Sheets, and Docs APIs enabled. (See the videos listed below if you're new to Google APIs.) Then install the Google APIs Client Library for Python if you haven't already: `pip{,3} install -U google-api-python-client`
12+
Create a new project (or reuse an existing one) from [your developer console](https://console.developers.google.com) with the three Google APIs above enabled. (See the videos listed at the bottom if you're new to Google APIs.) Ensure you have the Google APIs Client Library for Python installed; the fastest way of doing this is to use `pip install -U google-api-python-client` (or with `pip3` if you have both Python 2 and 3 on your computer).
1313

14-
This sample app requires you to [create a new Google Docs file](https://docs.google.com). Choose the template you wish to use, but we suggest using Letter/Spearmint to keep things simple. Replace the contact information in the Doc with template variables that we can merge with desired data.
15-
16-
Here is a [sample letter template](https://drive.google.com/open?id=1Rr7eKm3tvUgRKRoOYVSMF69IVAHIOPS72-l0CzsPmfM) to get an idea of what we mean: ![sample letter template](https://user-images.githubusercontent.com/1102504/44741564-51ea2480-aab2-11e8-871c-a836626b2a0d.png "sample letter template")
17-
18-
In the document, the variable names used by the sample app are:
14+
This sample app requires you to [create a new Google Docs file](https://docs.google.com) to serve as the letter template with variable placeholders. Choose the template you wish to use, but we recommend *Letter/Spearmint* to keep things simple. Replace the contact information in the Doc with template variables that we can merge with desired data. Here are the variables we're using:
1915

2016
### General
2117

@@ -36,15 +32,27 @@ In the document, the variable names used by the sample app are:
3632
* `{{TO_COMPANY}}` — recipient's organization
3733
* `{{TO_ADDRESS}}` — recipient's address
3834

39-
After you've templatized the Google Doc, be sure to grab its file ID — in your browser, look at the address bar, and extract the long alphanumeric string that makes up the Drive file ID from the URL: `https://docs.google.com/document/d/<DRIVE_FILE_ID>/edit`.
35+
Here's one [sample letter template](https://drive.google.com/open?id=1Xycxuuv7OhEQUuzbt_Mw0TPMq02MseSD1vZdBJ3nLjk) to get an idea of what we mean:
36+
37+
![sample letter template](https://user-images.githubusercontent.com/1102504/54470461-6b5c7080-4765-11e9-9912-01b44c734118.png "sample letter template")
4038

41-
Replace `YOUR_TMPL_DOC_FILE_ID` in the `docs_mail_merge.py` source file with this file ID as the string value (in quotes) for the `DOCS_FILE_ID` variable. Run the sample app, accept the OAuth2 permissions requested, and when the script has completed, you should have a new mail-merged Google Doc named `Merged form letter` in your Google Drive folder!
39+
Once you're done, grab your template's file ID — go to the URL in the address bar of your browser and extract the long alphanumeric string representing the Drive file ID: `https://docs.google.com/document/d/DRIVE_FILE_ID/edit`. Replace `YOUR_TMPL_DOC_FILE_ID` with this ID as the `DOCS_FILE_ID` string variable (keep in quotes).
4240

4341
## Data source
4442

45-
The application currently supports two different sources of data, plain text and Google Sheets. By default, the sample uses plain text via the `TARGET_TEXT` variable. A better option is to use a Google Sheet. Enable the API for your project in the developers console, and change the `source` variable at the bottom to `'sheets'`. Be sure you create a Sheet structured like the one below. Here is one [example Sheet](https://docs.google.com/spreadsheets/d/1vaoqPYGL1cJvkogV36nu3AKQ5rUacXj9TV-zqTvXuMU/edit) you can model yours with. Ensure you then set the `SHEETS_FILE_ID` variable to the file ID of your Google Sheet.
43+
Next, decide on the data source. This sample app currently supports plain text and Google Sheets. By default, the `TARGET_TEXT` variable is set to `'text'` but change to `'sheets'` once you have a Google Sheet with the data. The code supports a Sheet structured like this:
44+
45+
![sample Sheets data source](https://user-images.githubusercontent.com/1102504/54470464-731c1500-4765-11e9-9110-986519502cdf.png "sample Sheets data source")
46+
47+
Here is [one example Sheet](https://drive.google.com/open?id=18yqXLEMx6l__VAIN-Zo52pL18F3rXn0_-K6gZ-vwPcc) you can model yours with. Ensure you then set the `SHEETS_FILE_ID` variable to its file ID (get it the same way as your Google Doc).
48+
49+
## Application code
4650

47-
![sample Sheets data source](https://user-images.githubusercontent.com/1102504/54064578-62e6c180-41ca-11e9-86f6-9d147ac17200.png "sample Sheets data source")
51+
The application script (`docs_mail_merge.py`) supplies a data structure (`merge`) with the sender info, date, body of the letter, and placeholders for values that will be replaced by data from the selected data source. The data is fetched and merged into form letters in a loop, displaying links to each of the merged letters. One function is used to fetch the data, supported by private function "shims" for each data source. The other pair of functions: a private function to copy the template, and one more for merging the form data into a copy of the template.
52+
53+
If you run the sample app as written (with all real variables and data documents) and accept the OAuth2 permissions. You'll see one line of output per letter merged. Those letters, named `Merged form letter`, will also be found in your Google Drive. If you run the app with our data featured here, your merged letter should look like this:
54+
55+
![sample merged letter](https://user-images.githubusercontent.com/1102504/54470465-731c1500-4765-11e9-8a0a-93a3bb445d6e.png "sample merged letter")
4856

4957
## Testing
5058

@@ -53,20 +61,29 @@ The unit-test script is `docs_mail_merge_test.py`; see the file for a list of th
5361
## Reference
5462

5563
- Google Drive API
56-
- [API documentation](https://developers.google.com/drive)
57-
- [Support channels](https://developers.google.com/drive/api/v3/support)
64+
- [API documentation](https://developers.google.com/drive/)
65+
- [Support channels](https://developers.google.com/drive/api/v3/support/)
5866
- Google Docs API
59-
- [API documentation](https://developers.google.com/docs)
60-
- [Python quickstart](https://developers.google.com/docs/api/quickstart/python)
61-
- [Support channels](https://developers.google.com/docs/api/support)
67+
- [API documentation](https://developers.google.com/docs/)
68+
- [Support channels](https://developers.google.com/docs/api/support/)
6269
- Google Sheets API
63-
- [API documentation](https://developers.google.com/sheets)
64-
- [Support channels](https://developers.google.com/sheets/api/support)
65-
- [Google APIs client libraries](https://developers.google.com/api-client-library)
66-
- [G Suite developer overview &amp; video](https://developers.google.com/gsuite)
67-
- [G Suite (REST) APIs intro codelab](https://g.co/codelabs/gsuite-apis-intro) (~half-hour)
68-
- Introductory API videos
70+
- [API documentation](https://developers.google.com/sheets/)
71+
- [Support channels](https://developers.google.com/sheets/api/support/)
72+
- G Suite APIs
73+
- [G Suite developer overview &amp; video](https://developers.google.com/gsuite/)
74+
- [G Suite (REST) APIs intro codelab](https://g.co/codelabs/gsuite-apis-intro/) (30 mins)
75+
- [Google APIs client libraries](https://developers.google.com/api-client-library/)
76+
- Introductory videos
6977
- [New Google APIs project setup](https://goo.gl/RbyTFD) (6:54)
70-
- [Common Python boilerplate code review](https://goo.gl/KMfbeK) (3:48)
78+
- [Common OAuth2 boilerplate code (Python)](https://goo.gl/KMfbeK) (3:48)
7179
- [REST APIs intro (Drive API)](https://goo.gl/ZIgf8k) (6:20)
7280
- [Introducing the Docs API](https://youtu.be/jeU-tWKeb6g) (2:57)
81+
82+
## Enhancements
83+
84+
Here are some recommendations on how you can enhance this application:
85+
86+
1. Adapt this sample for your mail merge use case
87+
1. Support exporting merged letters as PDF (HINT: Drive API)
88+
1. Support importing data from other data sources, i.e., [Cloud SQL](https://cloud.google.com/sql/), Salesforce, etc.
89+
1. Augment the plain text data support to include processing data from CSV files

docs/mail-merge/docs_mail_merge.py

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
# limitations under the License.
1414

1515
"""
16-
docs-mail-merge.py (2.x or 3.x)
16+
docs-mail-merge.py (Python 2.x or 3.x)
1717
1818
Google Docs (REST) API mail-merge sample app
1919
"""
20+
# [START mail_merge_python]
2021
from __future__ import print_function
2122
import time
2223

@@ -41,10 +42,12 @@
4142
SOURCES = ('text', 'sheets')
4243
SOURCE = 'text' # Choose one of the data SOURCES
4344
COLUMNS = ['to_name', 'to_title', 'to_company', 'to_address']
44-
TEXT_SOURCE_DATA = [
45-
'Ms. Lara Brown', 'Googler', 'Google NYC',
46-
'111 8th Ave\nNew York, NY 10011-5201'
47-
]
45+
TEXT_SOURCE_DATA = (
46+
('Ms. Lara Brown', 'Googler', 'Google NYC', '111 8th Ave\n'
47+
'New York, NY 10011-5201'),
48+
('Mr. Jeff Erson', 'Googler', 'Google NYC', '76 9th Ave\n'
49+
'New York, NY 10011-4962'),
50+
)
4851

4952
def get_http_client():
5053
"""Uses project credentials in CLIENT_ID_FILE along with requested OAuth2
@@ -63,44 +66,28 @@ def get_http_client():
6366
DOCS = discovery.build('docs', 'v1', http=HTTP)
6467
SHEETS = discovery.build('sheets', 'v4', http=HTTP)
6568

66-
# fill-in your data to merge into document template variables
67-
merge = {
68-
'my_name': 'Mr. Jeff Erson',
69-
'my_address': '76 9th Ave\nNew York, NY 10011-4962',
70-
'my_email': 'http://google.com',
71-
'my_phone': '+1-212-565-0000',
72-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
73-
'date': time.ctime(),
74-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
75-
'body': 'Google, headquartered in Mountain View, unveiled the new Android '
76-
'phone at the Consumer Electronics Show. CEO Sundar Pichai said '
77-
'in his keynote that users love their new Android phones.'
78-
}
79-
8069
def get_data(source):
8170
"""Gets mail merge data from chosen data source.
8271
"""
8372
if source not in {'sheets', 'text'}:
8473
raise ValueError('ERROR: unsupported source %r; choose from %r' % (
8574
source, SOURCES))
86-
func = SAFE_DISPATCH[source]
87-
return dict(zip(COLUMNS, func()))
75+
return SAFE_DISPATCH[source]()
8876

8977
def _get_text_data():
9078
"""(private) Returns plain text data; can alter to read from CSV file.
9179
"""
9280
return TEXT_SOURCE_DATA
9381

9482
def _get_sheets_data(service=SHEETS):
95-
"""(private) Returns data from Google Sheets source. NOTE: this sample
96-
code gets all cells in 'Sheet1', the first default Sheet in a
97-
spreadsheet. Use any desired data range (in standard A1 notation).
98-
This sample app is coded to return only the 2nd row (the data).
83+
"""(private) Returns data from Google Sheets source. It gets all rows of
84+
'Sheet1' (the default Sheet in a new spreadsheet), but drops the first
85+
(header) row. Use any desired data range (in standard A1 notation).
9986
"""
10087
return service.spreadsheets().values().get(spreadsheetId=SHEETS_FILE_ID,
101-
range='Sheet1!2:2').execute().get('values')[0] # 2nd row only
88+
range='Sheet1').execute().get('values')[1:] # skip header row
10289

103-
# data source dispatch table [better alternative to using eval()]
90+
# data source dispatch table [better alternative vs. eval()]
10491
SAFE_DISPATCH = {k: globals().get('_get_%s_data' % k) for k in SOURCES}
10592

10693
def _copy_template(tmpl_id, source, service):
@@ -135,7 +122,32 @@ def merge_template(tmpl_id, source, service):
135122

136123

137124
if __name__ == '__main__':
138-
if SOURCE in SOURCES:
139-
merge.update(get_data(SOURCE))
140-
fid = merge_template(DOCS_FILE_ID, SOURCE, DRIVE)
141-
print('Merged letter: docs.google.com/document/d/%s/edit' % fid)
125+
# fill-in your data to merge into document template variables
126+
merge = {
127+
# sender data
128+
'my_name': 'Ayme A. Coder',
129+
'my_address': '1600 Amphitheatre Pkwy\n'
130+
'Mountain View, CA 94043-1351',
131+
'my_email': 'http://google.com',
132+
'my_phone': '+1-650-253-0000',
133+
# - - - - - - - - - - - - - - - - - - - - - - - - - -
134+
# recipient data (supplied by 'text' or 'sheets' data source)
135+
'to_name': None,
136+
'to_title': None,
137+
'to_company': None,
138+
'to_address': None,
139+
# - - - - - - - - - - - - - - - - - - - - - - - - - -
140+
'date': time.strftime('%Y %B %d'),
141+
# - - - - - - - - - - - - - - - - - - - - - - - - - -
142+
'body': 'Google, headquartered in Mountain View, unveiled the new '
143+
'Android phone at the Consumer Electronics Show. CEO Sundar '
144+
'Pichai said in his keynote that users love their new phones.'
145+
}
146+
147+
# get row data, then loop through & process each form letter
148+
data = get_data(SOURCE) # get data from data source
149+
for i, row in enumerate(data):
150+
merge.update(dict(zip(COLUMNS, row)))
151+
print('Merged letter %d: docs.google.com/document/d/%s/edit' % (
152+
i+1, merge_template(DOCS_FILE_ID, SOURCE, DRIVE)))
153+
# [END mail_merge_python]

docs/mail-merge/docs_mail_merge_test.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Copyright 2018-2019 Google LLC
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright ©2018-2019 Google LLC
24
#
35
# Licensed under the Apache License, Version 2.0 (the "License");
46
# you may not use this file except in compliance with the License.
@@ -24,7 +26,7 @@
2426
import unittest
2527

2628
from googleapiclient import discovery
27-
from docs_mail_merge6 import (CLIENT_ID_FILE, get_data, get_http_client,
29+
from docs_mail_merge import (CLIENT_ID_FILE, get_data, get_http_client,
2830
_copy_template)
2931

3032
class TestDocsMailMerge(unittest.TestCase):
@@ -73,18 +75,18 @@ def create_doc_test():
7375
def copy_doc_test():
7476
'Tests whether project can copy and delete a Google Docs file.'
7577
DRIVE = discovery.build('drive', 'v3', http=get_http_client())
76-
DOCS_FILE_ID = '1Rr7eKm3tvUgRKRoOYVSMF69IVAHIOPS72-l0CzsPmfM'
77-
doc_id = _copy_template(DOCS_FILE_ID, 'text')
78+
DOCS_FILE_ID = '1Xycxuuv7OhEQUuzbt_Mw0TPMq02MseSD1vZdBJ3nLjk'
79+
doc_id = _copy_template(DOCS_FILE_ID, 'text', DRIVE)
7880
DRIVE.files().delete(fileId=doc_id, fields='').execute()
7981
return True
8082

8183
def get_text_data_test():
8284
'Tests reading plain text data.'
83-
return get_data()
85+
return get_data('text')
8486

8587
def get_sheets_data_test():
8688
'Tests reading Google Sheets data.'
87-
return get_data(source='sheets')
89+
return get_data('sheets')
8890

8991
if __name__ == '__main__':
9092
unittest.main()

0 commit comments

Comments
 (0)