diff --git a/.gitignore b/.gitignore index 73a0dc0..101b3e4 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,6 @@ private.key # Current flask session flask_session/ + +# Workflow ID file +WORKFLOW_ID.txt \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index ac472b8..39f35b4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -12,6 +12,7 @@ from .monitor import views as monitor_views from .admin import views as admin_views from .connect import views as connect_views +from .maestro import views as maestro_views from .webforms import views as webforms_views from .views import core @@ -114,6 +115,10 @@ app.register_blueprint(connect_views.cneg001) +app.register_blueprint(maestro_views.mseg001) +app.register_blueprint(maestro_views.mseg002) +app.register_blueprint(maestro_views.mseg003) + app.register_blueprint(webforms_views.weg001) if "DYNO" in os.environ: # On Heroku? diff --git a/app/consts.py b/app/consts.py index 532015f..530ce45 100644 --- a/app/consts.py +++ b/app/consts.py @@ -29,6 +29,8 @@ # Base uri for callback function base_uri_suffix = "/restapi" +# Workflow name +workflow_name = "Example workflow - send invite to signer" # Default languages for brand languages = { @@ -114,5 +116,6 @@ "ROOMS": "Rooms", "ADMIN": "Admin", "CONNECT": "Connect", + "MAESTRO": "Maestro", "WEBFORMS": "WebForms" } diff --git a/app/docusign/ds_client.py b/app/docusign/ds_client.py index 384194e..348b4b4 100644 --- a/app/docusign/ds_client.py +++ b/app/docusign/ds_client.py @@ -33,6 +33,10 @@ "asset_group_account_read", "asset_group_account_clone_write", "asset_group_account_clone_read" ] +MAESTRO_SCOPES = [ + "signature", "aow_manage" +] + WEBFORMS_SCOPES = [ "signature", "webforms_read", "webforms_instance_read", "webforms_instance_write" ] @@ -61,6 +65,8 @@ def _auth_code_grant(cls, api): use_scopes.extend(CLICK_SCOPES) elif api == "Admin": use_scopes.extend(ADMIN_SCOPES) + elif api == "Maestro": + use_scopes.extend(MAESTRO_SCOPES) elif api == "WebForms": use_scopes.extend(WEBFORMS_SCOPES) else: @@ -99,6 +105,8 @@ def _jwt_auth(cls, api): use_scopes.extend(CLICK_SCOPES) elif api == "Admin": use_scopes.extend(ADMIN_SCOPES) + elif api == "Maestro": + use_scopes.extend(MAESTRO_SCOPES) elif api == "WebForms": use_scopes.extend(WEBFORMS_SCOPES) else: diff --git a/app/docusign/utils.py b/app/docusign/utils.py index 730751f..eefc0c0 100644 --- a/app/docusign/utils.py +++ b/app/docusign/utils.py @@ -1,7 +1,7 @@ from datetime import timedelta, datetime from functools import wraps import requests -import urllib +from urllib.parse import urlparse, parse_qs import json import re @@ -148,6 +148,16 @@ def get_user_info(access_token, base_path, oauth_host_name): api_client.set_oauth_host_name(oauth_host_name) return api_client.get_user_info(access_token) +def get_parameter_value_from_url(url, param_name): + parsed_url = urlparse(url) + query_params = parse_qs(parsed_url.query) + + # Access the parameter value (returns a list) + param_value_list = query_params.get(param_name, []) + + # If the parameter exists, return the first value; otherwise, return None + return param_value_list[0] if param_value_list else None + def replace_template_id(file_path, template_id): with open(file_path, 'r') as file: content = file.read() diff --git a/app/ds_config_sample.py b/app/ds_config_sample.py index 2d04866..b18d165 100644 --- a/app/ds_config_sample.py +++ b/app/ds_config_sample.py @@ -16,6 +16,7 @@ "rooms_api_client_host": "https://demo.rooms.docusign.com/restapi", "monitor_api_client_host": "https://lens-d.docusign.net", "admin_api_client_host": "https://api-d.docusign.net/management", + "maestro_api_client_host": "https://demo.services.docusign.net/", "webforms_api_client_host": "https://apps-d.docusign.com/api/webforms/v1.1", "allow_silent_authentication": True, # a user can be silently authenticated if they have an # active login session on another tab of the same browser diff --git a/app/maestro/__init__.py b/app/maestro/__init__.py new file mode 100644 index 0000000..1697d73 --- /dev/null +++ b/app/maestro/__init__.py @@ -0,0 +1,3 @@ +from .views import mseg001 +from .views import mseg002 +from .views import mseg003 diff --git a/app/maestro/examples/eg001_trigger_workflow.py b/app/maestro/examples/eg001_trigger_workflow.py new file mode 100644 index 0000000..7a99cac --- /dev/null +++ b/app/maestro/examples/eg001_trigger_workflow.py @@ -0,0 +1,75 @@ +from docusign_maestro import WorkflowManagementApi, WorkflowTriggerApi, TriggerPayload +from flask import session, request + +from app.docusign.utils import get_parameter_value_from_url +from app.ds_config import DS_CONFIG +from app.maestro.utils import create_maestro_api_client +from app.consts import pattern + + +class Eg001TriggerWorkflowController: + @staticmethod + def get_args(): + """Get request and session arguments""" + return { + "account_id": session["ds_account_id"], + "base_path": DS_CONFIG["maestro_api_client_host"], + "access_token": session["ds_access_token"], + "workflow_id": session["workflow_id"], + "instance_name": pattern.sub("", request.form.get("instance_name")), + "signer_email": pattern.sub("", request.form.get("signer_email")), + "signer_name": pattern.sub("", request.form.get("signer_name")), + "cc_email": pattern.sub("", request.form.get("cc_email")), + "cc_name": pattern.sub("", request.form.get("cc_name")), + } + + @staticmethod + def get_workflow_definitions(args): + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + workflow_management_api = WorkflowManagementApi(api_client) + workflow_definitions = workflow_management_api.get_workflow_definitions(args["account_id"], status="active") + + return workflow_definitions + + @staticmethod + def get_workflow_definition(args): + #ds-snippet-start:Maestro1Step2 + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + #ds-snippet-end:Maestro1Step2 + + #ds-snippet-start:Maestro1Step3 + workflow_management_api = WorkflowManagementApi(api_client) + workflow_definition = workflow_management_api.get_workflow_definition(args["account_id"], args["workflow_id"]) + #ds-snippet-end:Maestro1Step3 + + return workflow_definition + + @staticmethod + def trigger_workflow(workflow, args): + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + + #ds-snippet-start:Maestro1Step4 + trigger_payload = TriggerPayload( + instance_name=args["instance_name"], + participant={}, + payload={ + "signerEmail": args["signer_email"], + "signerName": args["signer_name"], + "ccEmail": args["cc_email"], + "ccName": args["cc_name"] + }, + metadata={} + ) + mtid = get_parameter_value_from_url(workflow.trigger_url, "mtid") + mtsec = get_parameter_value_from_url(workflow.trigger_url, "mtsec") + #ds-snippet-end:Maestro1Step4 + + #ds-snippet-start:Maestro1Step5 + workflow_trigger_api = WorkflowTriggerApi(api_client) + trigger_response = workflow_trigger_api.trigger_workflow( + args["account_id"], + trigger_payload, + mtid=mtid, mtsec=mtsec + ) + #ds-snippet-end:Maestro1Step5 + return trigger_response diff --git a/app/maestro/examples/eg002_cancel_workflow.py b/app/maestro/examples/eg002_cancel_workflow.py new file mode 100644 index 0000000..4425ef1 --- /dev/null +++ b/app/maestro/examples/eg002_cancel_workflow.py @@ -0,0 +1,45 @@ +from docusign_maestro import WorkflowInstanceManagementApi +from flask import session + +from app.ds_config import DS_CONFIG +from app.maestro.utils import create_maestro_api_client + + +class Eg002CancelWorkflowController: + @staticmethod + def get_args(): + """Get request and session arguments""" + return { + "account_id": session["ds_account_id"], + "base_path": DS_CONFIG["maestro_api_client_host"], + "access_token": session["ds_access_token"], + "workflow_id": session["workflow_id"], + "instance_id": session["instance_id"] + } + + @staticmethod + def get_instance_state(args): + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + workflow_instance_management_api = WorkflowInstanceManagementApi(api_client) + instance = workflow_instance_management_api.get_workflow_instance( + args["account_id"], + args["workflow_id"], + args["instance_id"] + ) + + return instance.instance_state + + @staticmethod + def cancel_workflow_instance(args): + #ds-snippet-start:Maestro2Step2 + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + #ds-snippet-end:Maestro2Step2 + + #ds-snippet-start:Maestro2Step3 + workflow_instance_management_api = WorkflowInstanceManagementApi(api_client) + cancel_result = workflow_instance_management_api.cancel_workflow_instance( + args["account_id"], + args["instance_id"] + ) + #ds-snippet-end:Maestro2Step3 + return cancel_result diff --git a/app/maestro/examples/eg003_get_workflow_status.py b/app/maestro/examples/eg003_get_workflow_status.py new file mode 100644 index 0000000..4777f0a --- /dev/null +++ b/app/maestro/examples/eg003_get_workflow_status.py @@ -0,0 +1,35 @@ +from docusign_maestro import WorkflowInstanceManagementApi +from flask import session + +from app.ds_config import DS_CONFIG +from app.maestro.utils import create_maestro_api_client + + +class Eg003GetWorkflowStatusController: + @staticmethod + def get_args(): + """Get request and session arguments""" + return { + "account_id": session["ds_account_id"], + "base_path": DS_CONFIG["maestro_api_client_host"], + "access_token": session["ds_access_token"], + "workflow_id": session["workflow_id"], + "instance_id": session["instance_id"] + } + + @staticmethod + def get_workflow_instance(args): + #ds-snippet-start:Maestro3Step2 + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + #ds-snippet-end:Maestro3Step2 + + #ds-snippet-start:Maestro3Step3 + workflow_instance_management_api = WorkflowInstanceManagementApi(api_client) + instance = workflow_instance_management_api.get_workflow_instance( + args["account_id"], + args["workflow_id"], + args["instance_id"] + ) + #ds-snippet-end:Maestro3Step3 + + return instance diff --git a/app/maestro/utils.py b/app/maestro/utils.py new file mode 100644 index 0000000..a44125d --- /dev/null +++ b/app/maestro/utils.py @@ -0,0 +1,584 @@ +import uuid +from docusign_maestro import ApiClient, WorkflowManagementApi, WorkflowDefinition, DeployRequest, \ + DSWorkflowTrigger, DSWorkflowVariableFromVariable, DeployStatus + +import json + + +def create_maestro_api_client(base_path, access_token): + api_client = ApiClient() + api_client.host = base_path + api_client.set_default_header(header_name="Authorization", header_value=f"Bearer {access_token}") + + return api_client + + +def create_workflow(args): + signer_id = str(uuid.uuid4()) + cc_id = str(uuid.uuid4()) + trigger_id = "wfTrigger" + + participants = { + signer_id: { + "participantRole": "Signer" + }, + cc_id: { + "participantRole": "CC" + } + } + + dac_id_field = f"dacId_{trigger_id}" + id_field = f"id_{trigger_id}" + signer_name_field = f"signerName_{trigger_id}" + signer_email_field = f"signerEmail_{trigger_id}" + cc_name_field = f"ccName_{trigger_id}" + cc_email_field = f"ccEmail_{trigger_id}" + + trigger = DSWorkflowTrigger( + name="Get_URL", + type="Http", + http_type="Get", + id=trigger_id, + input={ + 'metadata': { + 'customAttributes': {} + }, + 'payload': { + dac_id_field: { + 'source': 'step', + 'propertyName': 'dacId', + 'stepId': trigger_id + }, + id_field: { + 'source': 'step', + 'propertyName': 'id', + 'stepId': trigger_id + }, + signer_name_field: { + 'source': 'step', + 'propertyName': 'signerName', + 'stepId': trigger_id + }, + signer_email_field: { + 'source': 'step', + 'propertyName': 'signerEmail', + 'stepId': trigger_id + }, + cc_name_field: { + 'source': 'step', + 'propertyName': 'ccName', + 'stepId': trigger_id + }, + cc_email_field: { + 'source': 'step', + 'propertyName': 'ccEmail', + 'stepId': trigger_id + } + }, + 'participants': {} + }, + output={ + dac_id_field: { + 'source': 'step', + 'propertyName': 'dacId', + 'stepId': trigger_id + } + } + ) + + variables = { + dac_id_field: DSWorkflowVariableFromVariable(source='step', property_name='dacId', step_id=trigger_id), + id_field: DSWorkflowVariableFromVariable(source='step', property_name='id', step_id=trigger_id), + signer_name_field: DSWorkflowVariableFromVariable(source='step', property_name='signerName', + step_id=trigger_id), + signer_email_field: DSWorkflowVariableFromVariable(source='step', property_name='signerEmail', + step_id=trigger_id), + cc_name_field: DSWorkflowVariableFromVariable(source='step', property_name='ccName', step_id=trigger_id), + cc_email_field: DSWorkflowVariableFromVariable(source='step', property_name='ccEmail', step_id=trigger_id), + 'envelopeId_step2': DSWorkflowVariableFromVariable(source='step', property_name='envelopeId', step_id='step2', + type='String'), + 'combinedDocumentsBase64_step2': DSWorkflowVariableFromVariable(source='step', + property_name='combinedDocumentsBase64', + step_id='step2', type='File'), + 'fields.signer.text.value_step2': DSWorkflowVariableFromVariable(source='step', + property_name='fields.signer.text.value', + step_id='step2', type='String') + } + + step1 = { + 'id': 'step1', + 'name': 'Set Up Invite', + 'moduleName': 'Notification-SendEmail', + 'configurationProgress': 'Completed', + 'type': 'DS-EmailNotification', + 'config': { + 'templateType': 'WorkflowParticipantNotification', + 'templateVersion': 1, + 'language': 'en', + 'sender_name': 'DocuSign Orchestration', + 'sender_alias': 'Orchestration', + 'participantId': signer_id + }, + 'input': { + 'recipients': [ + { + 'name': { + 'source': 'step', + 'propertyName': 'signerName', + 'stepId': trigger_id + }, + 'email': { + 'source': 'step', + 'propertyName': 'signerEmail', + 'stepId': trigger_id + } + } + ], + 'mergeValues': { + 'CustomMessage': 'Follow this link to access and complete the workflow.', + 'ParticipantFullName': { + 'source': 'step', + 'propertyName': 'signerName', + 'stepId': trigger_id + } + } + }, + 'output': {} + } + + step2 = { + "id": 'step2', + "name": 'Get Signatures', + "moduleName": 'ESign', + "configurationProgress": 'Completed', + "type": 'DS-Sign', + "config": { + "participantId": signer_id, + }, + "input": { + "isEmbeddedSign": True, + "documents": [ + { + "type": 'FromDSTemplate', + "eSignTemplateId": args["template_id"], + }, + ], + "emailSubject": 'Please sign this document', + "emailBlurb": '', + "recipients": { + "signers": [ + { + "defaultRecipient": 'false', + "tabs": { + "signHereTabs": [ + { + "stampType": 'signature', + "name": 'SignHere', + "tabLabel": 'Sign Here', + "scaleValue": '1', + "optional": 'false', + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '191', + "yPosition": '148', + "tabId": '1', + "tabType": 'signhere', + }, + ], + 'textTabs': [ + { + "requireAll": 'false', + "value": '', + "required": 'false', + "locked": 'false', + "concealValueOnDocument": 'false', + "disableAutoSize": 'false', + "tabLabel": 'text', + "font": 'helvetica', + "fontSize": 'size14', + "localePolicy": {}, + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '153', + "yPosition": '230', + "width": '84', + "height": '23', + "tabId": '2', + "tabType": 'text', + }, + ], + "checkboxTabs": [ + { + "name": '', + "tabLabel": 'ckAuthorization', + "selected": 'false', + "selectedOriginal": 'false', + "requireInitialOnSharedChange": 'false', + "required": 'true', + "locked": 'false', + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '75', + "yPosition": '417', + "width": '0', + "height": '0', + "tabId": '3', + "tabType": 'checkbox', + }, + { + "name": '', + "tabLabel": 'ckAuthentication', + "selected": 'false', + "selectedOriginal": 'false', + "requireInitialOnSharedChange": 'false', + "required": 'true', + "locked": 'false', + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '75', + "yPosition": '447', + "width": '0', + "height": '0', + "tabId": '4', + "tabType": 'checkbox', + }, + { + "name": '', + "tabLabel": 'ckAgreement', + "selected": 'false', + "selectedOriginal": 'false', + "requireInitialOnSharedChange": 'false', + "required": 'true', + "locked": 'false', + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '75', + "yPosition": '478', + "width": '0', + "height": '0', + "tabId": '5', + "tabType": 'checkbox', + }, + { + "name": '', + "tabLabel": 'ckAcknowledgement', + "selected": 'false', + "selectedOriginal": 'false', + "requireInitialOnSharedChange": 'false', + "required": 'true', + "locked": 'false', + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '75', + "yPosition": '508', + "width": '0', + "height": '0', + "tabId": '6', + "tabType": 'checkbox', + }, + ], + "radioGroupTabs": [ + { + "documentId": '1', + "recipientId": '1', + "groupName": 'radio1', + "radios": [ + { + "pageNumber": '1', + "xPosition": '142', + "yPosition": '384', + "value": 'white', + "selected": 'false', + "tabId": '7', + "required": 'false', + "locked": 'false', + "bold": 'false', + "italic": 'false', + "underline": 'false', + "fontColor": 'black', + "fontSize": 'size7', + }, + { + "pageNumber": '1', + "xPosition": '74', + "yPosition": '384', + "value": 'red', + "selected": 'false', + "tabId": '8', + "required": 'false', + "locked": 'false', + "bold": 'false', + "italic": 'false', + "underline": 'false', + "fontColor": 'black', + "fontSize": 'size7', + }, + { + "pageNumber": '1', + "xPosition": '220', + "yPosition": '384', + "value": 'blue', + "selected": 'false', + "tabId": '9', + "required": 'false', + "locked": 'false', + "bold": 'false', + "italic": 'false', + "underline": 'false', + "fontColor": 'black', + "fontSize": 'size7', + }, + ], + "shared": 'false', + "requireInitialOnSharedChange": 'false', + "requireAll": 'false', + "tabType": 'radiogroup', + "value": '', + "originalValue": '', + }, + ], + "listTabs": [ + { + "listItems": [ + { + "text": 'Red', + "value": 'red', + "selected": 'false', + }, + { + "text": 'Orange', + "value": 'orange', + "selected": 'false', + }, + { + "text": 'Yellow', + "value": 'yellow', + "selected": 'false', + }, + { + "text": 'Green', + "value": 'green', + "selected": 'false', + }, + { + "text": 'Blue', + "value": 'blue', + "selected": 'false', + }, + { + "text": 'Indigo', + "value": 'indigo', + "selected": 'false', + }, + { + "text": 'Violet', + "value": 'violet', + "selected": 'false', + }, + ], + "value": '', + "originalValue": '', + "required": 'false', + "locked": 'false', + "requireAll": 'false', + "tabLabel": 'list', + "font": 'helvetica', + "fontSize": 'size14', + "localePolicy": {}, + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '142', + "yPosition": '291', + "width": '78', + "height": '0', + "tabId": '10', + "tabType": 'list', + }, + ], + "numericalTabs": [ + { + "validationType": 'currency', + "value": '', + "required": 'false', + "locked": 'false', + "concealValueOnDocument": 'false', + "disableAutoSize": 'false', + "tabLabel": 'numericalCurrency', + "font": 'helvetica', + "fontSize": 'size14', + "localePolicy": { + "cultureName": 'en-US', + "currencyPositiveFormat": + 'csym_1_comma_234_comma_567_period_89', + "currencyNegativeFormat": + 'opar_csym_1_comma_234_comma_567_period_89_cpar', + "currencyCode": 'usd', + }, + "documentId": '1', + "recipientId": '1', + "pageNumber": '1', + "xPosition": '163', + "yPosition": '260', + "width": '84', + "height": '0', + "tabId": '11', + "tabType": 'numerical', + }, + ], + }, + "signInEachLocation": 'false', + "agentCanEditEmail": 'false', + "agentCanEditName": 'false', + "requireUploadSignature": 'false', + "name": { + "source": 'step', + "propertyName": 'signerName', + "stepId": trigger_id, + }, + "email": { + "source": 'step', + "propertyName": 'signerEmail', + "stepId": trigger_id, + }, + "recipientId": '1', + "recipientIdGuid": '00000000-0000-0000-0000-000000000000', + "accessCode": '', + "requireIdLookup": 'false', + "routingOrder": '1', + "note": '', + "roleName": 'signer', + "completedCount": '0', + "deliveryMethod": 'email', + "templateLocked": 'false', + "templateRequired": 'false', + "inheritEmailNotificationConfiguration": 'false', + "recipientType": 'signer', + }, + ], + "carbonCopies": [ + { + "agentCanEditEmail": 'false', + "agentCanEditName": 'false', + "name": { + "source": 'step', + "propertyName": 'ccName', + "stepId": trigger_id, + }, + "email": { + "source": 'step', + "propertyName": 'ccEmail', + "stepId": trigger_id, + }, + "recipientId": '2', + "recipientIdGuid": '00000000-0000-0000-0000-000000000000', + "accessCode": '', + "requireIdLookup": 'false', + "routingOrder": '2', + "note": '', + "roleName": 'cc', + "completedCount": '0', + "deliveryMethod": 'email', + "templateLocked": 'false', + "templateRequired": 'false', + "inheritEmailNotificationConfiguration": 'false', + "recipientType": 'carboncopy', + }, + ], + "certifiedDeliveries": [], + }, + }, + "output": { + "envelopeId_step2": { + "source": 'step', + "propertyName": 'envelopeId', + "stepId": 'step2', + "type": 'String', + }, + "combinedDocumentsBase64_step2": { + "source": 'step', + "propertyName": 'combinedDocumentsBase64', + "stepId": 'step2', + "type": 'File', + }, + 'fields.signer.text.value_step2': { + "source": 'step', + "propertyName": 'fields.signer.text.value', + "stepId": 'step2', + "type": 'String', + }, + }, + } + + step3 = { + "id": 'step3', + "name": 'Show a Confirmation Screen', + "moduleName": 'ShowConfirmationScreen', + "configurationProgress": 'Completed', + "type": 'DS-ShowScreenStep', + "config": { + "participantId": signer_id + }, + "input": { + "httpType": "Post", + "payload": { + "participantId": signer_id, + "confirmationMessage": { + "title": 'Tasks complete', + "description": 'You have completed all your workflow tasks.' + } + } + }, + "output": {} + } + + workflow_definition = WorkflowDefinition( + workflow_name="Example workflow - send invite to signer", + workflow_description="", + document_version="1.0.0", + schema_version="1.0.0", + account_id=args["account_id"], + participants=participants, + trigger=trigger, + variables=variables, + steps=[step1, step2, step3] + ) + + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + workflow_management_api = WorkflowManagementApi(api_client) + # body = {"workflowDefinition": workflow_definition.__dict__} + workflow = workflow_management_api.create_workflow_definition( + args["account_id"], + {"workflowDefinition": workflow_definition} + ) + + return workflow.workflow_definition_id + + +def publish_workflow(args, workflow_id): + api_client = create_maestro_api_client(args["base_path"], args["access_token"]) + workflow_management_api = WorkflowManagementApi(api_client) + + try: + deploy_request = DeployRequest( + deployment_status=DeployStatus.PUBLISH + ) + workflow_management_api.publish_or_un_publish_workflow_definition( + args["account_id"], + workflow_id, + deploy_request + ) + except Exception as err: + if hasattr(err, 'response') and hasattr(err.response, 'data'): + response_data = json.loads(err.response.data) + if 'message' in response_data: + is_consent_required = response_data['message'] == 'Consent required' + if is_consent_required: + return response_data["consentUrl"] + raise err diff --git a/app/maestro/views/__init__.py b/app/maestro/views/__init__.py new file mode 100644 index 0000000..93520d8 --- /dev/null +++ b/app/maestro/views/__init__.py @@ -0,0 +1,3 @@ +from .eg001_trigger_workflow import mseg001 +from .eg002_cancel_workflow import mseg002 +from .eg003_get_workflow_status import mseg003 diff --git a/app/maestro/views/eg001_trigger_workflow.py b/app/maestro/views/eg001_trigger_workflow.py new file mode 100644 index 0000000..8df42bd --- /dev/null +++ b/app/maestro/views/eg001_trigger_workflow.py @@ -0,0 +1,184 @@ +"""Example 001: How to trigger a Maestro workflow""" + +import json + +from docusign_maestro.client.api_exception import ApiException +from flask import render_template, Blueprint, session + +from ..examples.eg001_trigger_workflow import Eg001TriggerWorkflowController +from ...docusign import authenticate, ensure_manifest, get_example_by_number +from ...ds_config import DS_CONFIG +from ...error_handlers import process_error +from ...consts import API_TYPE +from ..utils import create_workflow, publish_workflow + +example_number = 1 +api = API_TYPE["MAESTRO"] +eg = f"mseg00{example_number}" # reference (and url) for this example +mseg001 = Blueprint(eg, __name__) + + +@mseg001.route(f"/{eg}", methods=["POST"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def trigger_workflow(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Show results + """ + example = get_example_by_number(session["manifest"], example_number, api) + + # 1. Get required arguments + args = Eg001TriggerWorkflowController.get_args() + try: + # 1. Call the worker method + print("args:\n\n") + print(args) + workflow = Eg001TriggerWorkflowController.get_workflow_definition(args) + results = Eg001TriggerWorkflowController.trigger_workflow(workflow, args) + session["instance_id"] = results.instance_id + except ApiException as err: + if hasattr(err, "status"): + if err.status == 403: + return render_template( + "error.html", + err=err, + error_code=err.status, + error_message=session["manifest"]["SupportingTexts"]["ContactSupportToEnableFeature"] + .format("Maestro") + ) + + return process_error(err) + # 3. Show results + return render_template( + "example_done.html", + title=example["ExampleName"], + message=example["ResultsPageText"], + json=json.dumps(json.dumps(results.to_dict())) + ) + + +@mseg001.route(f"/{eg}", methods=["GET"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def get_view(): + """responds with the form for the example""" + example = get_example_by_number(session["manifest"], example_number, api) + additional_page_data = next((p for p in example["AdditionalPage"] if p["Name"] == "publish_workflow"), None) + + args = { + "account_id": session["ds_account_id"], + "base_path": DS_CONFIG["maestro_api_client_host"], + "access_token": session["ds_access_token"], + "template_id": session.get("template_id", None) + } + try: + workflows = Eg001TriggerWorkflowController.get_workflow_definitions(args) + + if workflows.count > 0: + sorted_workflows = sorted( + workflows.value, + key=lambda w: w.last_updated_date, + reverse=True + ) + + if sorted_workflows: + session["workflow_id"] = sorted_workflows[0].id + + if "workflow_id" not in session: + if "template_id" not in session: + return render_template( + "maestro/eg001_trigger_workflow.html", + title=example["ExampleName"], + example=example, + template_ok=False, + source_file="eg001_trigger_workflow.py", + source_url=DS_CONFIG["github_example_url"] + "eg001_trigger_workflow.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + ) + + # if there is no workflow, then create one + session["workflow_id"] = create_workflow(args) + consent_url = publish_workflow(args, session["workflow_id"]) + + if consent_url: + return render_template( + "maestro/eg001_publish_workflow.html", + title=example["ExampleName"], + message=additional_page_data["ResultsPageText"], + consent_url=consent_url + ) + + except ApiException as err: + if hasattr(err, "status"): + if err.status == 403: + return render_template( + "error.html", + err=err, + error_code=err.status, + error_message=session["manifest"]["SupportingTexts"]["ContactSupportToEnableFeature"] + .format("Maestro") + ) + + return process_error(err) + + return render_template( + "maestro/eg001_trigger_workflow.html", + title=example["ExampleName"], + example=example, + template_ok=True, + source_file="eg001_trigger_workflow.py", + source_url=DS_CONFIG["github_example_url"] + "eg001_trigger_workflow.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + ) + +@mseg001.route(f"/{eg}publish", methods=["POST"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def publish_workflow_view(): + """responds with the form for the example""" + example = get_example_by_number(session["manifest"], example_number, api) + additional_page_data = next((p for p in example["AdditionalPage"] if p["Name"] == "publish_workflow"), None) + + args = { + "account_id": session["ds_account_id"], + "base_path": DS_CONFIG["maestro_api_client_host"], + "access_token": session["ds_access_token"] + } + try: + consent_url = publish_workflow(args, session["workflow_id"]) + + if consent_url: + return render_template( + "maestro/eg001_publish_workflow.html", + title=example["ExampleName"], + message=additional_page_data["ResultsPageText"], + consent_url=consent_url + ) + + except ApiException as err: + if hasattr(err, "status"): + if err.status == 403: + return render_template( + "error.html", + err=err, + error_code=err.status, + error_message=session["manifest"]["SupportingTexts"]["ContactSupportToEnableFeature"] + .format("Maestro") + ) + + return process_error(err) + + return render_template( + "maestro/eg001_trigger_workflow.html", + title=example["ExampleName"], + example=example, + template_ok=True, + source_file="eg001_trigger_workflow.py", + source_url=DS_CONFIG["github_example_url"] + "eg001_trigger_workflow.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + ) diff --git a/app/maestro/views/eg002_cancel_workflow.py b/app/maestro/views/eg002_cancel_workflow.py new file mode 100644 index 0000000..e5a86fa --- /dev/null +++ b/app/maestro/views/eg002_cancel_workflow.py @@ -0,0 +1,103 @@ +"""Example 002: How to cancel a Maestro workflow instance""" + +import json + +from docusign_maestro.client.api_exception import ApiException +from flask import render_template, Blueprint, session + +from ..examples.eg002_cancel_workflow import Eg002CancelWorkflowController +from ...docusign import authenticate, ensure_manifest, get_example_by_number +from ...ds_config import DS_CONFIG +from ...error_handlers import process_error +from ...consts import API_TYPE + +example_number = 2 +api = API_TYPE["MAESTRO"] +eg = f"mseg00{example_number}" # reference (and url) for this example +mseg002 = Blueprint(eg, __name__) + + +@mseg002.route(f"/{eg}", methods=["POST"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def cancel_workflow(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Show results + """ + example = get_example_by_number(session["manifest"], example_number, api) + + # 1. Get required arguments + args = Eg002CancelWorkflowController.get_args() + try: + # 1. Call the worker method + results = Eg002CancelWorkflowController.cancel_workflow_instance(args) + except ApiException as err: + if hasattr(err, "status"): + if err.status == 403: + return render_template( + "error.html", + err=err, + error_code=err.status, + error_message=session["manifest"]["SupportingTexts"]["ContactSupportToEnableFeature"] + .format("Maestro") + ) + + return process_error(err) + # 3. Show results + return render_template( + "example_done.html", + title=example["ExampleName"], + message=example["ResultsPageText"].format(session["instance_id"]), + json=json.dumps(json.dumps(results.to_dict())) + ) + + +@mseg002.route(f"/{eg}", methods=["GET"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def get_view(): + """responds with the form for the example""" + example = get_example_by_number(session["manifest"], example_number, api) + + instance_ok = False + workflow_id = session.get("workflow_id", None) + instance_id = session.get("instance_id", None) + if workflow_id and instance_id: + args = { + "account_id": session["ds_account_id"], + "base_path": DS_CONFIG["maestro_api_client_host"], + "access_token": session["ds_access_token"], + "workflow_id": workflow_id, + "instance_id": instance_id + } + + try: + state = Eg002CancelWorkflowController.get_instance_state(args) + instance_ok = state.lower() == "in progress" + except ApiException as err: + if hasattr(err, "status"): + if err.status == 403: + return render_template( + "error.html", + err=err, + error_code=err.status, + error_message=session["manifest"]["SupportingTexts"]["ContactSupportToEnableFeature"] + .format("Maestro") + ) + + return process_error(err) + + return render_template( + "maestro/eg002_cancel_workflow.html", + title=example["ExampleName"], + example=example, + instance_ok=instance_ok, + workflow_id=workflow_id, + instance_id=instance_id, + source_file="eg002_cancel_workflow.py", + source_url=DS_CONFIG["github_example_url"] + "eg002_cancel_workflow.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + ) diff --git a/app/maestro/views/eg003_get_workflow_status.py b/app/maestro/views/eg003_get_workflow_status.py new file mode 100644 index 0000000..6a5cc37 --- /dev/null +++ b/app/maestro/views/eg003_get_workflow_status.py @@ -0,0 +1,76 @@ +"""Example 003: How to get the status of a Maestro workflow instance""" + +import json + +from docusign_maestro.client.api_exception import ApiException +from flask import render_template, Blueprint, session + +from ..examples.eg003_get_workflow_status import Eg003GetWorkflowStatusController +from ...docusign import authenticate, ensure_manifest, get_example_by_number +from ...ds_config import DS_CONFIG +from ...error_handlers import process_error +from ...consts import API_TYPE + +example_number = 3 +api = API_TYPE["MAESTRO"] +eg = f"mseg00{example_number}" # reference (and url) for this example +mseg003 = Blueprint(eg, __name__) + + +@mseg003.route(f"/{eg}", methods=["POST"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def get_workflow_status(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Show results + """ + example = get_example_by_number(session["manifest"], example_number, api) + + # 1. Get required arguments + args = Eg003GetWorkflowStatusController.get_args() + try: + # 1. Call the worker method + results = Eg003GetWorkflowStatusController.get_workflow_instance(args) + except ApiException as err: + if hasattr(err, "status"): + if err.status == 403: + return render_template( + "error.html", + err=err, + error_code=err.status, + error_message=session["manifest"]["SupportingTexts"]["ContactSupportToEnableFeature"] + .format("Maestro") + ) + + return process_error(err) + # 3. Show results + return render_template( + "example_done.html", + title=example["ExampleName"], + message=example["ResultsPageText"].format(results.instance_state), + json=json.dumps(json.dumps(results.to_dict(), default=str)) + ) + + +@mseg003.route(f"/{eg}", methods=["GET"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def get_view(): + """responds with the form for the example""" + example = get_example_by_number(session["manifest"], example_number, api) + + workflow_id = session.get("workflow_id", None) + instance_id = session.get("instance_id", None) + return render_template( + "maestro/eg003_get_workflow_status.html", + title=example["ExampleName"], + example=example, + workflow_id=workflow_id, + instance_id=instance_id, + source_file="eg003_get_workflow_status.py", + source_url=DS_CONFIG["github_example_url"] + "eg003_get_workflow_status.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + ) diff --git a/app/static/assets/search.js b/app/static/assets/search.js index 92e2e2c..8c564c7 100644 --- a/app/static/assets/search.js +++ b/app/static/assets/search.js @@ -6,6 +6,7 @@ const DS_SEARCH = (function () { ROOMS: "rooms", ADMIN: "admin", CONNECT: "connect", + MAESTRO: "maestro", WEBFORMS: "webforms" } @@ -143,6 +144,8 @@ const DS_SEARCH = (function () { return "eg"; case API_TYPES.CONNECT: return "cneg"; + case API_TYPES.MAESTRO: + return "mseg"; case API_TYPES.WEBFORMS: return "weg"; } diff --git a/app/templates/error.html b/app/templates/error.html index 35b9a86..1a8819d 100644 --- a/app/templates/error.html +++ b/app/templates/error.html @@ -7,7 +7,7 @@
Error information:
{% if error_code %} -{{ error_code }}: {{ error_message }}
+{{ error_code }}: {{ error_message | safe }}
{% else %}{{ err }}{% endif %} diff --git a/app/templates/home.html b/app/templates/home.html index 72e9b04..701c56f 100644 --- a/app/templates/home.html +++ b/app/templates/home.html @@ -45,7 +45,7 @@
{{ message.format(consent_url) | safe }}
+ + + +{% endblock %} diff --git a/app/templates/maestro/eg001_trigger_workflow.html b/app/templates/maestro/eg001_trigger_workflow.html new file mode 100644 index 0000000..3660b61 --- /dev/null +++ b/app/templates/maestro/eg001_trigger_workflow.html @@ -0,0 +1,59 @@ + {% extends "base.html" %} {% block content %} + +{% include 'example_info.html' %} + +{% set form_index = 0 %} +{% set instance_name_index = 0 %} +{% set signer_email_index = 1 %} +{% set signer_name_index = 2 %} +{% set cc_email_index = 3 %} +{% set cc_name_index = 4 %} +{% set redirect_to8_index = 0 %} + +{% if template_ok %} + +{% else %} + {{ example['RedirectsToOtherCodeExamples'][redirect_to8_index]['RedirectText'].format('href="eg008"') | safe }} + + +{% endif %} + +{% endblock %} diff --git a/app/templates/maestro/eg002_cancel_workflow.html b/app/templates/maestro/eg002_cancel_workflow.html new file mode 100644 index 0000000..a0545d7 --- /dev/null +++ b/app/templates/maestro/eg002_cancel_workflow.html @@ -0,0 +1,38 @@ + {% extends "base.html" %} {% block content %} + +{% include 'example_info.html' %} + +{% set form_index = 0 %} +{% set workflow_id_index = 0 %} +{% set instance_id_index = 1 %} +{% set redirect_to8_index = 0 %} + +{% if instance_ok %} + +{% else %} + {{ example['RedirectsToOtherCodeExamples'][redirect_to8_index]['RedirectText'].format('href="mseg001"') | safe }} + + +{% endif %} + +{% endblock %} diff --git a/app/templates/maestro/eg003_get_workflow_status.html b/app/templates/maestro/eg003_get_workflow_status.html new file mode 100644 index 0000000..50b4abd --- /dev/null +++ b/app/templates/maestro/eg003_get_workflow_status.html @@ -0,0 +1,38 @@ + {% extends "base.html" %} {% block content %} + +{% include 'example_info.html' %} + +{% set form_index = 0 %} +{% set workflow_id_index = 0 %} +{% set instance_id_index = 1 %} +{% set redirect_to8_index = 0 %} + +{% if workflow_id and instance_id %} + +{% else %} + {{ example['RedirectsToOtherCodeExamples'][redirect_to8_index]['RedirectText'].format('href="mseg001"') | safe }} + + +{% endif %} + +{% endblock %}