wscalls: preview unflattened payload (#66916)

This commit is contained in:
Serghei Mihai 2024-02-28 14:01:48 +01:00
parent ddbe8f65de
commit 12bdb4a498
7 changed files with 171 additions and 0 deletions

View File

@ -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 }}],[""]]'

View File

@ -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,

View File

@ -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')

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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',

View File

@ -90,6 +90,7 @@ def i18n_js(request):
'close': _('Close'),
'email_domain_suggest': _('Did you want to write'),
'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'