Skip to content

Commit 91ca61b

Browse files
Add Connected fields code example (#167)
* add connected fields example * update packages * update comments * support multiple tabs * change tab location & filter tabs * support more tabs on a document page
1 parent 7add73e commit 91ca61b

File tree

10 files changed

+389
-15
lines changed

10 files changed

+389
-15
lines changed

app/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .connect import views as connect_views
1515
from .webforms import views as webforms_views
1616
from .notary import views as notary_views
17+
from .connected_fields import views as connected_fields_views
1718
from .views import core
1819

1920
session_path = "/tmp/python_recipe_sessions"
@@ -120,6 +121,8 @@
120121

121122
app.register_blueprint(notary_views.neg004)
122123

124+
app.register_blueprint(connected_fields_views.feg001)
125+
123126
if "DYNO" in os.environ: # On Heroku?
124127
import logging
125128

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import base64
2+
import requests
3+
from os import path
4+
5+
from docusign_esign import EnvelopesApi, Text, Document, Signer, EnvelopeDefinition, SignHere, Tabs, \
6+
Recipients
7+
from flask import session, request
8+
9+
from ...consts import demo_docs_path, pattern
10+
from ...docusign import create_api_client
11+
from ...ds_config import DS_CONFIG
12+
13+
14+
class Eg001SetConnectedFieldsController:
15+
@staticmethod
16+
def get_args():
17+
"""Get request and session arguments"""
18+
# Parse request arguments
19+
signer_email = pattern.sub("", request.form.get("signer_email"))
20+
signer_name = pattern.sub("", request.form.get("signer_name"))
21+
selected_app_id = pattern.sub("", request.form.get("app_id"))
22+
envelope_args = {
23+
"signer_email": signer_email,
24+
"signer_name": signer_name,
25+
}
26+
args = {
27+
"account_id": session["ds_account_id"],
28+
"base_path": session["ds_base_path"],
29+
"access_token": session["ds_access_token"],
30+
"selected_app_id": selected_app_id,
31+
"envelope_args": envelope_args
32+
}
33+
return args
34+
35+
@staticmethod
36+
def get_tab_groups(args):
37+
"""
38+
1. Get the list of tab groups
39+
2. Filter by action contract and tab label
40+
3. Create a list of unique apps
41+
"""
42+
43+
#ds-snippet-start:ConnectedFields1Step2
44+
headers = {
45+
"Authorization": "Bearer " + args['access_token'],
46+
"Accept": "application/json",
47+
"Content-Type": "application/json"
48+
}
49+
#ds-snippet-end:ConnectedFields1Step2
50+
51+
#ds-snippet-start:ConnectedFields1Step3
52+
url = f"{args['base_path']}/v1/accounts/{args['account_id']}/connected-fields/tab-groups"
53+
54+
response = requests.get(url, headers=headers)
55+
response_data = response.json()
56+
57+
filtered_apps = list(
58+
app for app in response_data
59+
if any(
60+
("extensionData" in tab and "actionContract" in tab["extensionData"] and "Verify" in tab["extensionData"]["actionContract"]) or
61+
("tabLabel" in tab and "connecteddata" in tab["tabLabel"])
62+
for tab in app.get("tabs", [])
63+
)
64+
)
65+
66+
unique_apps = list({app['appId']: app for app in filtered_apps}.values())
67+
#ds-snippet-end:ConnectedFields1Step3
68+
69+
return unique_apps
70+
71+
@staticmethod
72+
#ds-snippet-start:ConnectedFields1Step4
73+
def extract_verification_data(selected_app_id, tab):
74+
extension_data = tab["extensionData"]
75+
76+
return {
77+
"app_id": selected_app_id,
78+
"extension_group_id": extension_data["extensionGroupId"] if "extensionGroupId" in extension_data else "",
79+
"publisher_name": extension_data["publisherName"] if "publisherName" in extension_data else "",
80+
"application_name": extension_data["applicationName"] if "applicationName" in extension_data else "",
81+
"action_name": extension_data["actionName"] if "actionName" in extension_data else "",
82+
"action_input_key": extension_data["actionInputKey"] if "actionInputKey" in extension_data else "",
83+
"action_contract": extension_data["actionContract"] if "actionContract" in extension_data else "",
84+
"extension_name": extension_data["extensionName"] if "extensionName" in extension_data else "",
85+
"extension_contract": extension_data["extensionContract"] if "extensionContract" in extension_data else "",
86+
"required_for_extension": extension_data["requiredForExtension"] if "requiredForExtension" in extension_data else "",
87+
"tab_label": tab["tabLabel"],
88+
"connection_key": (
89+
extension_data["connectionInstances"][0]["connectionKey"]
90+
if "connectionInstances" in extension_data and extension_data["connectionInstances"]
91+
else ""
92+
),
93+
"connection_value": (
94+
extension_data["connectionInstances"][0]["connectionValue"]
95+
if "connectionInstances" in extension_data and extension_data["connectionInstances"]
96+
else ""
97+
),
98+
}
99+
#ds-snippet-end:ConnectedFields1Step4
100+
101+
@classmethod
102+
def send_envelope(cls, args, app):
103+
"""
104+
1. Create the envelope request object
105+
2. Send the envelope
106+
3. Obtain the envelope_id
107+
"""
108+
#ds-snippet-start:ConnectedFields1Step6
109+
envelope_args = args["envelope_args"]
110+
# Create the envelope request object
111+
envelope_definition = cls.make_envelope(envelope_args, app)
112+
113+
# Call Envelopes::create API method
114+
# Exceptions will be caught by the calling function
115+
api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"])
116+
117+
envelope_api = EnvelopesApi(api_client)
118+
results = envelope_api.create_envelope(account_id=args["account_id"], envelope_definition=envelope_definition)
119+
120+
envelope_id = results.envelope_id
121+
#ds-snippet-end:ConnectedFields1Step6
122+
123+
return {"envelope_id": envelope_id}
124+
125+
@classmethod
126+
#ds-snippet-start:ConnectedFields1Step5
127+
def make_envelope(cls, args, app):
128+
"""
129+
Creates envelope
130+
args -- parameters for the envelope:
131+
signer_email, signer_name
132+
returns an envelope definition
133+
"""
134+
135+
# document 1 (pdf) has tag /sn1/
136+
#
137+
# The envelope has one recipient.
138+
# recipient 1 - signer
139+
with open(path.join(demo_docs_path, DS_CONFIG["doc_pdf"]), "rb") as file:
140+
content_bytes = file.read()
141+
base64_file_content = base64.b64encode(content_bytes).decode("ascii")
142+
143+
# Create the document model
144+
document = Document( # create the DocuSign document object
145+
document_base64=base64_file_content,
146+
name="Example document", # can be different from actual file name
147+
file_extension="pdf", # many different document types are accepted
148+
document_id=1 # a label used to reference the doc
149+
)
150+
151+
# Create the signer recipient model
152+
signer = Signer(
153+
# The signer
154+
email=args["signer_email"],
155+
name=args["signer_name"],
156+
recipient_id="1",
157+
routing_order="1"
158+
)
159+
160+
# Create a sign_here tab (field on the document)
161+
sign_here = SignHere(
162+
anchor_string="/sn1/",
163+
anchor_units="pixels",
164+
anchor_y_offset="10",
165+
anchor_x_offset="20"
166+
)
167+
168+
# Create text tabs (field on the document)
169+
text_tabs = []
170+
for tab in (t for t in app["tabs"] if "SuggestionInput" not in t["tabLabel"]):
171+
verification_data = cls.extract_verification_data(app["appId"], tab)
172+
extension_data = cls.get_extension_data(verification_data)
173+
174+
text_tab = {
175+
"requireInitialOnSharedChange": False,
176+
"requireAll": False,
177+
"name": verification_data["application_name"],
178+
"required": True,
179+
"locked": False,
180+
"disableAutoSize": False,
181+
"maxLength": 4000,
182+
"tabLabel": verification_data["tab_label"],
183+
"font": "lucidaconsole",
184+
"fontColor": "black",
185+
"fontSize": "size9",
186+
"documentId": "1",
187+
"recipientId": "1",
188+
"pageNumber": "1",
189+
"xPosition": f"{70 + 100 * int(len(text_tabs) / 10)}",
190+
"yPosition": f"{560 + 20 * (len(text_tabs) % 10)}",
191+
"width": "84",
192+
"height": "22",
193+
"templateRequired": False,
194+
"tabType": "text",
195+
"tooltip": verification_data["action_input_key"],
196+
"extensionData": extension_data
197+
}
198+
text_tabs.append(text_tab)
199+
200+
# Add the tabs model (including the sign_here and text tabs) to the signer
201+
# The Tabs object wants arrays of the different field/tab types
202+
signer.tabs = Tabs(sign_here_tabs=[sign_here], text_tabs=text_tabs)
203+
204+
# Next, create the top level envelope definition and populate it.
205+
envelope_definition = EnvelopeDefinition(
206+
email_subject="Please sign this document",
207+
documents=[document],
208+
# The Recipients object wants arrays for each recipient type
209+
recipients=Recipients(signers=[signer]),
210+
status="sent" # requests that the envelope be created and sent.
211+
)
212+
213+
return envelope_definition
214+
215+
def get_extension_data(verification_data):
216+
return {
217+
"extensionGroupId": verification_data["extension_group_id"],
218+
"publisherName": verification_data["publisher_name"],
219+
"applicationId": verification_data["app_id"],
220+
"applicationName": verification_data["application_name"],
221+
"actionName": verification_data["action_name"],
222+
"actionContract": verification_data["action_contract"],
223+
"extensionName": verification_data["extension_name"],
224+
"extensionContract": verification_data["extension_contract"],
225+
"requiredForExtension": verification_data["required_for_extension"],
226+
"actionInputKey": verification_data["action_input_key"],
227+
"extensionPolicy": 'MustVerifyToSign',
228+
"connectionInstances": [
229+
{
230+
"connectionKey": verification_data["connection_key"],
231+
"connectionValue": verification_data["connection_value"],
232+
}
233+
]
234+
}
235+
#ds-snippet-end:ConnectedFields1Step5
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .eg001_set_connected_fields import feg001
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Example 001: Set connected fields"""
2+
3+
from docusign_esign.client.api_exception import ApiException
4+
from flask import render_template, redirect, Blueprint, session
5+
6+
from ..examples.eg001_set_connected_fields import Eg001SetConnectedFieldsController
7+
from ...docusign import authenticate, ensure_manifest, get_example_by_number
8+
from ...ds_config import DS_CONFIG
9+
from ...error_handlers import process_error
10+
from ...consts import API_TYPE
11+
12+
example_number = 1
13+
api = API_TYPE["CONNECTED_FIELDS"]
14+
eg = f"feg00{example_number}" # reference (and url) for this example
15+
feg001 = Blueprint(eg, __name__)
16+
17+
18+
@feg001.route(f"/{eg}", methods=["POST"])
19+
@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"])
20+
@authenticate(eg=eg, api=api)
21+
def set_connected_fields():
22+
"""
23+
1. Get required arguments
24+
2. Call the worker method
25+
"""
26+
try:
27+
# 1. Get required arguments
28+
args = Eg001SetConnectedFieldsController.get_args()
29+
# 2. Call the worker method
30+
selected_app = next((app for app in session["apps"] if app["appId"] == args["selected_app_id"]), None)
31+
results = Eg001SetConnectedFieldsController.send_envelope(args, selected_app)
32+
except ApiException as err:
33+
return process_error(err)
34+
35+
session["envelope_id"] = results["envelope_id"]
36+
37+
example = get_example_by_number(session["manifest"], example_number, api)
38+
return render_template(
39+
"example_done.html",
40+
title=example["ExampleName"],
41+
message=example["ResultsPageText"].format(results['envelope_id'])
42+
)
43+
44+
45+
@feg001.route(f"/{eg}", methods=["GET"])
46+
@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"])
47+
@authenticate(eg=eg, api=api)
48+
def get_view():
49+
"""responds with the form for the example"""
50+
example = get_example_by_number(session["manifest"], example_number, api)
51+
52+
args = {
53+
"account_id": session["ds_account_id"],
54+
"base_path": "https://api-d.docusign.com",
55+
"access_token": session["ds_access_token"],
56+
}
57+
apps = Eg001SetConnectedFieldsController.get_tab_groups(args)
58+
59+
if not apps or len(apps) == 0:
60+
additional_page_data = next(
61+
(p for p in example["AdditionalPage"] if p["Name"] == "no_verification_app"),
62+
None
63+
)
64+
65+
return render_template(
66+
"example_done.html",
67+
title=example["ExampleName"],
68+
message=additional_page_data["ResultsPageText"]
69+
)
70+
71+
session["apps"] = apps
72+
return render_template(
73+
"connected_fields/eg001_set_connected_fields.html",
74+
title=example["ExampleName"],
75+
example=example,
76+
apps=apps,
77+
source_file="eg001_set_connected_fields.py",
78+
source_url=DS_CONFIG["github_example_url"] + "eg001_set_connected_fields.py",
79+
documentation=DS_CONFIG["documentation"] + eg,
80+
show_doc=DS_CONFIG["documentation"],
81+
signer_name=DS_CONFIG["signer_name"],
82+
signer_email=DS_CONFIG["signer_email"]
83+
)

app/consts.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,6 @@
117117
"ADMIN": "Admin",
118118
"CONNECT": "Connect",
119119
"WEBFORMS": "WebForms",
120-
"NOTARY": "Notary"
120+
"NOTARY": "Notary",
121+
"CONNECTED_FIELDS": "ConnectedFields"
121122
}

app/docusign/ds_client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
"signature", "organization_read", "notary_read", "notary_write"
4747
]
4848

49+
CONNECTED_FIELDS = [
50+
"signature", "adm_store_unified_repo_read"
51+
]
52+
4953

5054
class DSClient:
5155
ds_app = None
@@ -77,6 +81,8 @@ def _auth_code_grant(cls, api):
7781
use_scopes.extend(WEBFORMS_SCOPES)
7882
elif api == "Notary":
7983
use_scopes.extend(NOTARY_SCOPES)
84+
elif api == "ConnectedFields":
85+
use_scopes.extend(CONNECTED_FIELDS)
8086
else:
8187
use_scopes.extend(SCOPES)
8288
# remove duplicate scopes
@@ -115,6 +121,8 @@ def _pkce_auth(cls, api):
115121
use_scopes.extend(WEBFORMS_SCOPES)
116122
elif api == "Notary":
117123
use_scopes.extend(NOTARY_SCOPES)
124+
elif api == "ConnectedFields":
125+
use_scopes.extend(CONNECTED_FIELDS)
118126
else:
119127
use_scopes.extend(SCOPES)
120128
# remove duplicate scopes

app/static/assets/search.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const DS_SEARCH = (function () {
77
ADMIN: "admin",
88
CONNECT: "connect",
99
WEBFORMS: "webforms",
10-
NOTARY: "notary"
10+
NOTARY: "notary",
11+
CONNECTED_FIELDS: "connectedfields",
1112
}
1213

1314
const processJSONData = function () {
@@ -148,6 +149,8 @@ const DS_SEARCH = (function () {
148149
return "weg";
149150
case API_TYPES.NOTARY:
150151
return "neg";
152+
case API_TYPES.CONNECTED_FIELDS:
153+
return "feg";
151154
}
152155
}
153156

0 commit comments

Comments
 (0)