wscalls: unflatten payload when calling webservice (#66916) #1204
|
@ -305,3 +305,53 @@ def test_afterjobs_base_directory(pub):
|
|||
get_app(pub).get('/api/jobs/', status=403)
|
||||
# base directory is 404
|
||||
get_app(pub).get(sign_url('/api/jobs/?orig=coucou', '1234'), status=404)
|
||||
|
||||
|
||||
def test_preview_payload_structure(pub, admin_user):
|
||||
get_app(pub).get('/api/preview-payload-structure', status=403)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/api/preview-payload-structure')
|
||||
|
||||
assert resp.pyquery('div.payload-preview').length == 1
|
||||
assert '<h2>Payload structure preview</h2>' in resp.text
|
||||
assert resp.pyquery('div.payload-preview').text() == '{}'
|
||||
params = {
|
||||
'request$post_data$added_elements': 1,
|
||||
'request$post_data$element1key': 'user/first_name',
|
||||
'request$post_data$element1value$value_template': 'Foo',
|
||||
'request$post_data$element1value$value_python': '',
|
||||
'request$post_data$element2key': 'user/last_name',
|
||||
'request$post_data$element2value$value_template': 'Bar',
|
||||
'request$post_data$element2value$value_python': '',
|
||||
'request$post_data$element3key': 'user/0',
|
||||
}
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
assert resp.pyquery('div.payload-preview div.errornotice').length == 0
|
||||
assert resp.pyquery('div.payload-preview').text() == '{"user": {"first_name": "Foo","last_name": "Bar"}}'
|
||||
params.update(
|
||||
{
|
||||
'request$post_data$element3value$value_template': 'value',
|
||||
'request$post_data$element3value$value_python': '',
|
||||
}
|
||||
)
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
|
||||
assert resp.pyquery('div.payload-preview div.errornotice').length == 1
|
||||
assert 'Unable to preview payload' in resp.pyquery('div.payload-preview div.errornotice').text()
|
||||
assert (
|
||||
'Following error occured: there is a mix between lists and dicts'
|
||||
in resp.pyquery('div.payload-preview div.errornotice').text()
|
||||
)
|
||||
|
||||
params = {
|
||||
'post_data$element1key': '0/0',
|
||||
'post_data$element1value$value_template': 'Foo',
|
||||
'post_data$element1value$value_python': '',
|
||||
'post_data$element2key': '0/1',
|
||||
'post_data$element2value$value_template': '{{ form_name }}',
|
||||
'post_data$element2value$value_python': '',
|
||||
'post_data$element3key': '1/0',
|
||||
'post_data$element3value$value_template': '',
|
||||
}
|
||||
resp = app.get('/api/preview-payload-structure', params=params)
|
||||
assert resp.pyquery('div.payload-preview').text() == '[["Foo",{{ form_name }}],[""]]'
|
||||
|
|
|
@ -289,6 +289,47 @@ def test_webservice_empty_param_values(http_requests, pub):
|
|||
assert http_requests.get_last('body') == '{"toto": ""}'
|
||||
|
||||
|
||||
def test_webservice_with_unflattened_payload_keys(http_requests, pub):
|
||||
NamedWsCall.wipe()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello world'
|
||||
wscall.request = {
|
||||
'method': 'POST',
|
||||
'url': 'http://remote.example.net/json',
|
||||
'post_data': {'foo/0': 'first', 'foo/1': 'second', 'bar': 'example', 'foo/2': ''},
|
||||
}
|
||||
wscall.store()
|
||||
|
||||
wscall.call()
|
||||
assert http_requests.get_last('url') == 'http://remote.example.net/json'
|
||||
assert http_requests.get_last('body') == '{"bar": "example", "foo": ["first", "second", ""]}'
|
||||
assert http_requests.count() == 1
|
||||
|
||||
wscall.request = {
|
||||
'method': 'POST',
|
||||
'url': 'http://remote.example.net/json',
|
||||
'post_data': {'foo/0': 'first', 'foo/1': 'second', 'foo/bar': 'example'},
|
||||
}
|
||||
wscall.store()
|
||||
|
||||
pub.loggederror_class.wipe()
|
||||
http_requests.empty()
|
||||
smihai marked this conversation as resolved
Outdated
|
||||
wscall.call()
|
||||
assert http_requests.count() == 0
|
||||
smihai marked this conversation as resolved
Outdated
tnoel
commented
faire plutôt un http_requests.empty() avant de lancer le wscall, afin de faire un == 0 ici faire plutôt un http_requests.empty() avant de lancer le wscall, afin de faire un == 0 ici
smihai
commented
Ok, merci. Ok, merci.
|
||||
assert pub.loggederror_class.count() == 0
|
||||
|
||||
wscall.record_on_errors = True
|
||||
wscall.store()
|
||||
pub.loggederror_class.wipe()
|
||||
wscall.call()
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== '[WSCALL] Webservice call failure because unable to unflatten payload keys (there is a mix between lists and dicts)'
|
||||
)
|
||||
|
||||
|
||||
def test_webservice_timeout(http_requests, pub):
|
||||
NamedWsCall.wipe()
|
||||
|
||||
|
|
|
@ -574,6 +574,82 @@ def test_webservice_call(http_requests, pub):
|
|||
assert payload == {'one': 1, 'str': 'abcd', 'evalme': formdata.get_display_id()}
|
||||
|
||||
|
||||
def test_webservice_with_unflattened_payload_keys(http_requests, pub):
|
||||
wf = Workflow(name='wf1')
|
||||
wf.add_status('Status1', 'st1')
|
||||
wf.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'baz'
|
||||
formdef.fields = []
|
||||
formdef.workflow_id = wf.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.post_data = {
|
||||
'foo/0': 'first',
|
||||
'foo/1': 'second',
|
||||
'foo/2': '{{ form_name }}',
|
||||
'bar': 'example',
|
||||
'form//name': '{{ form_name }}',
|
||||
}
|
||||
pub.substitutions.feed(formdata)
|
||||
smihai marked this conversation as resolved
Outdated
tnoel
commented
même remarque sur le with pytest.raise et le http_requests.empty() même remarque sur le with pytest.raise et le http_requests.empty()
|
||||
item.perform(formdata)
|
||||
assert http_requests.count() == 1
|
||||
assert http_requests.get_last('url') == 'http://remote.example.net/'
|
||||
assert http_requests.get_last('method') == 'POST'
|
||||
payload = json.loads(http_requests.get_last('body'))
|
||||
assert payload == {'foo': ['first', 'second', 'baz'], 'bar': 'example', 'form/name': 'baz'}
|
||||
|
||||
http_requests.empty()
|
||||
pub.loggederror_class.wipe()
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.record_on_errors = True
|
||||
item.post_data = {'foo/1': 'first', 'foo/2': 'second'}
|
||||
|
||||
item.perform(formdata)
|
||||
assert http_requests.count() == 0
|
||||
assert pub.loggederror_class.count() == 1
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== '[WSCALL] Webservice call failure because unable to unflatten payload keys (incomplete array before key "foo/1")'
|
||||
)
|
||||
|
||||
http_requests.empty()
|
||||
pub.loggederror_class.wipe()
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.record_on_errors = True
|
||||
item.post_data = {'0/foo': 'value', '1/bar': 'value', 'name': '{{ form_name }}'}
|
||||
|
||||
item.perform(formdata)
|
||||
assert (
|
||||
pub.loggederror_class.select()[0].summary
|
||||
== '[WSCALL] Webservice call failure because unable to unflatten payload keys (there is a mix between lists and dicts)'
|
||||
)
|
||||
|
||||
http_requests.empty()
|
||||
pub.loggederror_class.wipe()
|
||||
item = WebserviceCallStatusItem()
|
||||
item.url = 'http://remote.example.net'
|
||||
item.record_on_errors = True
|
||||
item.post_data = {'0/foo': 'value', '1/bar': 'value'}
|
||||
|
||||
pub.substitutions.feed(formdata)
|
||||
item.perform(formdata)
|
||||
|
||||
assert http_requests.count() == 1
|
||||
payload = json.loads(http_requests.get_last('body'))
|
||||
assert payload == [{'foo': 'value'}, {'bar': 'value'}]
|
||||
|
||||
|
||||
def test_webservice_waitpoint(pub):
|
||||
item = WebserviceCallStatusItem()
|
||||
assert item.waitpoint
|
||||
|
|
|
@ -500,6 +500,7 @@ class WorkflowItemPage(Directory, DocumentableMixin):
|
|||
return redirect('..')
|
||||
|
||||
get_response().set_title('%s - %s' % (_('Workflow'), self.workflow.name))
|
||||
get_response().add_javascript(['jquery-ui.js'])
|
||||
context = {
|
||||
'view': self,
|
||||
'html_form': form,
|
||||
|
|
|
@ -203,6 +203,7 @@ class NamedWsCallPage(Directory, DocumentableMixin):
|
|||
return redirect('../%s/' % self.wscall.id)
|
||||
|
||||
get_response().breadcrumb.append(('edit', _('Edit')))
|
||||
get_response().add_javascript(['jquery-ui.js'])
|
||||
get_response().set_title(_('Edit webservice call'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Edit webservice call')
|
||||
|
|
78
wcs/api.py
|
@ -27,6 +27,7 @@ from django.utils.timezone import localtime, make_naive
|
|||
from quixote import get_publisher, get_request, get_response, get_session, redirect
|
||||
from quixote.directory import Directory
|
||||
from quixote.errors import MethodNotAllowedError, RequestError
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
import wcs.qommon.storage as st
|
||||
from wcs.admin.settings import UserFieldsFormDef
|
||||
|
@ -57,6 +58,7 @@ from wcs.sql_criterias import (
|
|||
StrictNotEqual,
|
||||
)
|
||||
from wcs.workflows import ContentSnapshotPart
|
||||
from wcs.wscalls import UnflattenKeysException, unflatten_keys
|
||||
|
||||
from .backoffice.data_management import CardPage as BackofficeCardPage
|
||||
from .backoffice.management import FormPage as BackofficeFormPage
|
||||
|
@ -1407,6 +1409,7 @@ class ApiDirectory(Directory):
|
|||
'geojson',
|
||||
'jobs',
|
||||
('card-file-by-token', 'card_file_by_token'),
|
||||
('preview-payload-structure', 'preview_payload_structure'),
|
||||
('sign-url-token', 'sign_url_token'),
|
||||
]
|
||||
|
||||
|
@ -1434,6 +1437,81 @@ class ApiDirectory(Directory):
|
|||
get_response().set_content_type('application/json')
|
||||
return json.dumps({'err': 0, 'data': list_roles})
|
||||
|
||||
def preview_payload_structure(self):
|
||||
if not (get_request().user and get_request().user.can_go_in_admin()):
|
||||
raise AccessForbiddenError('user has no access to backoffice')
|
||||
|
||||
def parse_payload():
|
||||
payload = {}
|
||||
for param, value in get_request().form.items():
|
||||
# skip elements which are not part of payload
|
||||
if 'post_data$element' not in param or param.endswith('value_python'):
|
||||
continue
|
||||
prefix, order, field = re.split(r'(\d)(?!\d)', param) # noqa pylint: disable=unused-variable
|
||||
# skip elements that aren't ordered
|
||||
if not order:
|
||||
continue
|
||||
|
||||
if order not in payload:
|
||||
payload[order] = []
|
||||
|
||||
if field == 'key':
|
||||
# skip empty keys
|
||||
if not value:
|
||||
continue
|
||||
# insert key on first position
|
||||
payload[order].insert(0, value)
|
||||
else:
|
||||
payload[order].append(value)
|
||||
return dict([v for v in payload.values() if len(v) > 1])
|
||||
|
||||
def format_payload(o, html=htmltext(''), last_element=True):
|
||||
if isinstance(o, (list, tuple)):
|
||||
html += htmltext('[<span class="payload-preview--obj">')
|
||||
while True:
|
||||
try:
|
||||
head, tail = o[0], o[1:]
|
||||
except IndexError:
|
||||
break
|
||||
html = format_payload(head, html=html, last_element=len(tail) < 1)
|
||||
o = tail
|
||||
html += htmltext('</span>]')
|
||||
elif isinstance(o, dict):
|
||||
html += htmltext('{<span class="payload-preview--obj">')
|
||||
for i, (k, v) in enumerate(o.items()):
|
||||
html += htmltext('<span class="payload-preview--key">"%s"</span>: ' % k)
|
||||
html = format_payload(v, html=html, last_element=i == len(o) - 1)
|
||||
html += htmltext('</span>}')
|
||||
else:
|
||||
# check if it's empty string, a template with text around or just text
|
||||
if not o or re.sub('^({[{|%]).+([%|}]})$', '', o):
|
||||
# and add double quotes
|
||||
html += htmltext('<span class="payload-preview--value">"%s"</span>' % o)
|
||||
else:
|
||||
html += htmltext('<span class="payload-preview--template-value">%s</span>' % o)
|
||||
# last element doesn't need separator
|
||||
if not last_element:
|
||||
html += htmltext('<span class="payload-preview--item-separator">,</span>')
|
||||
return html
|
||||
|
||||
payload = parse_payload()
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Payload structure preview')
|
||||
r += htmltext('<div class="payload-preview">')
|
||||
try:
|
||||
unflattened_payload = unflatten_keys(payload)
|
||||
r += htmltext('<div class="payload-preview--structure">')
|
||||
r += format_payload(unflattened_payload)
|
||||
r += htmltext('</div>')
|
||||
except UnflattenKeysException as e:
|
||||
r += htmltext('<div class="errornotice"><p>%s</p><p>%s %s</p></div>') % (
|
||||
_('Unable to preview payload.'),
|
||||
_('Following error occured: '),
|
||||
e,
|
||||
)
|
||||
r += htmltext('</div>')
|
||||
return r.getvalue()
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_request().is_json_marker = True
|
||||
return super()._q_traverse(path)
|
||||
|
|
|
@ -3332,3 +3332,21 @@ aside .bo-block.documentation {
|
|||
// always keep a bit of space, for documentation button
|
||||
max-width: calc(100% - 80px);
|
||||
}
|
||||
|
||||
.payload-preview {
|
||||
&--structure {
|
||||
font-family: monospace;
|
||||
line-height: 150%;
|
||||
}
|
||||
&--template-value {
|
||||
background: #ffc;
|
||||
}
|
||||
&--obj {
|
||||
margin-left: 1em;
|
||||
display: block;
|
||||
}
|
||||
&--item-separator::after {
|
||||
content: '\a'; // force line-break
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -304,6 +304,28 @@ $(function() {
|
|||
$window.trigger('scroll');
|
||||
}
|
||||
|
||||
$('div.WidgetDict[data-widget-name*="post_data"] input[type="text"]').on('change', function() {
|
||||
var $widget = $(this).parents('div.WidgetDict');
|
||||
var url = '/api/preview-payload-structure?' + $('input', $widget).serialize();
|
||||
var preview_button_id = 'payload-preview-button';
|
||||
var preview_button_selector = 'a#' + preview_button_id;
|
||||
if ($widget.find(preview_button_selector).length) {
|
||||
$widget.find(preview_button_selector).attr('href', url);
|
||||
} else {
|
||||
if ($(this).parents('div.dict-key').length < 0) return;
|
||||
if (!$(this).val().includes('/')) return;
|
||||
var eval_link = document.createElement('a');
|
||||
eval_link.setAttribute('rel', 'popup');
|
||||
eval_link.setAttribute('href', url);
|
||||
eval_link.setAttribute('id', preview_button_id);
|
||||
eval_link.setAttribute('class', 'pk-button');
|
||||
eval_link.setAttribute('data-selector', 'div.payload-preview');
|
||||
eval_link.setAttribute('data-title-selector', 'h2');
|
||||
eval_link.innerHTML = WCS_I18N.preview_payload_structure;
|
||||
$widget.append(eval_link);
|
||||
}
|
||||
}).trigger('change');
|
||||
|
||||
$('#inspect-test-tools form').on('submit', function() {
|
||||
var data = $(this).serialize();
|
||||
$.ajax({url: 'inspect-tool',
|
||||
|
|
|
@ -90,6 +90,7 @@ def i18n_js(request):
|
|||
'close': _('Close'),
|
||||
'email_domain_suggest': _('Did you want to write'),
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
Preview payload structure ? Preview payload structure ?
smihai
commented
En effet, c'est mieux. En effet, c'est mieux.
|
||||
'email_domain_fix': _('Apply fix'),
|
||||
'preview_payload_structure': _('Preview payload structure'),
|
||||
}
|
||||
return HttpResponse(
|
||||
'WCS_I18N = %s;\n' % json.dumps(strings, cls=misc.JSONEncoder), content_type='application/javascript'
|
||||
|
|
|
@ -35,7 +35,13 @@ from wcs.workflows import (
|
|||
WorkflowStatusItem,
|
||||
register_item_class,
|
||||
)
|
||||
from wcs.wscalls import PayloadError, call_webservice, get_app_error_code, record_wscall_error
|
||||
from wcs.wscalls import (
|
||||
PayloadError,
|
||||
UnflattenKeysException,
|
||||
call_webservice,
|
||||
get_app_error_code,
|
||||
record_wscall_error,
|
||||
)
|
||||
|
||||
from ..qommon import _, force_str, pgettext
|
||||
from ..qommon.errors import ConnectionError
|
||||
|
@ -298,6 +304,15 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
|
|||
]
|
||||
),
|
||||
},
|
||||
hint=_(
|
||||
'The / in parameter name allows to generate complex objects. '
|
||||
'Thus a parameter named "element/child" containing "value" will generate the payload '
|
||||
'"element": {"child": "value"}. If the subkey, i.e. "child", is an integer it will '
|
||||
'become a list index and two elements "element/0", "element/1" (indexes should '
|
||||
' start from zero) containing "value1" and "value2" will generate the payload '
|
||||
'"element": ["value1", "value2"]. It is possible to combine the two types, for '
|
||||
smihai marked this conversation as resolved
Outdated
tnoel
commented
Comme j'ai eu du support à faire ce matin sur le sujet, on pourrait ajouter un petit bout qui dit qu'en cas de element/0, element/1, le 0 est obligatoire. Genre ajouter un "index 0 is mandatory" : If the subkey, i.e. "child", is an integer it will become a list index and two elements "element/0", "element/1" containing "value1" and "value2" will generate the payload "element": ["value1", "value2"] (note: index 0 is mandatory). Bon, si c'est jugé trop lourdingue tu oublies, on essayera de faire préciser ça dans une doc, qui contiendra des exemples. Comme j'ai eu du support à faire ce matin sur le sujet, on pourrait ajouter un petit bout qui dit qu'en cas de element/0, element/1, le 0 est obligatoire. Genre ajouter un "index 0 is mandatory" :
_If the subkey, i.e. "child", is an integer it will become a list index and two elements "element/0", "element/1" containing "value1" and "value2" will generate the payload "element": ["value1", "value2"] (note: index 0 is mandatory)._
Bon, si c'est jugé trop lourdingue tu oublies, on essayera de faire préciser ça dans une doc, qui contiendra des exemples.
smihai
commented
On n'est pas à quelques mots près. J'ai rajouté la remarque sur les indexes. On n'est pas à quelques mots près. J'ai rajouté la remarque sur les indexes.
|
||||
'example "element/0/key1" to generate a list of objects.'
|
||||
),
|
||||
)
|
||||
|
||||
response_types = collections.OrderedDict([('json', _('JSON')), ('attachment', _('Attachment'))])
|
||||
|
@ -481,6 +496,17 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
|
|||
exception=e,
|
||||
)
|
||||
return
|
||||
except UnflattenKeysException as e:
|
||||
get_publisher().record_error(
|
||||
error_summary=e.get_summary(),
|
||||
exception=e,
|
||||
context='[WSCALL]',
|
||||
formdata=formdata,
|
||||
status_item=self,
|
||||
notify=self.notify_on_errors,
|
||||
record=self.record_on_errors,
|
||||
)
|
||||
return
|
||||
|
||||
app_error_code = get_app_error_code(response, data, self.response_type)
|
||||
app_error_code_header = response.headers.get('x-error-code')
|
||||
|
|
114
wcs/wscalls.py
|
@ -17,6 +17,7 @@
|
|||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
@ -52,6 +53,86 @@ class PayloadError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class UnflattenKeysException(Exception):
|
||||
def get_summary(self):
|
||||
return _('Webservice call failure because unable to unflatten payload keys (%s)') % self
|
||||
|
||||
|
||||
def unflatten_keys(d):
|
||||
"""Transform:
|
||||
|
||||
{"a/b/0/x": "1234"}
|
||||
smihai marked this conversation as resolved
Outdated
tnoel
commented
Et tout à coup je me demande si on ne veut pas, dès maintenant, prévoir la possibilité d'échappement ? Genre « // » ? Et tout à coup je me demande si on ne veut pas, dès maintenant, prévoir la possibilité d'échappement ? Genre « // » ?
fpeters
commented
C'est déjà géré, par cette partie :
il faudrait donc y ajouter un commentaire pour expliciter. C'est déjà géré, par cette partie :
```
elif isinstance(x, str):
return x.replace('//', '/')
```
il faudrait donc y ajouter un commentaire pour expliciter.
smihai
commented
Ok, j'ajoute un commentaire et un test. Ok, j'ajoute un commentaire et un test.
|
||||
|
||||
into:
|
||||
|
||||
{"a": {"b": [{"x": "1234"}]}}
|
||||
"""
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
Ce commentaire n'apporte pas grand chose. Ce commentaire n'apporte pas grand chose.
|
||||
if not isinstance(d, dict) or not d: # unflattening an empty dict has no sense
|
||||
return d
|
||||
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
Dans w.c.s. on a misc.is_ascii_digit() pour faire le is_number() de passerelle, c'est nécessaire parce que :
Dans w.c.s. on a misc.is_ascii_digit() pour faire le is_number() de passerelle, c'est nécessaire parce que :
```
>>> '²'.isnumeric()
True
>>> '൨'.isnumeric()
True
>>> 'Ⅱ'.isnumeric()
True
etc.
```
|
||||
def split_key(key):
|
||||
def map_key(x):
|
||||
if misc.is_ascii_digit(x):
|
||||
return int(x)
|
||||
elif isinstance(x, str):
|
||||
# allow / char escaping
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
On ne va pas rendre le séparateur paramétrable, la fonction sera plus lisible sans ça. (ça vaut quand même le coup d'expliciter l'objectif de la regex). On ne va pas rendre le séparateur paramétrable, la fonction sera plus lisible sans ça.
(ça vaut quand même le coup d'expliciter l'objectif de la regex).
smihai
commented
J'ai viré la regex car il n'y a pas besoin de l'utiliser pour découper la chaîne. J'ai viré la regex car il n'y a pas besoin de l'utiliser pour découper la chaîne.
|
||||
return x.replace('//', '/')
|
||||
return x
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
Tout le monde ne lit pas les regex facilement, un commentaire pour l'expliciter serait utile. Tout le monde ne lit pas les regex facilement, un commentaire pour l'expliciter serait utile.
|
||||
|
||||
# split key by single / only
|
||||
return [map_key(x) for x in re.split(r'(?<!/)/(?!/)', key)]
|
||||
|
||||
keys = [(split_key(key), key) for key in d]
|
||||
try:
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
a mix a mix
|
||||
keys.sort()
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
Le commentaire dit qu'e c'est parce qu'il y a un mélange mais l'exception dit que c'est parce qu'il y a un problème d'ordre ? Le commentaire dit qu'e c'est parce qu'il y a un mélange mais l'exception dit que c'est parce qu'il y a un problème d'ordre ?
smihai
commented
Corrigé le message de l'exception. Corrigé le message de l'exception.
|
||||
except TypeError:
|
||||
# sorting fail means that there is a mix between lists and dicts
|
||||
raise UnflattenKeysException(_('there is a mix between lists and dicts'))
|
||||
|
||||
def set_path(path, orig_key, d, value, i=0):
|
||||
assert path
|
||||
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
set the value set the value
|
||||
key, tail = path[i], path[i + 1 :]
|
||||
|
||||
if not tail: # end of path, set the value
|
||||
if isinstance(key, int):
|
||||
assert isinstance(d, list)
|
||||
if len(d) != key:
|
||||
raise UnflattenKeysException(_('incomplete array before key "%s"') % orig_key)
|
||||
d.append(value)
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
Je serais pour ici avoir un "return # end recursion", et enchainer la suite un nouveau d'indentation plus bas (retirer le else de la ligne suivante). Cela parce qu'à la lecture de la fonction je me suis perdu dans l'indentation et avec l'appel set_path() qui la termine je ne voyais pas comment la récursion s'arrêtait. Je serais pour ici avoir un "return # end recursion", et enchainer la suite un nouveau d'indentation plus bas (retirer le else de la ligne suivante).
Cela parce qu'à la lecture de la fonction je me suis perdu dans l'indentation et avec l'appel set_path() qui la termine je ne voyais pas comment la récursion s'arrêtait.
|
||||
else:
|
||||
assert isinstance(d, dict)
|
||||
d[key] = value
|
||||
return # end of recursion
|
||||
|
||||
new = [] if isinstance(tail[0], int) else {}
|
||||
|
||||
if isinstance(key, int):
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
C'est possible de réécrire le map sous forme de list comprehension ? (str(x) for x in in path[...]) C'est possible de réécrire le map sous forme de list comprehension ? (str(x) for x in in path[...])
|
||||
assert isinstance(d, list)
|
||||
if len(d) < key:
|
||||
raise UnflattenKeysException(
|
||||
_('incomplete array before %s in %s')
|
||||
% (('/'.join([str(x) for x in path[: i + 1]])), orig_key)
|
||||
)
|
||||
if len(d) == key:
|
||||
d.append(new)
|
||||
else:
|
||||
new = d[key]
|
||||
else:
|
||||
smihai marked this conversation as resolved
Outdated
fpeters
commented
Possibilité d'étendre le commentaire avec un rappel de ce que contiendrait keys ici ? Possibilité d'étendre le commentaire avec un rappel de ce que contiendrait keys ici ?
smihai
commented
Bonne remarque. Bonne remarque.
J'ai complété le test pour couvrir les 2 cas.
|
||||
new = d.setdefault(key, new)
|
||||
set_path(path, orig_key, new, value, i + 1)
|
||||
|
||||
# Is the first level an array (ie key is like "0/param") or a dict (key is like "param/0") ?
|
||||
if isinstance(keys[0][0][0], int):
|
||||
new = []
|
||||
else:
|
||||
new = {}
|
||||
for path, key in keys:
|
||||
value = d[key]
|
||||
set_path(path, key, new, value)
|
||||
return new
|
||||
|
||||
|
||||
def get_app_error_code(response, data, response_type):
|
||||
app_error_code = 0
|
||||
app_error_code_header = response.headers.get('x-error-code')
|
||||
|
@ -167,6 +248,7 @@ def call_webservice(
|
|||
else:
|
||||
if payload[key]:
|
||||
payload[key] = get_publisher().get_cached_complex_data(payload[key])
|
||||
payload = unflatten_keys(payload)
|
||||
|
||||
# if formdata has to be sent, it's the payload. If post_data exists,
|
||||
# it's added in formdata['extra']
|
||||
|
@ -366,13 +448,22 @@ class NamedWsCall(XmlStorableObject):
|
|||
if getattr(get_request(), 'disable_error_notifications', None) is True:
|
||||
notify_on_errors = False
|
||||
record_on_errors = False
|
||||
data = call_webservice(
|
||||
cache=True,
|
||||
notify_on_errors=notify_on_errors,
|
||||
record_on_errors=record_on_errors,
|
||||
**(self.request or {}),
|
||||
)[2]
|
||||
return json.loads(force_str(data))
|
||||
try:
|
||||
data = call_webservice(
|
||||
cache=True,
|
||||
notify_on_errors=notify_on_errors,
|
||||
record_on_errors=record_on_errors,
|
||||
**(self.request or {}),
|
||||
)[2]
|
||||
return json.loads(force_str(data))
|
||||
except UnflattenKeysException as e:
|
||||
get_publisher().record_error(
|
||||
error_summary=e.get_summary(),
|
||||
exception=e,
|
||||
context='[WSCALL]',
|
||||
notify=notify_on_errors,
|
||||
record=record_on_errors,
|
||||
)
|
||||
|
||||
|
||||
class WsCallsSubstitutionProxy:
|
||||
|
@ -444,6 +535,15 @@ class WsCallRequestWidget(CompositeWidget):
|
|||
'data-dynamic-display-child-of': method_widget.get_name(),
|
||||
'data-dynamic-display-value': methods.get('POST'),
|
||||
},
|
||||
hint=_(
|
||||
'The / in parameter name allows to generate complex objects. '
|
||||
'Thus a parameter named "element/child" containing "value" will generate the payload '
|
||||
'"element": {"child": "value"}. If the subkey, i.e. "child", is an integer it will '
|
||||
'become a list index and two elements "element/0", "element/1" (indexes should '
|
||||
' start from zero) containing "value1" and "value2" will generate the payload '
|
||||
'"element": ["value1", "value2"]. It is possible to combine the two types, for '
|
||||
'example "element/0/key1" to generate a list of objects.'
|
||||
),
|
||||
)
|
||||
|
||||
self.add(
|
||||
|
|
Plutôt utiliser un with pytest.raises le plus restrictif possible
Au final je ne léve pas d'exception mais plutôt enregistre l'erreur les logs.