Commit 6f2270f9 authored by Tomáš Peterka's avatar Tomáš Peterka

[hal_json_style] Unittest exotic results of search method

parent 6cf2326c
...@@ -12,10 +12,13 @@ Only in mode == 'search' ...@@ -12,10 +12,13 @@ Only in mode == 'search'
:param query: :param query:
:param select_list: :param select_list:
:param limit: :param limit:
:param form_relative_url: {str} relative URL of a form FIELD issuing the search (listbox/relation field...)
Only in mode == 'form' Only in mode == 'form'
:param form: :param form:
Only in mode == 'traverse'
TBD. TBD.
""" """
from ZTUtils import make_query from ZTUtils import make_query
...@@ -143,11 +146,7 @@ def getRealRelativeUrl(document): ...@@ -143,11 +146,7 @@ def getRealRelativeUrl(document):
def getFormRelativeUrl(form): def getFormRelativeUrl(form):
return portal.portal_catalog( return portal.portal_catalog(
portal_type=ComplexQuery( portal_type=("ERP5 Form", "ERP5 Report"),
Query(portal_type="ERP5 Form"),
Query(portal_type="ERP5 Report"),
logical_operator='or'
),
uid=form.getUid(), uid=form.getUid(),
id=form.getId(), id=form.getId(),
limit=1, limit=1,
...@@ -416,7 +415,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -416,7 +415,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# we abandoned Selections in RJS thus we mix selection query parameters into # we abandoned Selections in RJS thus we mix selection query parameters into
# listbox's default parameters # listbox's default parameters
default_params.update(selection_params) default_params.update(selection_params)
lines = field.get_value('lines') lines = field.get_value('lines')
list_method_name = traversed_document.Listbox_getListMethodName(field) list_method_name = traversed_document.Listbox_getListMethodName(field)
...@@ -449,7 +448,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -449,7 +448,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if list_method_param in REQUEST and list_method_param not in list_method_query_dict: if list_method_param in REQUEST and list_method_param not in list_method_query_dict:
list_method_query_dict[list_method_param] = REQUEST.get(list_method_param) list_method_query_dict[list_method_param] = REQUEST.get(list_method_param)
# MIDDLE-DANGEROUS! # MIDDLE-DANGEROUS!
# In case of reports (later even exports) substitute None for unknown # In case of reports (later even exports) substitute None for unknown
# parameters. We suppose Python syntax for parameters! # parameters. We suppose Python syntax for parameters!
# What we do here is literally putting every form field from REQUEST # What we do here is literally putting every form field from REQUEST
# into search method parameters - this is later put back into REQUEST # into search method parameters - this is later put back into REQUEST
...@@ -1169,16 +1168,14 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1169,16 +1168,14 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
"template": True "template": True
} }
} }
# Define document action # Define document action
if action_dict: if action_dict:
result_dict['_actions'] = action_dict result_dict['_actions'] = action_dict
elif mode == 'search': elif mode == 'search':
################################################# #################################################
# Portal catalog search # Portal catalog search
# #
# Possible call arguments example: # Possible call arguments example:
# form_relative_url: portal_skins/erp5_web/WebSite_view/listbox # form_relative_url: portal_skins/erp5_web/WebSite_view/listbox
...@@ -1321,7 +1318,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1321,7 +1318,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
continue continue
if result_index >= start + num_items: if result_index >= start + num_items:
break break
contents_item = {} contents_item = {}
contents_list.append(contents_item) contents_list.append(contents_item)
...@@ -1357,14 +1354,14 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1357,14 +1354,14 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
search_property_getter = getProtectedProperty search_property_getter = getProtectedProperty
search_property_hasser = lambda doc, attr: doc.hasProperty(attr) search_property_hasser = lambda doc, attr: doc.hasProperty(attr)
else: else:
# In case of reports the `search_result` can be list of # In case of reports the `search_result` can be list of
# PythonScripts.standard._Object - a reimplementation of plain dictionary # PythonScripts.standard._Object - a reimplementation of plain dictionary
# means we are iterating over plain objects # means we are iterating over plain objects
# list_method must be defined because POPOs can return only that # list_method must be defined because POPOs can return only that
contents_uid = "{}#{:d}".format(list_method, result_index) contents_uid = "{}#{:d}".format(list_method, result_index)
# JIO requires every item to have _links.self.href so it can construct # JIO requires every item to have _links.self.href so it can construct
# links to the document. Here we have a object in RAM (which should # links to the document. Here we have a object in RAM (which should
# never happen!) thus we provide temporary UID # never happen!) thus we provide temporary UID
contents_relative_url = "{}/{}".format(traversed_document.getRelativeUrl(), contents_uid) contents_relative_url = "{}/{}".format(traversed_document.getRelativeUrl(), contents_uid)
# property getter must be simple __getattr__ implementation # property getter must be simple __getattr__ implementation
...@@ -1377,7 +1374,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1377,7 +1374,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
'self': { 'self': {
"href": default_document_uri_template % { "href": default_document_uri_template % {
"root_url": site_root.absolute_url(), "root_url": site_root.absolute_url(),
"relative_url": contents_relative_url, "relative_url": contents_relative_url,
"script_id": script.id "script_id": script.id
}, },
}, },
...@@ -1389,7 +1386,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1389,7 +1386,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
'key': "%s_uid:list" % listbox_field_id, 'key': "%s_uid:list" % listbox_field_id,
'value': contents_uid 'value': contents_uid
} }
# render whole field in contents_item or at least search result value # render whole field in contents_item or at least search result value
for select in select_list: for select in select_list:
if editable_field_dict.has_key(select): if editable_field_dict.has_key(select):
# cell has a Form Field template thus render it using the field # cell has a Form Field template thus render it using the field
...@@ -1404,9 +1401,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1404,9 +1401,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
contents_item[select] = renderField( contents_item[select] = renderField(
traversed_document, traversed_document,
editable_field_dict[select], editable_field_dict[select],
listbox_form, listbox_form,
value=default_field_value, value=default_field_value,
key='field_%s_%s' % (editable_field_dict[select].id, contents_uid)) key='field_%s_%s' % (editable_field_dict[select].id, contents_uid))
REQUEST.other.pop('cell', None) REQUEST.other.pop('cell', None)
else: else:
...@@ -1414,19 +1412,20 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1414,19 +1412,20 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# value by resolving value in the correct order. The code is copy&pasted # value by resolving value in the correct order. The code is copy&pasted
# from ListBoxRendererLine.getValueList because it is universal # from ListBoxRendererLine.getValueList because it is universal
contents_value = None contents_value = None
if "." in select:
select = select[select.rindex('.') + 1:]
if len(select) == 0: if not isinstance(select, (str, unicode)) or len(select) == 0:
context.log('There is an empty column name in {!s}!'.format(select_list), level=200) context.log('There is an invalid column name in {!s}!'.format(select_list), level=200)
continue continue
if "." in select:
select = select[select.rindex('.') + 1:]
# 1. resolve attribute on a raw object (all wrappers removed) using # 1. resolve attribute on a raw object (all wrappers removed) using
# lowest-level secure getattr method given object type # lowest-level secure getattr method given object type
raw_search_result = search_result raw_search_result = search_result
if hasattr(search_result, 'aq_base'): if hasattr(search_result, 'aq_base'):
raw_search_result = search_result.aq_base raw_search_result = search_result.aq_base
if search_property_hasser(raw_search_result, select): if search_property_hasser(raw_search_result, select):
contents_value = search_property_getter(raw_search_result, select) contents_value = search_property_getter(raw_search_result, select)
...@@ -1470,7 +1469,9 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1470,7 +1469,9 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
has_brain_param = False has_brain_param = False
if hasattr(contents_value, "params"): if hasattr(contents_value, "params"):
has_mandatory_param = any(map(lambda param: '=' not in param and '*' not in param, has_mandatory_param = any(map(lambda param: '=' not in param and '*' not in param,
contents_value.params().split(","))) contents_value.params().split(","))) \
if contents_value.params() \
else False # because any([]) == True
has_brain_param = "brain" in contents_value.params() has_brain_param = "brain" in contents_value.params()
try: try:
if has_mandatory_param: if has_mandatory_param:
......
...@@ -66,14 +66,26 @@ def simulate(script_id, params_string, code_string): ...@@ -66,14 +66,26 @@ def simulate(script_id, params_string, code_string):
return decorated return decorated
return upperWrap return upperWrap
def createIndexedDocument(): def wipeFolder(folder):
"""Create a Foo document inside Foo module and pass it as "document" argument into wrapped function.""" folder.deleteContent(list(folder.objectIds()))
transaction.commit()
def createIndexedDocument(quantity=1):
"""Create `quantity` Foo document(s) in Foo module and pass it as `document(_list)` argument into the wrapped function."""
def decorator(func): def decorator(func):
def wrapped(self, *args, **kwargs): def wrapped(self, *args, **kwargs):
kwargs.update(document=self._makeDocument()) wipeFolder(self.portal.foo_module)
if quantity <= 1:
kwargs.update(document=self._makeDocument())
else:
kwargs.update(document_list=[self._makeDocument() for _ in range(quantity)])
self.portal.portal_caches.clearAllCache() self.portal.portal_caches.clearAllCache()
self.tic() self.tic()
return func(self, *args, **kwargs) try:
return func(self, *args, **kwargs)
finally:
wipeFolder(self.portal.foo_module)
self.tic() # unindex
return wrapped return wrapped
return decorator return decorator
...@@ -1074,6 +1086,82 @@ class TestERP5Document_getHateoas_mode_search(ERP5HALJSONStyleSkinsMixin): ...@@ -1074,6 +1086,82 @@ class TestERP5Document_getHateoas_mode_search(ERP5HALJSONStyleSkinsMixin):
mode="search", mode="search",
default_param_json='eyJcdTAwZWEiOiAiXHUwMGU4In0=') default_param_json='eyJcdTAwZWEiOiAiXHUwMGU4In0=')
@simulate('Base_getRequestUrl', '*args, **kwargs', 'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs', 'return "application/hal+json"')
@simulate('Test_listObjects', '*args, **kwargs', """
from Products.PythonScripts.standard import Object
return [Object(debit_price=1000.00, credit_price=100.00),
Object(debit_price=10.00, credit_price=0.00)]
""")
@simulate('Test_listProducts', '*args, **kwargs', """
return context.getPortalObject().foo_module.values()
""")
@simulate('Test_listCatalog', '*args, **kwargs', """
return context.getPortalObject().portal_catalog(portal_type='Foo', sort_on=[('id', 'ASC')])
""")
@createIndexedDocument(quantity=2)
@changeSkin('Hal')
def test_getHateoas_exotic_search_results(self, document_list):
"""Test that ingestion of `list_method` result does not fail.
The only limit for the result of `list_method` is that it should be an iterable.
Practically, because we code in python, it can be any object.
"""
fake_request = do_fake_request("GET")
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request,
mode="search",
local_roles=["Assignor", "Assignee"],
list_method='Test_listObjects',
select_list=['credit_price', 'debit_price']
)
self.assertEquals(fake_request.RESPONSE.status, 200)
self.assertEquals(fake_request.RESPONSE.getHeader('Content-Type'),
"application/hal+json"
)
result_dict = json.loads(result)
self.assertEqual(len(result_dict['_embedded']['contents']), 2)
self.assertEqual(result_dict['_embedded']['contents'][0]['debit_price'], 1000.0)
self.assertEqual(result_dict['_embedded']['contents'][0]['credit_price'], 100.0)
self.assertEqual(result_dict['_embedded']['contents'][1]['debit_price'], 10.0)
self.assertEqual(result_dict['_embedded']['contents'][1]['credit_price'], 0.0)
# Render a Document using Form Field template (only for field 'id')
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request,
mode="search",
local_roles=["Assignor", "Assignee"],
list_method='Test_listProducts',
select_list=['id'],
form_relative_url='portal_skins/erp5_ui_test/FooModule_viewFooList/listbox'
)
result_dict = json.loads(result)
self.assertEqual(2, len(result_dict['_embedded']['contents']))
self.assertIn("field_listbox", result_dict['_embedded']['contents'][0]['id']['key'])
self.assertEqual("StringField", result_dict['_embedded']['contents'][0]['id']['type'])
self.assertEqual(document_list[0].getId(), result_dict['_embedded']['contents'][0]['id']['default'])
self.assertIn("field_listbox", result_dict['_embedded']['contents'][1]['id']['key'])
self.assertEqual("StringField", result_dict['_embedded']['contents'][1]['id']['type'])
self.assertEqual(document_list[1].getId(), result_dict['_embedded']['contents'][1]['id']['default'])
# Test rendering without form template of attribute, getterm and a script
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request,
mode="search",
local_roles=["Assignor", "Assignee"],
list_method='Test_listCatalog',
select_list=['title', 'Foo_getLocalTitle', 'getTotalQuantity'] # property, Script, method
)
result_dict = json.loads(result)
self.assertEqual(len(result_dict['_embedded']['contents']), 2)
self.assertEqual(result_dict['_embedded']['contents'][0]['title'].encode('utf-8'), document_list[0].getTitle())
self.assertEqual(result_dict['_embedded']['contents'][0]['Foo_getLocalTitle'], None)
self.assertEqual(result_dict['_embedded']['contents'][0]['getTotalQuantity'], 0)
self.assertEqual(result_dict['_embedded']['contents'][1]['title'].encode('utf-8'), document_list[1].getTitle())
self.assertEqual(result_dict['_embedded']['contents'][1]['Foo_getLocalTitle'], None)
self.assertEqual(result_dict['_embedded']['contents'][1]['getTotalQuantity'], 0)
class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin): class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin):
@simulate('Base_getRequestHeader', '*args, **kwargs', @simulate('Base_getRequestHeader', '*args, **kwargs',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment