wscall: make it possible to store response as attachment (#10559)

This commit is contained in:
Thomas NOËL 2016-04-14 15:47:11 +02:00
parent 53dbcb54ca
commit e1f9c6b4dc
3 changed files with 91 additions and 31 deletions

View File

@ -14,7 +14,8 @@ from wcs.roles import Role
from wcs.workflows import (Workflow, WorkflowStatusItem,
SendmailWorkflowStatusItem, SendSMSWorkflowStatusItem,
DisplayMessageWorkflowStatusItem,
AbortActionException, WorkflowCriticalityLevel)
AbortActionException, WorkflowCriticalityLevel,
AttachmentEvolutionPart)
from wcs.wf.anonymise import AnonymiseWorkflowStatusItem
from wcs.wf.criticality import ModifyCriticalityWorkflowStatusItem, MODE_INC, MODE_DEC, MODE_SET
from wcs.wf.dispatch import DispatchWorkflowStatusItem
@ -637,6 +638,24 @@ def test_webservice_call(pub):
assert 'xxx_error_response' not in formdata.workflow_data
formdata.workflow_data = None
# check storing response as attachment
item = WebserviceCallStatusItem()
item.url = 'http://remote.example.net/xml'
item.post = False
item.varname = 'xxx'
item.response_type = 'attachment'
item.record_errors = True
item.perform(formdata)
assert formdata.workflow_data.get('xxx_status') == 200
assert formdata.workflow_data.get('xxx_content_type') == 'text/xml'
attachment = formdata.evolution[-1].parts[-1]
assert isinstance(attachment, AttachmentEvolutionPart)
assert attachment.base_filename == 'xxx.xml'
assert attachment.content_type == 'text/xml'
attachment.fp.seek(0)
assert attachment.fp.read(5) == '<?xml'
formdata.workflow_data = None
item = WebserviceCallStatusItem()
item.url = 'http://remote.example.net/400-json'
item.post = False

View File

@ -224,23 +224,29 @@ class HttpRequestsMocking(object):
'headers': headers,
'timeout': timeout})
status, data = {
'http://remote.example.net/204': (204, None),
'http://remote.example.net/400-json': (400, '{"err": 1, "err_desc": ":("}'),
'http://remote.example.net/404': (404, 'page not found'),
'http://remote.example.net/404-json': (404, '{"err": 1}'),
'http://remote.example.net/500': (500, 'internal server error'),
'http://remote.example.net/json': (200, '{"foo": "bar"}'),
'http://remote.example.net/xml': (200, '<?xml version="1.0"><foo/>'),
}.get(url, (200, ''))
status, data, headers = {
'http://remote.example.net/204': (204, None, None),
'http://remote.example.net/400-json': (400, '{"err": 1, "err_desc": ":("}', None),
'http://remote.example.net/404': (404, 'page not found', None),
'http://remote.example.net/404-json': (404, '{"err": 1}', None),
'http://remote.example.net/500': (500, 'internal server error', None),
'http://remote.example.net/json': (200, '{"foo": "bar"}', None),
'http://remote.example.net/xml': (200, '<?xml version="1.0"><foo/>',
{'content-type': 'text/xml'}),
}.get(url, (200, '', {}))
class FakeResponse(object):
def __init__(self, status, data):
def __init__(self, status, data, headers):
self.status = status
self.reason = 'whatever'
self.data = data
self.headers = headers or {}
self.length = len(data or '')
return FakeResponse(status, data), status, data, None
def getheader(self, header):
return self.headers.get(header, None)
return FakeResponse(status, data, headers), status, data, None
def get_last(self, attribute):
return self.requests[-1][attribute]

View File

@ -20,6 +20,8 @@ import sys
import traceback
import xml.etree.ElementTree as ET
import collections
import mimetypes
from StringIO import StringIO
from quixote.html import TemplateIO, htmltext
from qommon.errors import ConnectionError
@ -27,7 +29,7 @@ from qommon.form import *
from qommon.misc import (http_get_page, http_post_request, get_variadic_url,
JSONEncoder, json_loads, site_encode)
from wcs.workflows import (WorkflowStatusItem, register_item_class,
template_on_formdata, AbortActionException)
template_on_formdata, AbortActionException, AttachmentEvolutionPart)
from wcs.api import sign_url
TIMEOUT = 30
@ -90,6 +92,7 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
request_signature_key = None
post_data = None
_method = None
response_type = 'json'
action_on_4xx = ':stop'
action_on_5xx = ':stop'
@ -120,7 +123,7 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
return ('url', 'post', 'varname', 'request_signature_key', 'post_data',
'action_on_4xx', 'action_on_5xx', 'action_on_bad_data',
'action_on_network_errors', 'notify_on_errors',
'record_errors', 'label', 'method')
'record_errors', 'label', 'method', 'response_type')
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
if 'label' in parameters:
@ -161,6 +164,14 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
})
form.widgets.append(HtmlWidget(htmltext('<h3>%s</h3>') % _('Response')))
response_types = collections.OrderedDict(
[('json', _('JSON')), ('attachment', _('Attachment'))])
if 'response_type' in parameters:
form.add(RadiobuttonsWidget, '%sresponse_type' % prefix,
title=_('Response Type'),
options=response_types.items(),
value=self.response_type,
attrs={'data-dynamic-display-parent': 'true'})
if 'varname' in parameters:
form.add(VarnameWidget, '%svarname' % prefix,
title=_('Variable Name'), value=self.varname)
@ -169,10 +180,17 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
error_actions = [(':stop', _('Stop')), (':pass', _('Ignore'))]
error_actions.extend([(x.id, _('Jump to %s') % x.name) for x in
self.parent.parent.possible_status])
for attribute in ('action_on_4xx', 'action_on_5xx', 'action_on_bad_data',
'action_on_network_errors'):
for attribute in ('action_on_4xx', 'action_on_5xx', 'action_on_network_errors',
'action_on_bad_data'):
if not attribute in parameters:
continue
if attribute == 'action_on_bad_data':
attrs = {
'data-dynamic-display-child-of': '%sresponse_type' % prefix,
'data-dynamic-display-value': response_types.get('json'),
}
else:
attrs = {}
label = {
'action_on_4xx': _('Action on HTTP error 4xx'),
'action_on_5xx': _('Action on HTTP error 5xx'),
@ -182,7 +200,7 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
form.add(SingleSelectWidget, '%s%s' % (prefix, attribute),
title=label,
value=getattr(self, attribute),
options=error_actions)
options=error_actions, attrs=attrs)
if 'notify_on_errors' in parameters:
form.add(CheckboxWidget, '%snotify_on_errors' % prefix,
@ -238,7 +256,7 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
# bytes, to match a country 512kbps DSL line.
timeout = TIMEOUT
timeout += len(post_data) / 65536
response, status, data, authheader = http_post_request(
response, status, data, auth_header = http_post_request(
url, post_data, headers=headers, timeout=timeout)
else:
response, status, data, auth_header = http_get_page(
@ -256,19 +274,7 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
if status in (204, 205):
pass # not returning any content
elif (status // 100) == 2:
try:
d = json_loads(data)
except (ValueError, TypeError) as e:
formdata.update_workflow_data(workflow_data)
formdata.store()
self.action_on_error(self.action_on_bad_data, formdata,
response, data=data, exc_info=sys.exc_info())
else:
workflow_data['%s_response' % self.varname] = d
if isinstance(d.get('data'), dict) and d['data'].get('display_id'):
formdata.id_display = d.get('data', {}).get('display_id')
elif d.get('display_id'):
formdata.id_display = d.get('display_id')
self.store_response(formdata, response, data, workflow_data)
else: # on error, record data if it is JSON
try:
d = json_loads(data)
@ -284,6 +290,35 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
if (status // 100) == 5:
self.action_on_error(self.action_on_5xx, formdata, response, data=data)
def store_response(self, formdata, response, data, workflow_data):
if self.response_type == 'json':
try:
d = json_loads(data)
except (ValueError, TypeError) as e:
formdata.update_workflow_data(workflow_data)
formdata.store()
self.action_on_error(self.action_on_bad_data, formdata,
response, data=data, exc_info=sys.exc_info())
else:
workflow_data['%s_response' % self.varname] = d
if isinstance(d.get('data'), dict) and d['data'].get('display_id'):
formdata.id_display = d.get('data', {}).get('display_id')
elif d.get('display_id'):
formdata.id_display = d.get('display_id')
else: # store result as attachment
content_type = response.getheader('content-type') or ''
if content_type:
content_type = content_type.split(';')[0].strip().lower()
workflow_data['%s_content_type' % self.varname] = content_type
workflow_data['%s_length' % self.varname] = len(data)
extension = mimetypes.guess_extension(content_type, strict=False) or ''
filename = '%s%s' % (self.varname, extension)
fp_content = StringIO(data)
attachment = AttachmentEvolutionPart(filename, fp_content,
content_type=content_type,
varname=self.varname)
formdata.evolution[-1].add_part(attachment)
def action_on_error(self, action, formdata, response=None, data=None, exc_info=None):
if action in (':pass', ':stop') and (self.notify_on_errors or self.record_errors):
if exc_info: