|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# |
| 3 | +# Copyright ©2018-2019 Google LLC |
| 4 | +# |
| 5 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | +# you may not use this file except in compliance with the License. |
| 7 | +# You may obtain a copy of the License at apache.org/licenses/LICENSE-2.0. |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +""" |
| 16 | +docs-mail-merge.py (2.x or 3.x) |
| 17 | +
|
| 18 | +Google Docs (REST) API mail-merge sample app |
| 19 | +""" |
| 20 | +from __future__ import print_function |
| 21 | +import time |
| 22 | + |
| 23 | +from googleapiclient import discovery |
| 24 | +from httplib2 import Http |
| 25 | +from oauth2client import file, client, tools |
| 26 | + |
| 27 | +# Fill-in IDs of your Docs template & any Sheets data source |
| 28 | +DOCS_FILE_ID = 'YOUR_TMPL_DOC_FILE_ID' |
| 29 | +SHEETS_FILE_ID = 'YOUR_SHEET_DATA_FILE_ID' |
| 30 | + |
| 31 | +# authorization constants |
| 32 | +CLIENT_ID_FILE = 'credentials.json' |
| 33 | +TOKEN_STORE_FILE = 'token.json' |
| 34 | +SCOPES = ( # iterable or space-delimited string |
| 35 | + 'https://www.googleapis.com/auth/drive', |
| 36 | + 'https://www.googleapis.com/auth/documents', |
| 37 | + 'https://www.googleapis.com/auth/spreadsheets.readonly', |
| 38 | +) |
| 39 | + |
| 40 | +# application constants |
| 41 | +SOURCES = ('text', 'sheets') |
| 42 | +SOURCE = 'text' # Choose one of the data SOURCES |
| 43 | +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 | +] |
| 48 | + |
| 49 | +def get_http_client(): |
| 50 | + """Uses project credentials in CLIENT_ID_FILE along with requested OAuth2 |
| 51 | + scopes for authorization, and caches API tokens in TOKEN_STORE_FILE. |
| 52 | + """ |
| 53 | + store = file.Storage(TOKEN_STORE_FILE) |
| 54 | + creds = store.get() |
| 55 | + if not creds or creds.invalid: |
| 56 | + flow = client.flow_from_clientsecrets(CLIENT_ID_FILE, SCOPES) |
| 57 | + creds = tools.run_flow(flow, store) |
| 58 | + return creds.authorize(Http()) |
| 59 | + |
| 60 | +# service endpoints to Google APIs |
| 61 | +HTTP = get_http_client() |
| 62 | +DRIVE = discovery.build('drive', 'v3', http=HTTP) |
| 63 | +DOCS = discovery.build('docs', 'v1', http=HTTP) |
| 64 | +SHEETS = discovery.build('sheets', 'v4', http=HTTP) |
| 65 | + |
| 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 | + |
| 80 | +def get_data(source): |
| 81 | + """Gets mail merge data from chosen data source. |
| 82 | + """ |
| 83 | + if source not in {'sheets', 'text'}: |
| 84 | + raise ValueError('ERROR: unsupported source %r; choose from %r' % ( |
| 85 | + source, SOURCES)) |
| 86 | + func = SAFE_DISPATCH[source] |
| 87 | + return dict(zip(COLUMNS, func())) |
| 88 | + |
| 89 | +def _get_text_data(): |
| 90 | + """(private) Returns plain text data; can alter to read from CSV file. |
| 91 | + """ |
| 92 | + return TEXT_SOURCE_DATA |
| 93 | + |
| 94 | +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). |
| 99 | + """ |
| 100 | + return service.spreadsheets().values().get(spreadsheetId=SHEETS_FILE_ID, |
| 101 | + range='Sheet1!2:2').execute().get('values')[0] # 2nd row only |
| 102 | + |
| 103 | +# data source dispatch table [better alternative to using eval()] |
| 104 | +SAFE_DISPATCH = {k: globals().get('_get_%s_data' % k) for k in SOURCES} |
| 105 | + |
| 106 | +def _copy_template(tmpl_id, source, service): |
| 107 | + """(private) Copies letter template document using Drive API then |
| 108 | + returns file ID of (new) copy. |
| 109 | + """ |
| 110 | + body = {'name': 'Merged form letter (%s)' % source} |
| 111 | + return service.files().copy(body=body, fileId=tmpl_id, |
| 112 | + fields='id').execute().get('id') |
| 113 | + |
| 114 | +def merge_template(tmpl_id, source, service): |
| 115 | + """Copies template document and merges data into newly-minted copy then |
| 116 | + returns its file ID. |
| 117 | + """ |
| 118 | + # copy template and set context data struct for merging template values |
| 119 | + copy_id = _copy_template(tmpl_id, source, service) |
| 120 | + context = merge.iteritems() if hasattr({}, 'iteritems') else merge.items() |
| 121 | + |
| 122 | + # "search & replace" API requests for mail merge substitutions |
| 123 | + reqs = [{'replaceAllText': { |
| 124 | + 'containsText': { |
| 125 | + 'text': '{{%s}}' % key.upper(), # {{VARS}} are uppercase |
| 126 | + 'matchCase': True, |
| 127 | + }, |
| 128 | + 'replaceText': value, |
| 129 | + }} for key, value in context] |
| 130 | + |
| 131 | + # send requests to Docs API to do actual merge |
| 132 | + DOCS.documents().batchUpdate(body={'requests': reqs}, |
| 133 | + documentId=copy_id, fields='').execute() |
| 134 | + return copy_id |
| 135 | + |
| 136 | + |
| 137 | +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) |
0 commit comments