diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0f292b31 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# start by pulling the python image +FROM python:3.11.9-slim-bullseye + +# copy the requirements file into the image +COPY ./requirements.txt /app/requirements.txt + +# switch working directory +WORKDIR /app + +# install the dependencies and packages in the requirements file +RUN pip install -r requirements.txt + +# copy every content from the local file to the image +COPY . /app + +# configure the container to run in an executed manner +ENTRYPOINT [ "python" ] + +CMD ["run.py", "--docker"] diff --git a/README.md b/README.md index 064236ab..dfe22f92 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ # Python Launcher Code Examples +> +>### PLEASE! Share your feedback in a [two-question survey](https://docs.google.com/forms/d/e/1FAIpQLScPa74hwhJwi7XWDDj4-XZVOQTF9jJWgbIFEpulXokCqYWT4A/viewform?usp=pp_url&entry.680551577=Python). +> +> ### GitHub repo: [code-examples-python](./README.md) -This GitHub repo includes code examples for the DocuSign Admin API, Click API, eSignature REST API, Monitor API, and Rooms API. By default, the launcher will display the eSignature examples. To switch between API code examples, select "Choose API" in the top menu. +If you downloaded this project using the [Quickstart](https://developers.docusign.com/docs/esign-rest-api/quickstart/) tool, it may be configured in one of three ways: + +* **[JWT Grant remote signing example](#jwt-grant-remote-signing-example)**–demonstrates how to implement JSON Web Token authentication. It includes a single remote signing workflow. +* **[Authorization Code Grant embedded signing example](#authorization-code-grant-embedded-signing-example)**–demonstrates how to implement Authorization Code Grant authentication. It includes a single embedded signing workflow. +* **[Multiple code examples, Authorization Code Grant and JWT Grant](#installation-steps)**–includes the full range of examples and authentication types. + +***Installation and running instructions vary depending on the configuration. Follow the link that matches your project type to get started.*** + +This GitHub repo includes code examples for the [Web Forms API](https://developers.docusign.com/docs/web-forms-api/), [Docusign Admin API](https://developers.docusign.com/docs/admin-api/), [Click API](https://developers.docusign.com/docs/click-api/), [eSignature REST API](https://developers.docusign.com/docs/esign-rest-api/), [Monitor API](https://developers.docusign.com/docs/monitor-api/), and [Rooms API](https://developers.docusign.com/docs/rooms-api/). + ## Introduction @@ -107,6 +120,15 @@ For a list of code examples that use the Web Forms API, see the [How-to guides o **Note:** You will need to alias the python command to run Python 3 or use `python3 run.py` 1. Open a browser to http://localhost:3000 +### Installation steps with docker + +**Note**: Running the launcher with docker will use Python 3.11 + +1. Open the Docker application +1. `docker image build -t docusign .` +1. `docker run --name docusign_python -p 3000:3000 -d docusign` +1. Open a browser to http://localhost:3000 + ### Installation steps for JWT Grant authentication **Note:** If you downloaded this code using [Quickstart](https://developers.docusign.com/docs/esign-rest-api/quickstart/) from the Docusign Developer Center, skip step 4 as it was automatically performed for you. @@ -135,6 +157,23 @@ Also, in order to select JSON Web Token authentication in the launcher, in app/d See [Docusign Quickstart overview](https://developers.docusign.com/docs/esign-rest-api/quickstart/overview/) on the Docusign Developer Center for more information on how to run the JWT grant remote signing project and the Authorization Code Grant embedded signing project. +### Authorization Code Grant embedded signing example: +Run in Git Bash: +``` +$ cd +$ pip install -r requirements.txt +$ python3 -m app.quick_acg.run +``` + +Open a browser to http://localhost:3000 + +### JWT grant remote signing example: +Run in Windows Command Prompt (CMD): +``` +$ cd +$ python3 jwt_console.py +``` + ### Installation steps for JWT grant remote signing example Follow the instructions below if you downloaded the JWT grant remote signing example. @@ -162,4 +201,4 @@ This repository uses the MIT License. See [LICENSE](./LICENSE) for details. ### Pull Requests Pull requests are welcomed. Pull requests will only be considered if their content -uses the MIT License. \ No newline at end of file +uses the MIT License. diff --git a/app/__init__.py b/app/__init__.py index 39f35b44..2f38180a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,131 +1,135 @@ -import os - -from flask import Flask, session, current_app -from flask_wtf.csrf import CSRFProtect - -from .ds_config import DS_CONFIG -from .eSignature import views as esignature_views -from .docusign.views import ds -from .api_type import EXAMPLES_API_TYPE -from .rooms import views as rooms_views -from .click import views as click_views -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 - -session_path = "/tmp/python_recipe_sessions" -app = Flask(__name__) - -app.config.from_pyfile("config.py") - -# See https://flask-wtf.readthedocs.io/en/stable/csrf.html -csrf = CSRFProtect(app) - -# Set whether this is a quickstart in config -#app.config["quickstart"] = DS_CONFIG["quickstart"] - -# Set whether user has logged in -#app.config["isLoggedIn"] = False - -# Register home page -app.register_blueprint(core) - -# Register OAuth -app.register_blueprint(ds) -# Register examples - -app.register_blueprint(rooms_views.reg001) -app.register_blueprint(rooms_views.reg002) -app.register_blueprint(rooms_views.reg003) -app.register_blueprint(rooms_views.reg004) -app.register_blueprint(rooms_views.reg005) -app.register_blueprint(rooms_views.reg006) -app.register_blueprint(rooms_views.reg007) -app.register_blueprint(rooms_views.reg008) -app.register_blueprint(rooms_views.reg009) - -app.register_blueprint(monitor_views.meg001) - -app.register_blueprint(admin_views.aeg001) -app.register_blueprint(admin_views.aeg002) -app.register_blueprint(admin_views.aeg003) -app.register_blueprint(admin_views.aeg004) -app.register_blueprint(admin_views.aeg005) -app.register_blueprint(admin_views.aeg006) -app.register_blueprint(admin_views.aeg007) -app.register_blueprint(admin_views.aeg008) -app.register_blueprint(admin_views.aeg009) -app.register_blueprint(admin_views.aeg010) -app.register_blueprint(admin_views.aeg011) -app.register_blueprint(admin_views.aeg012) - -app.register_blueprint(click_views.ceg001) -app.register_blueprint(click_views.ceg002) -app.register_blueprint(click_views.ceg003) -app.register_blueprint(click_views.ceg004) -app.register_blueprint(click_views.ceg005) -app.register_blueprint(click_views.ceg006) - -app.register_blueprint(esignature_views.eg001) -app.register_blueprint(esignature_views.eg002) -app.register_blueprint(esignature_views.eg003) -app.register_blueprint(esignature_views.eg004) -app.register_blueprint(esignature_views.eg005) -app.register_blueprint(esignature_views.eg006) -app.register_blueprint(esignature_views.eg007) -app.register_blueprint(esignature_views.eg008) -app.register_blueprint(esignature_views.eg009) -app.register_blueprint(esignature_views.eg010) -app.register_blueprint(esignature_views.eg011) -app.register_blueprint(esignature_views.eg012) -app.register_blueprint(esignature_views.eg013) -app.register_blueprint(esignature_views.eg014) -app.register_blueprint(esignature_views.eg015) -app.register_blueprint(esignature_views.eg016) -app.register_blueprint(esignature_views.eg017) -app.register_blueprint(esignature_views.eg018) -app.register_blueprint(esignature_views.eg019) -app.register_blueprint(esignature_views.eg020) -app.register_blueprint(esignature_views.eg022) -app.register_blueprint(esignature_views.eg023) -app.register_blueprint(esignature_views.eg024) -app.register_blueprint(esignature_views.eg025) -app.register_blueprint(esignature_views.eg026) -app.register_blueprint(esignature_views.eg027) -app.register_blueprint(esignature_views.eg028) -app.register_blueprint(esignature_views.eg029) -app.register_blueprint(esignature_views.eg030) -app.register_blueprint(esignature_views.eg031) -app.register_blueprint(esignature_views.eg032) -app.register_blueprint(esignature_views.eg033) -app.register_blueprint(esignature_views.eg034) -app.register_blueprint(esignature_views.eg035) -app.register_blueprint(esignature_views.eg036) -app.register_blueprint(esignature_views.eg037) -app.register_blueprint(esignature_views.eg038) -app.register_blueprint(esignature_views.eg039) -app.register_blueprint(esignature_views.eg040) -app.register_blueprint(esignature_views.eg041) -app.register_blueprint(esignature_views.eg042) -app.register_blueprint(esignature_views.eg043) -app.register_blueprint(esignature_views.eg044) - -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? - import logging - - stream_handler = logging.StreamHandler() - app.logger.addHandler(stream_handler) - app.logger.setLevel(logging.INFO) - app.logger.info("Recipe example startup") - app.config.update(dict(PREFERRED_URL_SCHEME="https")) +import os + +from flask import Flask, session, current_app +from flask_wtf.csrf import CSRFProtect + +from .ds_config import DS_CONFIG +from .eSignature import views as esignature_views +from .docusign.views import ds +from .api_type import EXAMPLES_API_TYPE +from .rooms import views as rooms_views +from .click import views as click_views +from .monitor import views as monitor_views +from .admin import views as admin_views +from .connect import views as connect_views +from .webforms import views as webforms_views +from .notary import views as notary_views +from .connected_fields import views as connected_fields_views +from .views import core + +session_path = "/tmp/python_recipe_sessions" +app = Flask(__name__) + +app.config.from_pyfile("config.py") + +# See https://flask-wtf.readthedocs.io/en/stable/csrf.html +csrf = CSRFProtect(app) + +# Set whether this is a quickstart in config +#app.config["quickstart"] = DS_CONFIG["quickstart"] + +# Set whether user has logged in +#app.config["isLoggedIn"] = False + +# Register home page +app.register_blueprint(core) + +# Register OAuth +app.register_blueprint(ds) +# Register examples + +app.register_blueprint(rooms_views.reg001) +app.register_blueprint(rooms_views.reg002) +app.register_blueprint(rooms_views.reg003) +app.register_blueprint(rooms_views.reg004) +app.register_blueprint(rooms_views.reg005) +app.register_blueprint(rooms_views.reg006) +app.register_blueprint(rooms_views.reg007) +app.register_blueprint(rooms_views.reg008) +app.register_blueprint(rooms_views.reg009) + +app.register_blueprint(monitor_views.meg001) + +app.register_blueprint(admin_views.aeg001) +app.register_blueprint(admin_views.aeg002) +app.register_blueprint(admin_views.aeg003) +app.register_blueprint(admin_views.aeg004) +app.register_blueprint(admin_views.aeg005) +app.register_blueprint(admin_views.aeg006) +app.register_blueprint(admin_views.aeg007) +app.register_blueprint(admin_views.aeg008) +app.register_blueprint(admin_views.aeg009) +app.register_blueprint(admin_views.aeg010) +app.register_blueprint(admin_views.aeg011) +app.register_blueprint(admin_views.aeg012) +app.register_blueprint(admin_views.aeg013) + +app.register_blueprint(click_views.ceg001) +app.register_blueprint(click_views.ceg002) +app.register_blueprint(click_views.ceg003) +app.register_blueprint(click_views.ceg004) +app.register_blueprint(click_views.ceg005) +app.register_blueprint(click_views.ceg006) + +app.register_blueprint(esignature_views.eg001) +app.register_blueprint(esignature_views.eg002) +app.register_blueprint(esignature_views.eg003) +app.register_blueprint(esignature_views.eg004) +app.register_blueprint(esignature_views.eg005) +app.register_blueprint(esignature_views.eg006) +app.register_blueprint(esignature_views.eg007) +app.register_blueprint(esignature_views.eg008) +app.register_blueprint(esignature_views.eg009) +app.register_blueprint(esignature_views.eg010) +app.register_blueprint(esignature_views.eg011) +app.register_blueprint(esignature_views.eg012) +app.register_blueprint(esignature_views.eg013) +app.register_blueprint(esignature_views.eg014) +app.register_blueprint(esignature_views.eg015) +app.register_blueprint(esignature_views.eg016) +app.register_blueprint(esignature_views.eg017) +app.register_blueprint(esignature_views.eg018) +app.register_blueprint(esignature_views.eg019) +app.register_blueprint(esignature_views.eg020) +app.register_blueprint(esignature_views.eg022) +app.register_blueprint(esignature_views.eg023) +app.register_blueprint(esignature_views.eg024) +app.register_blueprint(esignature_views.eg025) +app.register_blueprint(esignature_views.eg026) +app.register_blueprint(esignature_views.eg027) +app.register_blueprint(esignature_views.eg028) +app.register_blueprint(esignature_views.eg029) +app.register_blueprint(esignature_views.eg030) +app.register_blueprint(esignature_views.eg031) +app.register_blueprint(esignature_views.eg032) +app.register_blueprint(esignature_views.eg033) +app.register_blueprint(esignature_views.eg034) +app.register_blueprint(esignature_views.eg035) +app.register_blueprint(esignature_views.eg036) +app.register_blueprint(esignature_views.eg037) +app.register_blueprint(esignature_views.eg038) +app.register_blueprint(esignature_views.eg039) +app.register_blueprint(esignature_views.eg040) +app.register_blueprint(esignature_views.eg041) +app.register_blueprint(esignature_views.eg042) +app.register_blueprint(esignature_views.eg043) +app.register_blueprint(esignature_views.eg044) +app.register_blueprint(esignature_views.eg045) + +app.register_blueprint(connect_views.cneg001) + +app.register_blueprint(webforms_views.weg001) +app.register_blueprint(webforms_views.weg002) + +app.register_blueprint(notary_views.neg004) + +app.register_blueprint(connected_fields_views.feg001) + +if "DYNO" in os.environ: # On Heroku? + import logging + + stream_handler = logging.StreamHandler() + app.logger.addHandler(stream_handler) + app.logger.setLevel(logging.INFO) + app.logger.info("Recipe example startup") + app.config.update(dict(PREFERRED_URL_SCHEME="https")) diff --git a/app/admin/examples/eg013_create_account.py b/app/admin/examples/eg013_create_account.py new file mode 100644 index 00000000..c57208fd --- /dev/null +++ b/app/admin/examples/eg013_create_account.py @@ -0,0 +1,89 @@ +from docusign_admin import ApiClient, ProvisionAssetGroupApi, SubAccountCreateRequest, \ + SubAccountCreateRequestSubAccountCreationSubscription, \ + SubAccountCreateRequestSubAccountCreationTargetAccountDetails, \ + SubAccountCreateRequestSubAccountCreationTargetAccountAdmin +from flask import session, request + +from ..utils import get_organization_id +from ...ds_config import DS_CONFIG + + +class Eg013CreateAccountController: + @staticmethod + def get_args(): + """Get required session and request arguments""" + organization_id = get_organization_id() + + return { + "access_token": session["ds_access_token"], # Represents your {ACCESS_TOKEN} + "organization_id": organization_id, + "base_path": DS_CONFIG["admin_api_client_host"], + "email": request.form.get("email"), + "first_name": request.form.get("first_name"), + "last_name": request.form.get("last_name"), + "subscription_id": session.get("subscription_id"), + "plan_id": session.get("plan_id"), + } + + @staticmethod + def worker(args): + """ + 1. Create an API client with headers + 2. Get the list of eligible accounts + 3. Construct the request body + 4. Create the account + """ + + access_token = args["access_token"] + + # Create an API client with headers + #ds-snippet-start:Admin13Step2 + api_client = ApiClient(host=args["base_path"]) + api_client.set_default_header( + header_name="Authorization", + header_value=f"Bearer {access_token}" + ) + #ds-snippet-end:Admin13Step2 + + #ds-snippet-start:Admin13Step4 + account_data = SubAccountCreateRequest( + subscription_details=SubAccountCreateRequestSubAccountCreationSubscription( + id=args["subscription_id"], + plan_id=args["plan_id"], + modules=[] + ), + target_account=SubAccountCreateRequestSubAccountCreationTargetAccountDetails( + name="CreatedThroughAPI", + country_code="US", + admin=SubAccountCreateRequestSubAccountCreationTargetAccountAdmin( + email=args["email"], + first_name=args["first_name"], + last_name=args["last_name"], + locale="en" + ) + ) + ) + #ds-snippet-end:Admin13Step4 + + #ds-snippet-start:Admin13Step5 + asset_group_api = ProvisionAssetGroupApi(api_client=api_client) + results = asset_group_api.create_asset_group_account(args["organization_id"], account_data) + #ds-snippet-end:Admin13Step5 + + return results + + @staticmethod + def get_organization_plan_items(args): + access_token = args["access_token"] + api_client = ApiClient(host=args["base_path"]) + api_client.set_default_header( + header_name="Authorization", + header_value=f"Bearer {access_token}" + ) + + #ds-snippet-start:Admin13Step3 + asset_group_api = ProvisionAssetGroupApi(api_client=api_client) + plan_items = asset_group_api.get_organization_plan_items(args["organization_id"]) + #ds-snippet-end:Admin13Step3 + + return plan_items diff --git a/app/admin/views/__init__.py b/app/admin/views/__init__.py index 89766a61..a9230335 100644 --- a/app/admin/views/__init__.py +++ b/app/admin/views/__init__.py @@ -1,12 +1,13 @@ -from .eg001_create_a_new_user import aeg001 -from .eg002_create_active_clm_esign_user import aeg002 -from .eg003_bulk_export_user_data import aeg003 -from .eg004_add_users_via_bulk_import import aeg004 -from .eg005_audit_users import aeg005 -from .eg006_get_user_profile_by_email import aeg006 -from .eg007_get_user_profile_by_user_id import aeg007 -from .eg008_update_user_product_permission_profile import aeg008 -from .eg009_delete_user_product_permission_profile import aeg009 -from .eg010_delete_user_data_from_organization import aeg010 -from .eg011_delete_user_data_from_account import aeg011 -from .eg012_clone_account import aeg012 +from .eg001_create_a_new_user import aeg001 +from .eg002_create_active_clm_esign_user import aeg002 +from .eg003_bulk_export_user_data import aeg003 +from .eg004_add_users_via_bulk_import import aeg004 +from .eg005_audit_users import aeg005 +from .eg006_get_user_profile_by_email import aeg006 +from .eg007_get_user_profile_by_user_id import aeg007 +from .eg008_update_user_product_permission_profile import aeg008 +from .eg009_delete_user_product_permission_profile import aeg009 +from .eg010_delete_user_data_from_organization import aeg010 +from .eg011_delete_user_data_from_account import aeg011 +from .eg012_clone_account import aeg012 +from .eg013_create_account import aeg013 diff --git a/app/admin/views/eg013_create_account.py b/app/admin/views/eg013_create_account.py new file mode 100644 index 00000000..0be7df3e --- /dev/null +++ b/app/admin/views/eg013_create_account.py @@ -0,0 +1,72 @@ +"""Example 013: How to create an account. """ + +import json + +from docusign_admin.client.api_exception import ApiException +from flask import Blueprint, render_template, session + +from app.docusign import authenticate, ensure_manifest, get_example_by_number +from app.error_handlers import process_error +from ..examples.eg013_create_account import Eg013CreateAccountController +from ...ds_config import DS_CONFIG +from ...consts import API_TYPE + +example_number = 13 +api = API_TYPE["ADMIN"] +eg = f"aeg0{example_number}" # Reference (and URL) for this example +aeg013 = Blueprint(eg, __name__) + + +@aeg013.route(f"/{eg}", methods=["POST"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def create_account(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Render the response + """ + example = get_example_by_number(session["manifest"], example_number, api) + + # 1. Get required arguments + args = Eg013CreateAccountController.get_args() + try: + # 2. Call the worker method to create an account + results = Eg013CreateAccountController.worker(args) + except ApiException as err: + return process_error(err) + + return render_template( + "example_done.html", + title=example["ExampleName"], + message=example["ResultsPageText"], + json=json.dumps(json.dumps(results.to_dict(), default=str)) + ) + + +@aeg013.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) + + args = Eg013CreateAccountController.get_args() + + try: + plan_items = Eg013CreateAccountController.get_organization_plan_items(args) + session["subscription_id"] = plan_items[0].subscription_id + session["plan_id"] = plan_items[0].plan_id + + except ApiException as err: + process_error(err) + + return render_template( + "admin/eg013_create_account.html", + title=example["ExampleName"], + example=example, + source_file="eg013_create_account.py", + source_url=DS_CONFIG["admin_github_url"] + "eg013_create_account.py", + documentation=DS_CONFIG["documentation"] + eg + ) + diff --git a/app/api_type.py b/app/api_type.py index 94420987..0bc16fb5 100644 --- a/app/api_type.py +++ b/app/api_type.py @@ -1 +1 @@ -EXAMPLES_API_TYPE ={'Rooms': False, 'ESignature': True, 'Click': False, 'Monitor': False, 'Admin': False} \ No newline at end of file +EXAMPLES_API_TYPE ={'Rooms': False, 'ESignature': True, 'Click': False, 'Monitor': False, 'Admin': False, 'Notary': False} \ No newline at end of file diff --git a/app/connected_fields/examples/eg001_set_connected_fields.py b/app/connected_fields/examples/eg001_set_connected_fields.py new file mode 100644 index 00000000..ece80005 --- /dev/null +++ b/app/connected_fields/examples/eg001_set_connected_fields.py @@ -0,0 +1,235 @@ +import base64 +import requests +from os import path + +from docusign_esign import EnvelopesApi, Text, Document, Signer, EnvelopeDefinition, SignHere, Tabs, \ + Recipients +from flask import session, request + +from ...consts import demo_docs_path, pattern +from ...docusign import create_api_client +from ...ds_config import DS_CONFIG + + +class Eg001SetConnectedFieldsController: + @staticmethod + def get_args(): + """Get request and session arguments""" + # Parse request arguments + signer_email = pattern.sub("", request.form.get("signer_email")) + signer_name = pattern.sub("", request.form.get("signer_name")) + selected_app_id = pattern.sub("", request.form.get("app_id")) + envelope_args = { + "signer_email": signer_email, + "signer_name": signer_name, + } + args = { + "account_id": session["ds_account_id"], + "base_path": session["ds_base_path"], + "access_token": session["ds_access_token"], + "selected_app_id": selected_app_id, + "envelope_args": envelope_args + } + return args + + @staticmethod + def get_tab_groups(args): + """ + 1. Get the list of tab groups + 2. Filter by action contract and tab label + 3. Create a list of unique apps + """ + + #ds-snippet-start:ConnectedFields1Step2 + headers = { + "Authorization": "Bearer " + args['access_token'], + "Accept": "application/json", + "Content-Type": "application/json" + } + #ds-snippet-end:ConnectedFields1Step2 + + #ds-snippet-start:ConnectedFields1Step3 + url = f"{args['base_path']}/v1/accounts/{args['account_id']}/connected-fields/tab-groups" + + response = requests.get(url, headers=headers) + response_data = response.json() + + filtered_apps = list( + app for app in response_data + if any( + ("extensionData" in tab and "actionContract" in tab["extensionData"] and "Verify" in tab["extensionData"]["actionContract"]) or + ("tabLabel" in tab and "connecteddata" in tab["tabLabel"]) + for tab in app.get("tabs", []) + ) + ) + + unique_apps = list({app['appId']: app for app in filtered_apps}.values()) + #ds-snippet-end:ConnectedFields1Step3 + + return unique_apps + + @staticmethod + #ds-snippet-start:ConnectedFields1Step4 + def extract_verification_data(selected_app_id, tab): + extension_data = tab["extensionData"] + + return { + "app_id": selected_app_id, + "extension_group_id": extension_data["extensionGroupId"] if "extensionGroupId" in extension_data else "", + "publisher_name": extension_data["publisherName"] if "publisherName" in extension_data else "", + "application_name": extension_data["applicationName"] if "applicationName" in extension_data else "", + "action_name": extension_data["actionName"] if "actionName" in extension_data else "", + "action_input_key": extension_data["actionInputKey"] if "actionInputKey" in extension_data else "", + "action_contract": extension_data["actionContract"] if "actionContract" in extension_data else "", + "extension_name": extension_data["extensionName"] if "extensionName" in extension_data else "", + "extension_contract": extension_data["extensionContract"] if "extensionContract" in extension_data else "", + "required_for_extension": extension_data["requiredForExtension"] if "requiredForExtension" in extension_data else "", + "tab_label": tab["tabLabel"], + "connection_key": ( + extension_data["connectionInstances"][0]["connectionKey"] + if "connectionInstances" in extension_data and extension_data["connectionInstances"] + else "" + ), + "connection_value": ( + extension_data["connectionInstances"][0]["connectionValue"] + if "connectionInstances" in extension_data and extension_data["connectionInstances"] + else "" + ), + } + #ds-snippet-end:ConnectedFields1Step4 + + @classmethod + def send_envelope(cls, args, app): + """ + 1. Create the envelope request object + 2. Send the envelope + 3. Obtain the envelope_id + """ + #ds-snippet-start:ConnectedFields1Step6 + envelope_args = args["envelope_args"] + # Create the envelope request object + envelope_definition = cls.make_envelope(envelope_args, app) + + # Call Envelopes::create API method + # Exceptions will be caught by the calling function + api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"]) + + envelope_api = EnvelopesApi(api_client) + results = envelope_api.create_envelope(account_id=args["account_id"], envelope_definition=envelope_definition) + + envelope_id = results.envelope_id + #ds-snippet-end:ConnectedFields1Step6 + + return {"envelope_id": envelope_id} + + @classmethod + #ds-snippet-start:ConnectedFields1Step5 + def make_envelope(cls, args, app): + """ + Creates envelope + args -- parameters for the envelope: + signer_email, signer_name + returns an envelope definition + """ + + # document 1 (pdf) has tag /sn1/ + # + # The envelope has one recipient. + # recipient 1 - signer + with open(path.join(demo_docs_path, DS_CONFIG["doc_pdf"]), "rb") as file: + content_bytes = file.read() + base64_file_content = base64.b64encode(content_bytes).decode("ascii") + + # Create the document model + document = Document( # create the DocuSign document object + document_base64=base64_file_content, + name="Example document", # can be different from actual file name + file_extension="pdf", # many different document types are accepted + document_id=1 # a label used to reference the doc + ) + + # Create the signer recipient model + signer = Signer( + # The signer + email=args["signer_email"], + name=args["signer_name"], + recipient_id="1", + routing_order="1" + ) + + # Create a sign_here tab (field on the document) + sign_here = SignHere( + anchor_string="/sn1/", + anchor_units="pixels", + anchor_y_offset="10", + anchor_x_offset="20" + ) + + # Create text tabs (field on the document) + text_tabs = [] + for tab in (t for t in app["tabs"] if "SuggestionInput" not in t["tabLabel"]): + verification_data = cls.extract_verification_data(app["appId"], tab) + extension_data = cls.get_extension_data(verification_data) + + text_tab = { + "requireInitialOnSharedChange": False, + "requireAll": False, + "name": verification_data["application_name"], + "required": True, + "locked": False, + "disableAutoSize": False, + "maxLength": 4000, + "tabLabel": verification_data["tab_label"], + "font": "lucidaconsole", + "fontColor": "black", + "fontSize": "size9", + "documentId": "1", + "recipientId": "1", + "pageNumber": "1", + "xPosition": f"{70 + 100 * int(len(text_tabs) / 10)}", + "yPosition": f"{560 + 20 * (len(text_tabs) % 10)}", + "width": "84", + "height": "22", + "templateRequired": False, + "tabType": "text", + "tooltip": verification_data["action_input_key"], + "extensionData": extension_data + } + text_tabs.append(text_tab) + + # Add the tabs model (including the sign_here and text tabs) to the signer + # The Tabs object wants arrays of the different field/tab types + signer.tabs = Tabs(sign_here_tabs=[sign_here], text_tabs=text_tabs) + + # Next, create the top level envelope definition and populate it. + envelope_definition = EnvelopeDefinition( + email_subject="Please sign this document", + documents=[document], + # The Recipients object wants arrays for each recipient type + recipients=Recipients(signers=[signer]), + status="sent" # requests that the envelope be created and sent. + ) + + return envelope_definition + + def get_extension_data(verification_data): + return { + "extensionGroupId": verification_data["extension_group_id"], + "publisherName": verification_data["publisher_name"], + "applicationId": verification_data["app_id"], + "applicationName": verification_data["application_name"], + "actionName": verification_data["action_name"], + "actionContract": verification_data["action_contract"], + "extensionName": verification_data["extension_name"], + "extensionContract": verification_data["extension_contract"], + "requiredForExtension": verification_data["required_for_extension"], + "actionInputKey": verification_data["action_input_key"], + "extensionPolicy": 'MustVerifyToSign', + "connectionInstances": [ + { + "connectionKey": verification_data["connection_key"], + "connectionValue": verification_data["connection_value"], + } + ] + } + #ds-snippet-end:ConnectedFields1Step5 diff --git a/app/connected_fields/views/__init__.py b/app/connected_fields/views/__init__.py new file mode 100644 index 00000000..c0ffe6d2 --- /dev/null +++ b/app/connected_fields/views/__init__.py @@ -0,0 +1 @@ +from .eg001_set_connected_fields import feg001 \ No newline at end of file diff --git a/app/connected_fields/views/eg001_set_connected_fields.py b/app/connected_fields/views/eg001_set_connected_fields.py new file mode 100644 index 00000000..c4fe8ab5 --- /dev/null +++ b/app/connected_fields/views/eg001_set_connected_fields.py @@ -0,0 +1,83 @@ +"""Example 001: Set connected fields""" + +from docusign_esign.client.api_exception import ApiException +from flask import render_template, redirect, Blueprint, session + +from ..examples.eg001_set_connected_fields import Eg001SetConnectedFieldsController +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 = 1 +api = API_TYPE["CONNECTED_FIELDS"] +eg = f"feg00{example_number}" # reference (and url) for this example +feg001 = Blueprint(eg, __name__) + + +@feg001.route(f"/{eg}", methods=["POST"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def set_connected_fields(): + """ + 1. Get required arguments + 2. Call the worker method + """ + try: + # 1. Get required arguments + args = Eg001SetConnectedFieldsController.get_args() + # 2. Call the worker method + selected_app = next((app for app in session["apps"] if app["appId"] == args["selected_app_id"]), None) + results = Eg001SetConnectedFieldsController.send_envelope(args, selected_app) + except ApiException as err: + return process_error(err) + + session["envelope_id"] = results["envelope_id"] + + example = get_example_by_number(session["manifest"], example_number, api) + return render_template( + "example_done.html", + title=example["ExampleName"], + message=example["ResultsPageText"].format(results['envelope_id']) + ) + + +@feg001.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) + + args = { + "account_id": session["ds_account_id"], + "base_path": "https://api-d.docusign.com", + "access_token": session["ds_access_token"], + } + apps = Eg001SetConnectedFieldsController.get_tab_groups(args) + + if not apps or len(apps) == 0: + additional_page_data = next( + (p for p in example["AdditionalPage"] if p["Name"] == "no_verification_app"), + None + ) + + return render_template( + "example_done.html", + title=example["ExampleName"], + message=additional_page_data["ResultsPageText"] + ) + + session["apps"] = apps + return render_template( + "connected_fields/eg001_set_connected_fields.html", + title=example["ExampleName"], + example=example, + apps=apps, + source_file="eg001_set_connected_fields.py", + source_url=DS_CONFIG["github_example_url"] + "eg001_set_connected_fields.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + signer_name=DS_CONFIG["signer_name"], + signer_email=DS_CONFIG["signer_email"] + ) diff --git a/app/consts.py b/app/consts.py index 530ce453..bf24e8e8 100644 --- a/app/consts.py +++ b/app/consts.py @@ -22,7 +22,7 @@ # Name of static pdf file pdf_file = "World_Wide_Corp_lorem.pdf" -web_form_template_file = "World_Wide_Corp_Form.pdf" +web_form_template_file = "World_Wide_Corp_Web_Form.pdf" web_form_config_file = "web-form-config.json" @@ -116,6 +116,7 @@ "ROOMS": "Rooms", "ADMIN": "Admin", "CONNECT": "Connect", - "MAESTRO": "Maestro", - "WEBFORMS": "WebForms" + "WEBFORMS": "WebForms", + "NOTARY": "Notary", + "CONNECTED_FIELDS": "ConnectedFields" } diff --git a/app/docusign/ds_client.py b/app/docusign/ds_client.py index 348b4b44..a8408622 100644 --- a/app/docusign/ds_client.py +++ b/app/docusign/ds_client.py @@ -1,10 +1,14 @@ import uuid -from os import path +from os import path, urandom +import hashlib +import base64 +import secrets import requests -from flask import current_app as app, url_for, redirect, render_template, request, session +from flask import current_app as app, url_for, redirect, render_template, request, session, redirect from flask_oauthlib.client import OAuth +from requests_oauthlib import OAuth2Session from docusign_esign import ApiClient from docusign_esign.client.api_exception import ApiException @@ -30,17 +34,22 @@ ADMIN_SCOPES = [ "signature", "organization_read", "group_read", "permission_read", "user_read", "user_write", "account_read", "domain_read", "identity_provider_read", "impersonation", "user_data_redact", - "asset_group_account_read", "asset_group_account_clone_write", "asset_group_account_clone_read" -] - -MAESTRO_SCOPES = [ - "signature", "aow_manage" + "asset_group_account_read", "asset_group_account_clone_write", "asset_group_account_clone_read", + "organization_sub_account_write", "organization_sub_account_read" ] WEBFORMS_SCOPES = [ "signature", "webforms_read", "webforms_instance_read", "webforms_instance_write" ] +NOTARY_SCOPES = [ + "signature", "organization_read", "notary_read", "notary_write" +] + +CONNECTED_FIELDS = [ + "signature", "adm_store_unified_repo_read" +] + class DSClient: ds_app = None @@ -48,7 +57,10 @@ class DSClient: @classmethod def _init(cls, auth_type, api): if auth_type == "code_grant": - cls._auth_code_grant(api) + if session.get("pkce_failed", False): + cls._auth_code_grant(api) + else: + cls._pkce_auth(api) elif auth_type == "jwt": cls._jwt_auth(api) @@ -65,10 +77,12 @@ 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) + elif api == "Notary": + use_scopes.extend(NOTARY_SCOPES) + elif api == "ConnectedFields": + use_scopes.extend(CONNECTED_FIELDS) else: use_scopes.extend(SCOPES) # remove duplicate scopes @@ -92,6 +106,31 @@ def _auth_code_grant(cls, api): access_token_method="POST" ) + @classmethod + def _pkce_auth(cls, api): + """Authorize with the Authorization Code Grant - OAuth 2.0 flow""" + use_scopes = [] + + if api == "Rooms": + use_scopes.extend(ROOMS_SCOPES) + elif api == "Click": + use_scopes.extend(CLICK_SCOPES) + elif api == "Admin": + use_scopes.extend(ADMIN_SCOPES) + elif api == "WebForms": + use_scopes.extend(WEBFORMS_SCOPES) + elif api == "Notary": + use_scopes.extend(NOTARY_SCOPES) + elif api == "ConnectedFields": + use_scopes.extend(CONNECTED_FIELDS) + else: + use_scopes.extend(SCOPES) + # remove duplicate scopes + use_scopes = list(set(use_scopes)) + + redirect_uri = DS_CONFIG["app_url"] + url_for("ds.ds_callback") + cls.ds_app = OAuth2Session(DS_CONFIG["ds_client_id"], redirect_uri=redirect_uri, scope=use_scopes) + @classmethod def _jwt_auth(cls, api): """JSON Web Token authorization""" @@ -105,10 +144,10 @@ 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) + elif api == "Notary": + use_scopes.extend(NOTARY_SCOPES) else: use_scopes.extend(SCOPES) # remove duplicate scopes @@ -152,7 +191,13 @@ def destroy(cls): def login(cls, auth_type, api): cls._init(auth_type, api) if auth_type == "code_grant": - return cls.get(auth_type, api).authorize(callback=url_for("ds.ds_callback", _external=True)) + if session.get("pkce_failed", False): + return cls.get(auth_type, api).authorize(callback=url_for("ds.ds_callback", _external=True)) + else: + code_verifier = cls.generate_code_verifier() + code_challenge = cls.generate_code_challenge(code_verifier) + session["code_verifier"] = code_verifier + return redirect(cls.get_auth_url_with_pkce(code_challenge)) elif auth_type == "jwt": return cls._jwt_auth(api) @@ -160,7 +205,10 @@ def login(cls, auth_type, api): def get_token(cls, auth_type): resp = None if auth_type == "code_grant": - resp = cls.get(auth_type).authorized_response() + if session.get("pkce_failed", False): + resp = cls.get(auth_type).authorized_response() + else: + return cls.fetch_token_with_pkce(request.url) elif auth_type == "jwt": resp = cls.get(auth_type).to_dict() @@ -189,3 +237,44 @@ def get(cls, auth_type, api=API_TYPE["ESIGNATURE"]): if not cls.ds_app: cls._init(auth_type, api) return cls.ds_app + + @classmethod + def generate_code_verifier(cls): + # Generate a random 32-byte string and base64-url encode it + return secrets.token_urlsafe(32) + + @classmethod + def generate_code_challenge(cls, code_verifier): + # Hash the code verifier using SHA-256 + sha256_hash = hashlib.sha256(code_verifier.encode()).digest() + + # Base64 encode the hash and make it URL safe + base64_encoded = base64.urlsafe_b64encode(sha256_hash).decode().rstrip('=') + + return base64_encoded + + @classmethod + def get_auth_url_with_pkce(cls, code_challenge): + authorize_url = DS_CONFIG["authorization_server"] + "/oauth/auth" + auth_url, state = cls.ds_app.authorization_url( + authorize_url, + code_challenge=code_challenge, + code_challenge_method='S256', # PKCE uses SHA-256 hashing, + approval_prompt="auto" + ) + + return auth_url + + @classmethod + def fetch_token_with_pkce(cls, authorization_response): + access_token_url = DS_CONFIG["authorization_server"] + "/oauth/token" + token = cls.get("code_grant", session.get("api")).fetch_token( + access_token_url, + authorization_response=authorization_response, + client_id=DS_CONFIG["ds_client_id"], + client_secret=DS_CONFIG["ds_client_secret"], + code_verifier=session["code_verifier"], + code_challenge_method="S256" + ) + + return token diff --git a/app/docusign/views.py b/app/docusign/views.py index a0e26d40..d056de50 100644 --- a/app/docusign/views.py +++ b/app/docusign/views.py @@ -86,7 +86,14 @@ def ds_callback(): # Save the redirect eg if present redirect_url = session.pop("eg", None) - resp = DSClient.get_token(session["auth_type"]) + try: + resp = DSClient.get_token(session["auth_type"]) + except Exception as err: + if session.get("pkce_failed", False): + raise err + + session["pkce_failed"] = True + return redirect(url_for("ds.ds_login")) # app.logger.info("Authenticated with DocuSign.") session["ds_access_token"] = resp["access_token"] diff --git a/app/ds_config_sample.py b/app/ds_config_sample.py index cd9593f0..71899332 100644 --- a/app/ds_config_sample.py +++ b/app/ds_config_sample.py @@ -1,10 +1,10 @@ # ds_config.py # -# DocuSign configuration settings +# Docusign configuration settings DS_CONFIG = { - "ds_client_id": "{INTEGRATION_KEY_AUTH_CODE}", # The app's DocuSign integration key - "ds_client_secret": "{SECRET_KEY}", # The app's DocuSign integration key's secret + "ds_client_id": "{INTEGRATION_KEY_AUTH_CODE}", # The app's Docusign integration key + "ds_client_secret": "{SECRET_KEY}", # The app's Docusign integration key's secret "organization_id": "{ORGANIZATION_ID}", # A GUID value that identifies the organization "signer_email": "{SIGNER_EMAIL}", "signer_name": "{SIGNER_NAME}", @@ -16,11 +16,10 @@ "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", + "webforms_api_client_host": "https://apps-d.docusign.com/api/webforms", "allow_silent_authentication": True, # a user can be silently authenticated if they have an # active login session on another tab of the same browser - "target_account_id": None, # Set if you want a specific DocuSign AccountId, + "target_account_id": None, # Set if you want a specific Docusign AccountId, # If None, the user's default account will be used. "demo_doc_path": "demo_documents", "doc_salary_docx": "World_Wide_Corp_salary.docx", diff --git a/app/eSignature/examples/eg011_embedded_sending.py b/app/eSignature/examples/eg011_embedded_sending.py index 9e438eb8..9e42084b 100644 --- a/app/eSignature/examples/eg011_embedded_sending.py +++ b/app/eSignature/examples/eg011_embedded_sending.py @@ -1,8 +1,9 @@ import base64 from os import path -from docusign_esign import EnvelopesApi, ReturnUrlRequest, EnvelopesApi, EnvelopeDefinition, \ - Document, Signer, CarbonCopy, SignHere, Tabs, Recipients +from docusign_esign import EnvelopesApi, EnvelopesApi, EnvelopeDefinition, \ + Document, Signer, CarbonCopy, SignHere, Tabs, Recipients, EnvelopeViewRequest, EnvelopeViewSettings, \ + EnvelopeViewRecipientSettings, EnvelopeViewDocumentSettings, EnvelopeViewTaggerSettings, EnvelopeViewTemplateSettings from flask import url_for, session, request from ...consts import pattern, demo_docs_path @@ -36,6 +37,7 @@ def get_args(): "access_token": session["ds_access_token"], "envelope_args": envelope_args, "ds_return_url": url_for("ds.ds_return", _external=True), + "starting_view": starting_view, } return args @@ -58,7 +60,7 @@ def worker(cls, args, doc_docx_path, doc_pdf_path): @classmethod #ds-snippet-start:eSign11Step3 def create_sender_view(cls, args, envelope_id): - view_request = ReturnUrlRequest(return_url=args["ds_return_url"]) + view_request = cls.make_envelope_view_request(args) # Exceptions will be caught by the calling function api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"]) @@ -66,15 +68,47 @@ def create_sender_view(cls, args, envelope_id): sender_view = envelope_api.create_sender_view( account_id=args["account_id"], envelope_id=envelope_id, - return_url_request=view_request + envelope_view_request=view_request ) # Switch to Recipient and Documents view if requested by the user url = sender_view.url - if args["starting_view"] == "recipient": - url = url.replace("send=1", "send=0") return url + + @classmethod + def make_envelope_view_request(cls, args): + view_request = EnvelopeViewRequest( + return_url=args["ds_return_url"], + view_access="envelope", + settings=EnvelopeViewSettings( + starting_screen=args["starting_view"], + send_button_action="send", + show_back_button="false", + back_button_action="previousPage", + show_header_actions="false", + show_discard_action="false", + lock_token="", + recipient_settings=EnvelopeViewRecipientSettings( + show_edit_recipients="false", + show_contacts_list="false" + ), + document_settings=EnvelopeViewDocumentSettings( + show_edit_documents="false", + show_edit_document_visibility="false", + show_edit_pages="false" + ), + tagger_settings=EnvelopeViewTaggerSettings( + palette_sections="default", + palette_default="custom" + ), + template_settings=EnvelopeViewTemplateSettings( + show_matching_templates_prompt="true" + ) + ) + ) + + return view_request #ds-snippet-end:eSign11Step3 @classmethod diff --git a/app/eSignature/examples/eg045_delete_restore_envelope.py b/app/eSignature/examples/eg045_delete_restore_envelope.py new file mode 100644 index 00000000..c7def3b6 --- /dev/null +++ b/app/eSignature/examples/eg045_delete_restore_envelope.py @@ -0,0 +1,48 @@ +from docusign_esign import FoldersApi, FoldersRequest + +from ...docusign import create_api_client + + +class Eg045DeleteRestoreEnvelopeController: + @staticmethod + def delete_envelope(args): + #ds-snippet-start:eSign45Step2 + api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"]) + folders_api = FoldersApi(api_client) + #ds-snippet-end:eSign45Step2 + + #ds-snippet-start:eSign45Step3 + folders_request = FoldersRequest( + envelope_ids=[args["envelope_id"]] + ) + #ds-snippet-end:eSign45Step3 + + #ds-snippet-start:eSign45Step4 + results = folders_api.move_envelopes(account_id=args["account_id"], folder_id=args["delete_folder_id"], folders_request=folders_request) + #ds-snippet-end:eSign45Step4 + return results + + @staticmethod + def move_envelope_to_folder(args): + api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"]) + folders_api = FoldersApi(api_client) + + #ds-snippet-start:eSign45Step6 + folders_request = FoldersRequest( + envelope_ids=[args["envelope_id"]], + from_folder_id=args["from_folder_id"] + ) + + results = folders_api.move_envelopes(account_id=args["account_id"], folder_id=args["folder_id"], folders_request=folders_request) + #ds-snippet-end:eSign45Step6 + return results + + @staticmethod + def get_folders(args): + api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"]) + folders_api = FoldersApi(api_client) + + #ds-snippet-start:eSign45Step5 + results = folders_api.list(account_id=args["account_id"]) + #ds-snippet-end:eSign45Step5 + return results diff --git a/app/eSignature/utils.py b/app/eSignature/utils.py new file mode 100644 index 00000000..b1d72d79 --- /dev/null +++ b/app/eSignature/utils.py @@ -0,0 +1,10 @@ +def get_folder_id_by_name(folders, folder_name): + for folder in folders: + if folder.name.lower() == folder_name.lower(): + return folder.folder_id + + subfolders = folder.folders + if subfolders is not None and len(subfolders) > 0: + folder_id = get_folder_id_by_name(subfolders, folder_name) + if folder_id is not None: + return folder_id \ No newline at end of file diff --git a/app/eSignature/views/__init__.py b/app/eSignature/views/__init__.py index 7fcacd6d..00c5cc90 100644 --- a/app/eSignature/views/__init__.py +++ b/app/eSignature/views/__init__.py @@ -41,3 +41,4 @@ from .eg042_document_generation import eg042 from .eg043_shared_access import eg043 from .eg044_focused_view import eg044 +from .eg045_delete_restore_envelope import eg045 diff --git a/app/eSignature/views/eg020_phone_authentication.py b/app/eSignature/views/eg020_phone_authentication.py index 28878a3b..4704f7b6 100644 --- a/app/eSignature/views/eg020_phone_authentication.py +++ b/app/eSignature/views/eg020_phone_authentication.py @@ -32,6 +32,13 @@ def phone_authentication(): # 1. Get required arguments args = Eg020PhoneAuthenticationController.get_args() + if args["envelope_args"]["signer_email"] == DS_CONFIG["signer_email"]: + return render_template( + "error.html", + error_code=400, + error_message=session["manifest"]["SupportingTexts"]["IdenticalEmailsNotAllowedErrorMessage"] + ) + try: # Step 2: Call the worker method for authenticating with phone results = Eg020PhoneAuthenticationController.worker(args) diff --git a/app/eSignature/views/eg022_kba_authentication.py b/app/eSignature/views/eg022_kba_authentication.py index c9887d3b..a9d5e021 100644 --- a/app/eSignature/views/eg022_kba_authentication.py +++ b/app/eSignature/views/eg022_kba_authentication.py @@ -31,6 +31,13 @@ def kba_authentication(): # 1. Get required arguments args = Eg022KBAAuthenticationController.get_args() + + if args["envelope_args"]["signer_email"] == DS_CONFIG["signer_email"]: + return render_template( + "error.html", + error_code=400, + error_message=session["manifest"]["SupportingTexts"]["IdenticalEmailsNotAllowedErrorMessage"] + ) try: # Step 2: Call the worker method for kba results = Eg022KBAAuthenticationController.worker(args) diff --git a/app/eSignature/views/eg023_idv_authentication.py b/app/eSignature/views/eg023_idv_authentication.py index 29f534a4..b9dfc2a6 100644 --- a/app/eSignature/views/eg023_idv_authentication.py +++ b/app/eSignature/views/eg023_idv_authentication.py @@ -31,6 +31,13 @@ def idv_authentication(): # 1. Get required data args = Eg023IDVAuthenticationController.get_args() + + if args["envelope_args"]["signer_email"] == DS_CONFIG["signer_email"]: + return render_template( + "error.html", + error_code=400, + error_message=session["manifest"]["SupportingTexts"]["IdenticalEmailsNotAllowedErrorMessage"] + ) try: # 2: Call the worker method for idv authentication results = Eg023IDVAuthenticationController.worker(args) diff --git a/app/eSignature/views/eg045_delete_restore_envelope.py b/app/eSignature/views/eg045_delete_restore_envelope.py new file mode 100644 index 00000000..34289069 --- /dev/null +++ b/app/eSignature/views/eg045_delete_restore_envelope.py @@ -0,0 +1,155 @@ +""" Example 045: Delete and undelete an Envelope """ + +from os import path + +from docusign_esign.client.api_exception import ApiException +from flask import render_template, session, Blueprint, request, redirect + +from ..examples.eg045_delete_restore_envelope import Eg045DeleteRestoreEnvelopeController +from ..utils import get_folder_id_by_name +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 pattern, API_TYPE + +example_number = 45 +api = API_TYPE["ESIGNATURE"] +eg = f"eg0{example_number}" # reference (and url) for this example +restore_endpoint = f"{eg}restore" +delete_folder_id = "recyclebin" +restore_folder_id = "sentitems" +eg045 = Blueprint(eg, __name__) + +@eg045.route(f"/{eg}", methods=["POST"]) +@authenticate(eg=eg, api=api) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +def delete_envelope(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Render success response + """ + + # 1. Get required arguments + args = { + "account_id": session["ds_account_id"], + "base_path": session["ds_base_path"], + "access_token": session["ds_access_token"], + "envelope_id": pattern.sub("", request.form.get("envelope_id")), + "delete_folder_id": delete_folder_id + } + try: + # 2. Call the worker method + Eg045DeleteRestoreEnvelopeController.delete_envelope(args) + except ApiException as err: + return process_error(err) + + session["envelope_id"] = args["envelope_id"] # Save for use by second part of example + + # 3. Render success response + example = get_example_by_number(session["manifest"], example_number, api) + additional_page_data = next( + (p for p in example["AdditionalPage"] if p["Name"] == "envelope_is_deleted"), + None + ) + return render_template( + "example_done.html", + title=example["ExampleName"], + message=additional_page_data["ResultsPageText"].format(args["envelope_id"]), + redirect_url=restore_endpoint + ) + +@eg045.route(f"/{restore_endpoint}", methods=["POST"]) +@authenticate(eg=eg, api=api) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +def restore_envelope(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Render success response + """ + + # 1. Get required arguments + folder_name = pattern.sub("", request.form.get("folder_name")) + args = { + "account_id": session["ds_account_id"], + "base_path": session["ds_base_path"], + "access_token": session["ds_access_token"], + "envelope_id": pattern.sub("", session.get("envelope_id")), + "from_folder_id": delete_folder_id + } + + example = get_example_by_number(session["manifest"], example_number, api) + try: + # 2. Call the worker method + folders = Eg045DeleteRestoreEnvelopeController.get_folders(args) + args["folder_id"] = get_folder_id_by_name(folders.folders, folder_name) + + if args["folder_id"] is None: + additional_page_data = next( + (p for p in example["AdditionalPage"] if p["Name"] == "folder_does_not_exist"), + None + ) + + return render_template( + "example_done.html", + title=example["ExampleName"], + message=additional_page_data["ResultsPageText"].format(folder_name), + redirect_url=restore_endpoint + ) + + Eg045DeleteRestoreEnvelopeController.move_envelope_to_folder(args) + except ApiException as err: + return process_error(err) + + # 3. Render success response with envelopeId + return render_template( + "example_done.html", + title=example["ExampleName"], + message=example["ResultsPageText"].format(session.get("envelope_id", ""), args["folder_id"], folder_name) + ) + +@eg045.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) + + return render_template( + "eSignature/eg045_delete_envelope.html", + title=example["ExampleName"], + example=example, + envelope_id=session.get("envelope_id", ""), + submit_button_text=session["manifest"]["SupportingTexts"]["HelpingTexts"]["SubmitButtonDeleteText"], + source_file="eg045_delete_restore_envelope.py", + source_url=DS_CONFIG["github_example_url"] + "eg045_delete_restore_envelope.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + signer_name=DS_CONFIG["signer_name"], + signer_email=DS_CONFIG["signer_email"] + ) + +@eg045.route(f"/{restore_endpoint}", methods=["GET"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def get_restore_view(): + """responds with the form for the example""" + example = get_example_by_number(session["manifest"], example_number, api) + + if not session.get("envelope_id"): + return redirect(eg) + + return render_template( + "eSignature/eg045_restore_envelope.html", + title=example["ExampleName"], + example=example, + envelope_id=session.get("envelope_id"), + submit_button_text=session["manifest"]["SupportingTexts"]["HelpingTexts"]["SubmitButtonRestoreText"], + source_file="eg045_delete_restore_envelope.py", + source_url=DS_CONFIG["github_example_url"] + "eg045_delete_restore_envelope.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + signer_name=DS_CONFIG["signer_name"], + signer_email=DS_CONFIG["signer_email"] + ) diff --git a/app/maestro/__init__.py b/app/maestro/__init__.py deleted file mode 100644 index 1697d73a..00000000 --- a/app/maestro/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 7a99cac2..00000000 --- a/app/maestro/examples/eg001_trigger_workflow.py +++ /dev/null @@ -1,75 +0,0 @@ -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 deleted file mode 100644 index 4425ef17..00000000 --- a/app/maestro/examples/eg002_cancel_workflow.py +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 4777f0a2..00000000 --- a/app/maestro/examples/eg003_get_workflow_status.py +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index a44125d6..00000000 --- a/app/maestro/utils.py +++ /dev/null @@ -1,584 +0,0 @@ -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 deleted file mode 100644 index 93520d8f..00000000 --- a/app/maestro/views/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 8df42bd2..00000000 --- a/app/maestro/views/eg001_trigger_workflow.py +++ /dev/null @@ -1,184 +0,0 @@ -"""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 deleted file mode 100644 index e5a86fa5..00000000 --- a/app/maestro/views/eg002_cancel_workflow.py +++ /dev/null @@ -1,103 +0,0 @@ -"""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 deleted file mode 100644 index 6a5cc378..00000000 --- a/app/maestro/views/eg003_get_workflow_status.py +++ /dev/null @@ -1,76 +0,0 @@ -"""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/monitor/examples/eg001_get_monitoring_data.py b/app/monitor/examples/eg001_get_monitoring_data.py index 59ce5b6c..9b9c30e5 100644 --- a/app/monitor/examples/eg001_get_monitoring_data.py +++ b/app/monitor/examples/eg001_get_monitoring_data.py @@ -1,5 +1,6 @@ from docusign_monitor import DataSetApi from flask import session +from datetime import datetime, timedelta, timezone from app.monitor.utils import create_monitor_api_client @@ -25,10 +26,11 @@ def worker(args): ) #ds-snippet-end:Monitor1Step2 #ds-snippet-start:Monitor1Step3 + cursor_date = datetime.now(timezone.utc).replace(year=datetime.now(timezone.utc).year - 1) dataset_api = DataSetApi(api_client=api_client) - cursor_value = '' - limit = 100 + cursor_value = cursor_date.strftime('%Y-%m-%dT00:00:00Z') + limit = 2000 function_results = [] complete = False diff --git a/app/notary/examples/eg004_send_with_third_party_notary.py b/app/notary/examples/eg004_send_with_third_party_notary.py new file mode 100644 index 00000000..f2c67ad9 --- /dev/null +++ b/app/notary/examples/eg004_send_with_third_party_notary.py @@ -0,0 +1,184 @@ +import base64 +from os import path + +from docusign_esign import EnvelopesApi, EnvelopeDefinition, Document, Signer, Notary, SignHere, Tabs, Recipients, \ + NotarySeal, NotaryRecipient, RecipientSignatureProvider, RecipientSignatureProviderOptions + +from ...consts import demo_docs_path, pattern +from ...jwt_helpers import create_api_client + + +class Eg004SendWithThirdPartyNotary: + + @classmethod + def worker(cls, args): + """ + 1. Create the envelope request object + 2. Send the envelope + """ + + envelope_args = args["envelope_args"] + # Create the envelope request object + envelope_definition = cls.make_envelope(envelope_args) + #ds-snippet-start:Notary4Step2 + api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"]) + envelopes_api = EnvelopesApi(api_client) + #ds-snippet-end:Notary4Step2 + + #ds-snippet-start:Notary4Step4 + results = envelopes_api.create_envelope(account_id=args["account_id"], envelope_definition=envelope_definition) + #ds-snippet-end:Notary4Step4 + + envelope_id = results.envelope_id + + return {"envelope_id": envelope_id} + + #ds-snippet-start:Notary4Step3 + @classmethod + def make_envelope(cls, args): + """ + Creates envelope + Document 1: An HTML document. + DocuSign will convert all of the documents to the PDF format. + The recipients" field tags are placed using anchor strings. + """ + + # document 1 (html) has sign here anchor tag **signature_1** + # + # The envelope has two recipients. + # recipient 1 - signer + # The envelope will be sent first to the signer. + + # create the envelope definition + env = EnvelopeDefinition( + email_subject="Please sign this document set" + ) + doc1_b64 = base64.b64encode(bytes(cls.create_document1(args), "utf-8")).decode("ascii") + + # Create the document models + document1 = Document( # create the DocuSign document object + document_base64=doc1_b64, + name="Order acknowledgement", # can be different from actual file name + file_extension="html", # many different document types are accepted + document_id="1" # a label used to reference the doc + ) + # The order in the docs array determines the order in the envelope + env.documents = [document1] + + # Create the signer recipient model + signer1 = Signer( + email=args["signer_email"], + name=args["signer_name"], + recipient_id="2", + routing_order="1", + client_user_id="1000", + notary_id="1" + ) + # routingOrder (lower means earlier) determines the order of deliveries + # to the recipients. Parallel routing order is supported by using the + # same integer as the order for two or more recipients. + + # Create signHere fields (also known as tabs) on the documents, + # We"re using anchor (autoPlace) positioning + # + # The DocuSign platform searches throughout your envelope"s + # documents for matching anchor strings. So the + # signHere2 tab will be used in both document 2 and 3 since they + # use the same anchor string for their "signer 1" tabs. + sign_here1 = SignHere( + document_id="1", + x_position="200", + y_position="235", + page_number="1" + ) + + sign_here2 = SignHere( + stamp_type="stamp", + document_id="1", + x_position="200", + y_position="150", + page_number="1" + + ) + + # Add the tabs model (including the sign_here tabs) to the signer + # The Tabs object wants arrays of the different field/tab types + signer1.tabs = Tabs(sign_here_tabs=[sign_here1, sign_here2]) + + notary_seal_tab = NotarySeal( + x_position = "300", + y_position = "235", + document_id = "1", + page_number = "1", + ) + + notary_sign_here = SignHere( + x_position = "300", + y_position = "150", + document_id = "1", + page_number = "1", + ) + + notary_tabs = Tabs( + sign_here_tabs = [notary_sign_here], + notary_seal_tabs = [ notary_seal_tab ], + ) + + recipient_signature_provider = RecipientSignatureProvider( + seal_documents_with_tabs_only = "false", + signature_provider_name = "ds_authority_idv", + signature_provider_options = RecipientSignatureProviderOptions() + ) + + notary_recipient = NotaryRecipient( + name = "Notary", + recipient_id = "1", + routing_order = "1", + tabs = notary_tabs, + notary_type = "remote", + notary_source_type = "thirdparty", + notary_third_party_partner = "onenotary", + recipient_signature_providers = [recipient_signature_provider] + ) + + # Add the recipients to the envelope object + recipients = Recipients(signers=[signer1], notaries= [notary_recipient]) + env.recipients = recipients + + # Request that the envelope be sent by setting |status| to "sent". + # To request that the envelope be created as a draft, set to "created" + env.status = args["status"] + + return env + + @classmethod + def create_document1(cls, args): + """ Creates document 1 -- an html document""" + + return f""" + + + + + + +

World Wide Corp

+

Order Processing Division

+

Ordered by {args["signer_name"]}

+

Email: {args["signer_email"]}

+

+ Candy bonbon pastry jujubes lollipop wafer biscuit biscuit. Topping brownie sesame snaps sweet roll pie. + Croissant danish biscuit soufflé caramels jujubes jelly. Dragée danish caramels lemon drops dragée. + Gummi bears cupcake biscuit tiramisu sugar plum pastry. Dragée gummies applicake pudding liquorice. + Donut jujubes oat cake jelly-o. + Dessert bear claw chocolate cake gummies lollipop sugar plum ice cream gummies cheesecake. +

+ +

Agreed: **signature_1**/

+ + + """ + #ds-snippet-end:Notary4Step3 diff --git a/app/notary/views/__init__.py b/app/notary/views/__init__.py new file mode 100644 index 00000000..90384b69 --- /dev/null +++ b/app/notary/views/__init__.py @@ -0,0 +1 @@ +from .eg004_send_with_third_party_notary import neg004 \ No newline at end of file diff --git a/app/notary/views/eg004_send_with_third_party_notary.py b/app/notary/views/eg004_send_with_third_party_notary.py new file mode 100644 index 00000000..de3efbcc --- /dev/null +++ b/app/notary/views/eg004_send_with_third_party_notary.py @@ -0,0 +1,86 @@ +""" Example 004: Send envelope with third party Notary """ + +from os import path + +from docusign_esign.client.api_exception import ApiException +from flask import render_template, session, Blueprint, request + +from ..examples.eg004_send_with_third_party_notary import Eg004SendWithThirdPartyNotary +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 pattern, API_TYPE + +example_number = 4 +api = API_TYPE["NOTARY"] +eg = f"neg00{example_number}" # reference (and url) for this example +neg004 = Blueprint(eg, __name__) + +def get_args(): + """Get request and session arguments""" + + # More data validation would be a good idea here + # Strip anything other than characters listed + signer_email = pattern.sub("", request.form.get("signer_email")) + signer_name = pattern.sub("", request.form.get("signer_name")) + + envelope_args = { + "signer_email": signer_email, + "signer_name": signer_name, + "status": "sent", + } + args = { + "account_id": session["ds_account_id"], + "base_path": session["ds_base_path"], + "access_token": session["ds_access_token"], + "envelope_args": envelope_args + } + return args + +@neg004.route(f"/{eg}", methods=["POST"]) +@authenticate(eg=eg, api=api) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +def sign_by_email(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Render success response with envelopeId + """ + + # 1. Get required arguments + #args = Eg002SigningViaEmailController.get_args() + args = get_args() + try: + # 1. Call the worker method + results = Eg004SendWithThirdPartyNotary.worker(args) + except ApiException as err: + return process_error(err) + + session["envelope_id"] = results["envelope_id"] # Save for use by other examples which need an envelopeId + + # 2. Render success response with envelopeId + example = get_example_by_number(session["manifest"], example_number, api) + return render_template( + "example_done.html", + title=example["ExampleName"], + message=example["ResultsPageText"].format(results['envelope_id']) + ) + +@neg004.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) + + return render_template( + "notary/eg004_send_with_third_party_notary.html", + title=example["ExampleName"], + example=example, + source_file="eg004_send_with_third_party_notary.py", + source_url=DS_CONFIG["github_example_url"] + "eg004_send_with_third_party_notary.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + signer_name=DS_CONFIG["signer_name"], + signer_email=DS_CONFIG["signer_email"] + ) diff --git a/app/quick_acg/quick_acg_app/templates/base.html b/app/quick_acg/quick_acg_app/templates/base.html index a34c465e..6e1b71b6 100644 --- a/app/quick_acg/quick_acg_app/templates/base.html +++ b/app/quick_acg/quick_acg_app/templates/base.html @@ -15,7 +15,7 @@