esup_signature: add new-with-workflow endpoint (#77670) #251

Merged
ecazenave merged 3 commits from wip/77670-esup-workflow into main 2023-05-30 14:35:13 +02:00
2 changed files with 270 additions and 14 deletions

View File

@ -35,7 +35,7 @@ SIGN_REQUEST_SCHEMA = {
'title': '',
'description': '',
'type': 'object',
'required': ['file', 'recipients_emails', 'eppn'],
'required': ['file', 'recipients_emails', 'create_by_eppn'],
'unflatten': True,
'properties': collections.OrderedDict(
{
@ -62,13 +62,120 @@ SIGN_REQUEST_SCHEMA = {
'description': 'Recipients emails',
'items': {'type': 'string'},
},
'eppn': {'type': 'string', 'description': 'EPPN of the sign request owner'},
'recipients_cc_emails': {
'type': 'array',
'description': 'Recipients CC emails',
'items': {'type': 'string'},
},
'all_sign_to_complete': {
'type': 'string',
'description': 'Every recipient has to sign',
'enum': ['true', 'false'],

Est-ce que pour plus de clarté on n’a pas intérêt à décider d’une valeur par défaut pour tous ces paramètres booléens nouvellement ajoutés au schéma ?

Est-ce que pour plus de clarté on n’a pas intérêt à décider d’une valeur par défaut pour tous ces paramètres booléens nouvellement ajoutés au schéma ?

Fait.

Fait.
'default': 'false',
},
'user_sign_first': {
'type': 'string',
'description': 'the author must sign first',
'enum': ['true', 'false'],
'default': 'false',
},
'pending': {
'type': 'string',
'description': 'Pending',
'enum': ['true', 'false'],
'default': 'true',
},
'force_all_sign': {
'type': 'string',
'description': 'Force signing on every document',
'enum': ['true', 'false'],
'default': 'false',
},
'comment': {'type': 'string', 'description': 'Comment'},
'sign_type': {
'type': 'string',
'description': 'Signature type',
'enum': ['visa', 'pdfImageStamp', 'certSign', 'nexuSign'],
'default': 'pdfImageStamp',
},
'create_by_eppn': {'type': 'string', 'description': 'EPPN of the sign request owner'},
'title': {'type': 'string', 'description': 'Title'},
'target_url': {
'type': 'string',
'description': 'End location',
},
}
),
}

Typo ici (s/WTIH/WITH), à reporter dans le test aussi.

Typo ici (s/WTIH/WITH), à reporter dans le test aussi.

Corrigé.

Corrigé.
SIGN_REQUEST_WITH_WORKFLOW_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': '',
'description': '',
'type': 'object',
'required': ['file', 'eppn', 'workflow_id'],
'unflatten': True,
'properties': collections.OrderedDict(
{
'file': {
'type': 'object',
'description': 'File object',
'required': ['filename', 'content_type', 'content'],
'properties': {
'filename': {
'type': 'string',
},
'content_type': {
'type': 'string',
'description': 'MIME content-type',
},
'content': {
'type': 'string',
'description': 'Content, base64 encoded',
},
},
},
'recipients_emails': {
'type': 'array',
'description': 'Recipients emails at each step',
'items': {'type': 'string'},
},
'target_emails': {
'type': 'array',
'description': 'Target emails',
'items': {'type': 'string'},
},
'all_sign_to_completes': {
'type': 'array',
'description': 'Steps numbers were every recipient has to sign',
'items': {'type': 'string'},
},
'eppn': {'type': 'string', 'description': 'EPPN of the sign request owner'},
'workflow_id': {'type': 'string', 'description': 'Identifier of the workflow'},
'title': {'type': 'string', 'description': 'Title'},
'target_urls': {
'type': 'array',
'description': 'End locations',
'items': {'type': 'string'},
},
'signrequest_params_jsonstring': {
'type': 'string',
'description': 'Signature parameters',
},
}
),
}
def clean_list(some_list):
return [elem for elem in some_list if elem]
def to_bool(some_str):
return some_str == 'true'
class EsupSignature(BaseResource, HTTPResource):
base_url = models.URLField(_('API URL'))
@ -77,12 +184,12 @@ class EsupSignature(BaseResource, HTTPResource):
class Meta:
verbose_name = _('Esup Signature')
def _call(self, path, method='get', data=None, files=None, expect_json=True):
def _call(self, path, method='get', params=None, files=None, expect_json=True):
url = urllib.parse.urljoin(self.base_url, path)
kwargs = {}
if method == 'post':
kwargs['data'] = data
kwargs['params'] = params
kwargs['files'] = files
try:
@ -125,8 +232,18 @@ class EsupSignature(BaseResource, HTTPResource):
'recipients_emails/0': 'xx@foo.com',
'recipients_emails/1': 'yy@foo.com',
'recipients_emails/2': 'zz@foo.com',
'eppn': 'aa@foo.com',
'recipients_cc_emails/0': 'xx@foo.com',
'recipients_cc_emails/1': 'yy@foo.com',
'recipients_cc_emails/2': 'zz@foo.com',
'all_sign_to_complete': 'true',
'user_sign_first': 'false',
'pending': 'true',
'force_all_sign': 'false',
'comment': 'a comment',
'sign_type': 'pdfImageStamp',
'create_by_eppn': 'aa@foo.com',
'title': 'a title',
'target_url': 'smb://foo.bar/location-1/',
},
},
)
@ -143,16 +260,92 @@ class EsupSignature(BaseResource, HTTPResource):
)
}
recipients_emails = [email for email in post_data['recipients_emails'] if email]
data = {
'signType': 'pdfImageStamp',
'recipientsEmails': recipients_emails,
'eppn': post_data['eppn'],
params = {
'recipientsEmails': clean_list(post_data['recipients_emails']),
'recipientsCCEmails': clean_list(post_data.get('recipients_cc_emails', [])),
'comment': post_data.get('comment', ''),
'signType': post_data.get('sign_type', 'pdfImageStamp'),
'createByEppn': post_data['create_by_eppn'],
'title': post_data.get('title', ''),
'pending': True,
'targetUrl': post_data.get('target_url', ''),
}
return {'data': self._call('ws/signrequests/new', method='post', data=data, files=files)}
bool_params = {

Du détail, mais pas compris pourquoi la valeur par défaut est en dur ici et ne reprend pas celle définie dans le schéma.

Du détail, mais pas compris pourquoi la valeur par défaut est en dur ici et ne reprend pas celle définie dans le schéma.

Dans la norme JSON schema : "The default keyword specifies a default value. This value is not used to fill in missing values during the validation process."

Charge donc à l'application de "fournir" les valeurs par défaut.

Dans la norme JSON schema : "The default keyword specifies a default value. This value is not used to fill in missing values during the validation process." Charge donc à l'application de "fournir" les valeurs par défaut.

Cool, j’ignorais ça, merci pour la clarification.

Cool, j’ignorais ça, merci pour la clarification.
'all_sign_to_complete': ('allSignToComplete', False),
'user_sign_first': ('userSignFirst', False),
'pending': ('pending', True),
'force_all_sign': ('forceAllSign', False),
}
for key, value in bool_params.items():
ext_param, default = value
params[ext_param] = default
if key in post_data:
params[ext_param] = to_bool(post_data[key])
return {'data': self._call('ws/signrequests/new', method='post', params=params, files=files)}
@endpoint(
name='new-with-workflow',
description=_('Create a sign request'),
perm='can_access',
post={
'request_body': {
'schema': {
'application/json': SIGN_REQUEST_WITH_WORKFLOW_SCHEMA,
}
},
'input_example': {
'file': {
'filename': 'example-1.pdf',
'content_type': 'application/pdf',
'content': 'JVBERi0xL...(base64 PDF)...',
},
'workflow_id': '99',
'eppn': 'aa@foo.com',
'title': 'a title',
'recipients_emails/0': '0*xx@foo.com',
'recipients_emails/1': '0*yy@foo.com',
'recipients_emails/2': '1*zz@foo.com',
'all_sign_to_completes/0': '12',
'all_sign_to_completes/1': '13',
'target_emails/0': 'xx@foo.com',
'target_emails/1': 'yy@foo.com',
'target_emails/2': 'zz@foo.com',
'signrequest_params_jsonstring': 'List [ OrderedMap { "xPos": 100, "yPos": 100, "signPageNumber": 1 }, '
'OrderedMap { "xPos": 200, "yPos": 200, "signPageNumber": 1 } ]',
'target_urls/0': 'smb://foo.bar/location-1/',
'target_urls/1': 'smb://foo.bar/location-2/',
},
},
)
def new_with_workflow(self, request, post_data):
try:
file_bytes = io.BytesIO(base64.b64decode(post_data['file']['content']))
except (TypeError, binascii.Error):
raise APIError("Can't decode file")
files = {
'multipartFiles': (
post_data['file']['filename'],
file_bytes,
post_data['file']['content_type'],
)
}
params = {
'createByEppn': post_data['eppn'],
'title': post_data.get('title', ''),
'recipientsEmails': clean_list(post_data.get('recipients_emails', [])),
'allSignToCompletes': clean_list(post_data.get('all_sign_to_completes', [])),
'targetEmails': clean_list(post_data.get('target_emails', [])),
'signRequestParamsJsonString': post_data.get('signrequest_params_jsonstring', ''),
'targetUrls': clean_list(post_data.get('target_urls', [])),
}
return {
'data': self._call(
'/ws/workflows/%s/new' % post_data['workflow_id'], method='post', params=params, files=files
)
}
@endpoint(
methods=['get'],

View File

@ -31,11 +31,26 @@ def test_new(app, connector):
},
'recipients_emails/0': 'foo@invalid',
'recipients_emails/1': 'bar@invalid',
'eppn': 'baz@invalid',
'create_by_eppn': 'baz@invalid',
'title': 'a title',
}
with responses.RequestsMock() as rsps:
rsps.post('https://esup-signature.invalid/ws/signrequests/new', status=200, json=9)
query_params = {
'recipientsEmails': ['foo@invalid', 'bar@invalid'],
'createByEppn': 'baz@invalid',
'title': 'a title',
'signType': 'pdfImageStamp',
'pending': True,
'allSignToComplete': False,
'userSignFirst': False,
'forceAllSign': False,
}
rsps.post(
'https://esup-signature.invalid/ws/signrequests/new',
match=[responses.matchers.query_param_matcher(query_params)],
status=200,
json=9,
)
resp = app.post_json('/esup-signature/esup-signature/new', params=params)
assert len(rsps.calls) == 1
assert rsps.calls[0].request.headers['Content-Type'].startswith('multipart/form-data')
@ -44,6 +59,54 @@ def test_new(app, connector):
assert json_resp['data'] == 9
def test_new_with_workflow(app, connector):
params = {
'file': {
'filename': 'bla',
'content': base64.b64encode(b'who what').decode(),
'content_type': 'text/plain',
},
'workflow_id': '99',
'eppn': 'aa@foo.com',
'title': 'a title',
'recipients_emails/0': '0*xx@foo.com',
'recipients_emails/1': '0*yy@foo.com',
'recipients_emails/2': '1*zz@foo.com',
'all_sign_to_completes/0': '12',
'all_sign_to_completes/1': '13',
'target_emails/0': 'xx@foo.com',
'target_emails/1': 'yy@foo.com',
'target_emails/2': 'zz@foo.com',
'signrequest_params_jsonstring': 'List [ OrderedMap { "xPos": 100, "yPos": 100, "signPageNumber": 1 }, '
'OrderedMap { "xPos": 200, "yPos": 200, "signPageNumber": 1 } ]',
'target_urls/0': 'smb://foo.bar/location-1/',
'target_urls/1': 'smb://foo.bar/location-2/',
}
with responses.RequestsMock() as rsps:
query_params = {
'createByEppn': 'aa@foo.com',
'title': 'a title',
'recipientsEmails': ['0*xx@foo.com', '0*yy@foo.com', '1*zz@foo.com'],
'allSignToCompletes': ['12', '13'],
'targetEmails': ['xx@foo.com', 'yy@foo.com', 'zz@foo.com'],
'signRequestParamsJsonString': 'List [ OrderedMap { "xPos": 100, "yPos": 100, "signPageNumber": 1 }, '
'OrderedMap { "xPos": 200, "yPos": 200, "signPageNumber": 1 } ]',
'targetUrls': ['smb://foo.bar/location-1/', 'smb://foo.bar/location-2/'],
}
rsps.post(
'https://esup-signature.invalid/ws/workflows/99/new',
match=[responses.matchers.query_param_matcher(query_params)],
status=200,
json=9,
)
resp = app.post_json('/esup-signature/esup-signature/new-with-workflow', params=params)
assert len(rsps.calls) == 1
assert rsps.calls[0].request.headers['Content-Type'].startswith('multipart/form-data')
json_resp = resp.json
assert json_resp['err'] == 0
assert json_resp['data'] == 9
def test_status(app, connector):
with responses.RequestsMock() as rsps:
rsps.get('https://esup-signature.invalid/ws/signrequests/1', status=200, json={'status': 'completed'})