Compare commits

..

2 Commits

Author SHA1 Message Date
Serghei Mihai e9766d6c10 wscalls: preview unflattened payload (#66916)
gitea/wcs/pipeline/head There was a failure building this commit Details
2024-04-10 14:59:08 +02:00
Serghei Mihai 84a3a28d69 wscalls: unflatten payload when calling webservice (#66916) 2024-04-10 14:29:38 +02:00
6 changed files with 151 additions and 48 deletions

View File

@ -275,3 +275,54 @@ 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_json_payload(pub, admin_user):
get_app(pub).get('/api/preview-json-payload', status=403)
app = login(get_app(pub))
resp = app.get('/api/preview-json-payload')
assert resp.pyquery('div.payload-preview').length == 1
assert '<h2>Payload preview</h2>' in resp.text
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-json-payload', params=params)
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-json-payload', params=params)
assert resp.pyquery('div.payload-preview div.errornotice').length == 1
assert 'Unable to generate 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',
'post_data$element1value$value_template': 'Foo',
'post_data$element1value$value_python': '',
'post_data$element2key': '1',
'post_data$element2value$value_template': '{{ form_name }}',
'post_data$element2value$value_python': '',
'post_data$element3key': '2',
'post_data$element3value$value_template': '',
}
resp = app.get('/api/preview-json-payload', params=params)
assert resp.pyquery('div.payload-preview').text() == '["Foo",{{ form_name }},"",]'

View File

@ -297,13 +297,13 @@ def test_webservice_with_unflattened_payload_keys(http_requests, pub):
wscall.request = {
'method': 'POST',
'url': 'http://remote.example.net/json',
'post_data': {'foo/0': 'first', 'foo/1': 'second', 'bar': 'example'},
'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.get_last('body') == '{"bar": "example", "foo": ["first", "second", ""]}'
assert http_requests.count() == 1
wscall.request = {

View File

@ -18,6 +18,7 @@ import base64
import copy
import datetime
import json
import re
import urllib.parse
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
@ -26,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
@ -56,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
@ -1406,6 +1409,7 @@ class ApiDirectory(Directory):
'geojson',
'jobs',
('card-file-by-token', 'card_file_by_token'),
('preview-json-payload', 'preview_json_payload'),
('sign-url-token', 'sign_url_token'),
]
@ -1433,6 +1437,71 @@ class ApiDirectory(Directory):
get_response().set_content_type('application/json')
return json.dumps({'err': 0, 'data': list_roles})
def preview_json_payload(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('')):
if isinstance(o, (list, tuple)):
html += htmltext('[<span class="payload-preview--obj">')
for item in o:
html = format_payload(item, html)
html += htmltext('</span>]')
elif isinstance(o, dict):
html += htmltext('{<span class="payload-preview--obj">')
for k, v in o.items():
html += htmltext('<span class="payload-preview--key">"%s"</span>: ' % k)
html = format_payload(v, html)
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 put double quotes
o = '"%s"' % o
html += htmltext('<span class="payload-preview--value">%s</span>' % o)
html += htmltext('<span class="payload-preview--item-separator">,</span>')
return html
payload = parse_payload()
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Payload preview')
r += htmltext('<div class="payload-preview">')
try:
unflattened_payload = unflatten_keys(payload)
r += format_payload(unflattened_payload)
except UnflattenKeysException as e:
r += htmltext('<div class="errornotice"><p>%s</p><p>%s %s</p></div>') % (
_('Unable to generate 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

@ -3131,3 +3131,31 @@ ul.objects-list.single-links li a.link-action-icon.duplicate {
content: "\f24d"; /* clone */
}
}
span.payload-preview {
&--value {
font-family: Monospace;
& + & {
display: block;
}
}
&--value + &--key::before {
content: '\a';
white-space: pre;
}
&--obj {
margin-left: 1em;
display: block;
}
&--item-separator + &--key::before, &--item-separator + &--value::before {
content: '\a';
white-space: pre;
}
&--obj + &--key::before, &--obj + &--value::before {
content: ',\a';
white-space: pre;
}
&--item-separator:last-child {
display: none;
}
}

View File

@ -305,7 +305,7 @@ $(function() {
$('div.WidgetDict[data-widget-name*="post_data"] input[type="text"]').on('change', function() {
var $widget = $(this).parents('div.WidgetDict');
var url = '/preview-json-payload?' + $('input', $widget).serialize();
var url = '/api/preview-json-payload?' + $('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) {

View File

@ -21,11 +21,8 @@ from importlib import import_module
from quixote import get_publisher, get_request, get_response, get_session, get_session_manager, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from quixote.util import StaticDirectory
from wcs.wscalls import UnflattenKeysException, unflatten_keys
from . import portfolio
from .api import ApiDirectory
from .categories import Category
@ -216,7 +213,6 @@ class RootDirectory(Directory):
'fargo',
'static',
'actions',
('preview-json-payload', 'preview_json_payload'),
('r', 'tiny_redirect'),
]
@ -304,47 +300,6 @@ class RootDirectory(Directory):
get_response().set_content_type('text/plain')
return json.dumps(results, cls=misc.JSONEncoder)
def preview_json_payload(self):
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])
r = TemplateIO(html=True)
payload = parse_payload()
r += htmltext('<h2>%s</h2>') % _('Payload preview')
r += htmltext('<div class="payload-preview">')
try:
dump = json.dumps(unflatten_keys(payload), ensure_ascii=False, indent=2)
r += htmltext('<pre>%s</pre>') % dump.replace('"{{', '{{').replace('}}"', '}}')
except UnflattenKeysException as e:
r += htmltext('<div class="errornotice"><p>%s</p><p>%s<code>%s</code></p></div>') % (
_('Unable to generate payload.'),
_('Following error occured: '),
e,
)
r += htmltext('</div>')
return r.getvalue()
def feed_substitution_parts(self):
get_publisher().substitutions.feed(get_session())
get_publisher().substitutions.feed(get_request().user)