Commit f8215c96 authored by Tomáš Peterka's avatar Tomáš Peterka

[hal_json_style] Improve search-value resolution (bug 20171215-133705C)

-  introduce External Function for Acquisition resolution to get over permissions
-  improve recursive TALES expression support
-  closely follow value resolution from ListBox
-  remove external method for default value resolution
parent e5c3b2f5
from Acquisition import aq_self, aq_base
def Base_aqSelf(self):
return aq_self(self)
def Base_aqBase(self):
return aq_base(self)
def Field_getSubFieldKeyDict(self, field, field_id, key=None): def Field_getSubFieldKeyDict(self, field, field_id, key=None):
"""XXX""" """XXX"""
return field.generate_subfield_key(field_id, key=key) return field.generate_subfield_key(field_id, key=key)
def Field_getDefaultValue(self, field, key, value, REQUEST):
return field._get_default(key, value, REQUEST)
def WorkflowTool_listActionParameterList(self): def WorkflowTool_listActionParameterList(self):
action_list = self.listActions() action_list = self.listActions()
info = self._getOAI(None) info = self._getOAI(None)
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>_function</string> </key> <key> <string>_function</string> </key>
<value> <string>Field_getDefaultValue</string> </value> <value> <string>Base_aqBase</string> </value>
</item> </item>
<item> <item>
<key> <string>_module</string> </key> <key> <string>_module</string> </key>
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>Field_getDefaultValue</string> </value> <value> <string>Base_aqBase</string> </value>
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>Base_aqSelf</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>HalStyle</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_aqSelf</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -117,7 +117,7 @@ def selectKwargsForCallable(func, initial_kwargs, kwargs_dict): ...@@ -117,7 +117,7 @@ def selectKwargsForCallable(func, initial_kwargs, kwargs_dict):
def getUidAndAccessorForAnything(search_result, result_index, traversed_document): def getUidAndAccessorForAnything(search_result, result_index, traversed_document):
"""Return unique ID, unique URL, getter and hasser for any combination of `search_result` and `index`. """Return unique ID, unique URL, getter for any combination of `search_result` and `index`.
You want to use this method when you need a unique reference to random object in iterable (for example You want to use this method when you need a unique reference to random object in iterable (for example
result of list_method or stat_method). This will give you UID and URL for identification within JIO and result of list_method or stat_method). This will give you UID and URL for identification within JIO and
...@@ -126,9 +126,7 @@ def getUidAndAccessorForAnything(search_result, result_index, traversed_document ...@@ -126,9 +126,7 @@ def getUidAndAccessorForAnything(search_result, result_index, traversed_document
Usage:: Usage::
for i, random_object in enumerate(unknown_iterable): for i, random_object in enumerate(unknown_iterable):
uid, url, getter, hasser = object_ids_and_access(random_object, i) uid, url, getter = object_ids_and_access(random_object, i)
if hasser(random_object, "linkable"):
result[uid] = {'url': portal.abolute_url() + url}
value = getter(random_object, "value") value = getter(random_object, "value")
""" """
if hasattr(search_result, "getObject"): if hasattr(search_result, "getObject"):
...@@ -138,22 +136,14 @@ def getUidAndAccessorForAnything(search_result, result_index, traversed_document ...@@ -138,22 +136,14 @@ def getUidAndAccessorForAnything(search_result, result_index, traversed_document
contents_relative_url = getRealRelativeUrl(search_result) contents_relative_url = getRealRelativeUrl(search_result)
# get property in secure way from documents # get property in secure way from documents
search_property_getter = getProtectedProperty search_property_getter = getProtectedProperty
def search_property_hasser (doc, attr): elif hasattr(search_result, 'hasProperty') and hasattr(search_result, 'getId'):
"""Brains cannot access Properties - they use permissioned getters."""
try:
return doc.hasProperty(attr)
except (AttributeError, Unauthorized) as e:
log('Cannot state ownership of property "{}" on {!s} because of "{!s}"'.format(
attr, doc, e))
return False
elif hasattr(search_result, "aq_self"):
# Zope products have at least ID thus we work with that # Zope products have at least ID thus we work with that
contents_uid = search_result.uid contents_uid = search_result.uid
# either we got a document with relativeUrl or we got product and use ID # either we got a document with relativeUrl or we got product and use ID
contents_relative_url = getRealRelativeUrl(search_result) or search_result.getId() contents_relative_url = getRealRelativeUrl(search_result) or search_result.getId()
# documents and products have the same way of accessing properties # documents and products have the same way of accessing properties
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
...@@ -167,13 +157,12 @@ def getUidAndAccessorForAnything(search_result, result_index, traversed_document ...@@ -167,13 +157,12 @@ def getUidAndAccessorForAnything(search_result, result_index, traversed_document
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
search_property_getter = lambda obj, attr: getattr(obj, attr, None) search_property_getter = lambda obj, attr: getattr(obj, attr, None)
search_property_hasser = lambda obj, attr: hasattr(obj, attr)
return contents_uid, contents_relative_url, search_property_getter, search_property_hasser return contents_uid, contents_relative_url, search_property_getter
def getAttrFromAnything(search_result, select, search_property_getter, search_property_hasser, kwargs): def getAttrFromAnything(search_result, select, search_property_getter, kwargs):
"""Given `search_result` extract value named `select` using helper getter/hasser. """Given `search_result` extract value named `select` using helper getter.
:param search_result: any dict-like object (usually dict or Brain or Document) :param search_result: any dict-like object (usually dict or Brain or Document)
:param select: field name (can represent actual attributes, Properties or even Scripts) :param select: field name (can represent actual attributes, Properties or even Scripts)
...@@ -201,43 +190,46 @@ def getAttrFromAnything(search_result, select, search_property_getter, search_pr ...@@ -201,43 +190,46 @@ def getAttrFromAnything(search_result, select, search_property_getter, search_pr
# or obvious getter (starts with "get" or Capital letter - Script) # or obvious getter (starts with "get" or Capital letter - Script)
accessor_name = select accessor_name = select
# 1. resolve attribute on a raw object (all wrappers removed) using # Following value resolution is copied from product/ERP5Form/ListBox.py#L2223
# lowest-level secure getattr method given object type # 1. resolve attribute on unwrapped object using getter
raw_search_result = search_result try:
if hasattr(search_result, 'aq_base'): unwrapped_search_result = search_result.Base_aqSelf()
raw_search_result = search_result.aq_base if contents_value is None and unwrapped_search_result is not None:
# BUT! only if there is no accessor (because that is the prefered way) try:
if search_property_hasser(raw_search_result, select) and not hasattr(raw_search_result, accessor_name): if getattr(unwrapped_search_result, select, None) is not None:
contents_value = search_property_getter(raw_search_result, select) # I know this looks suspicious but product/ERP5Form/ListBox.py#L2224
contents_value = getattr(search_result, select)
except Unauthorized:
pass
except AttributeError:
pass # search_result is not a Document
# 2. use the fact that wrappers (brain or acquisition wrapper) use # 2. use the fact that wrappers (brain or acquisition wrapper) use
# permissioned getters # permissioned getters
unwrapped_search_result = search_result try:
if hasattr(search_result, 'aq_self'): raw_search_result = search_result.Base_aqBase()
unwrapped_search_result = search_result.aq_self if contents_value is None and raw_search_result is not None:
try:
if getattr(raw_search_result, accessor_name, None) is not None:
contents_value = getattr(search_result, accessor_name, None)
except Unauthorized:
pass
except AttributeError:
pass # search_result is not a Document
# Following part resolves values on other objects than Documents
# Prefer getter (accessor) than raw property name
if contents_value is None: if contents_value is None:
# again we check on a unwrapped object to avoid acquisition resolution
# which would certainly find something which we don't want
try: try:
if hasattr(raw_search_result, accessor_name) and callable(getattr(search_result, accessor_name)): contents_value = getattr(search_result, accessor_name)
# test on raw object but get the actual accessor using wrapper and acquisition except (AttributeError, Unauthorized):
# do not call it here - it will be done later in generic call part pass
contents_value = getattr(search_result, accessor_name)
except (AttributeError, KeyError, Unauthorized) as error:
log("Could not evaluate {} nor {} on {} with error {!s}".format(
select, accessor_name, search_result, error), level=100) # WARNING
if contents_value is None and search_property_hasser(search_result, select):
# maybe it is just a attribute
contents_value = search_property_getter(search_result, select)
if contents_value is None: if contents_value is None:
try: try:
contents_value = getattr(search_result, select, None) contents_value = search_property_getter(search_result, select)
except (Unauthorized, AttributeError, KeyError) as error: except (AttributeError, Unauthorized):
log("Cannot resolve {} on {!s} because {!s}".format( pass
select, raw_search_result, error), level=100)
if callable(contents_value): if callable(contents_value):
has_mandatory_param = False has_mandatory_param = False
...@@ -1430,7 +1422,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1430,7 +1422,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# we can render fields which need 'here' to be set to currently rendered document # we can render fields which need 'here' to be set to currently rendered document
#REQUEST.set('here', search_result) #REQUEST.set('here', search_result)
contents_item = {} contents_item = {}
contents_uid, contents_relative_url, property_getter, property_hasser = \ contents_uid, contents_relative_url, property_getter = \
getUidAndAccessorForAnything(search_result, result_index, traversed_document) getUidAndAccessorForAnything(search_result, result_index, traversed_document)
# _links.self.href is mandatory for JIO so it can create reference to the # _links.self.href is mandatory for JIO so it can create reference to the
...@@ -1453,20 +1445,21 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1453,20 +1445,21 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
'value': contents_uid 'value': contents_uid
} }
# if default value is given by evaluating Tales expression then we only
# put "cell" to request (expected by tales) and let the field evaluate
REQUEST.set('cell', search_result)
for select in select_list: for select in select_list:
default_field_value = None default_field_value = None
# every `select` can have a template field or be just a exotic getter for a value # every `select` can have a template field or be just a exotic getter for a value
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
# fields are nice because they are standard # fields are nice because they are standard
REQUEST.set('cell', search_result) if (not editable_field_dict[select].tales.get("default", "") and # no tales
# if default value is given by evaluating Tales expression then we only (editable_field_dict[select].meta_type != 'ProxyField' or
# put "cell" to request (expected by tales) and let the field evaluate not hasattr(editable_field_dict[select], "get_recursive_tales") or # no recursive tales
if (not editable_field_dict[select].get_tales("default") and # no tales
(not hasattr(editable_field_dict[select], "get_recursive_tales") or # no recursive tales
editable_field_dict[select].get_recursive_tales("default") == "")): editable_field_dict[select].get_recursive_tales("default") == "")):
# if there is no tales expr (or is empty) we extract the value from search result # if there is no tales expr (or is empty) we extract the value from search result
default_field_value = getAttrFromAnything(search_result, select, property_getter, property_hasser, {}) default_field_value = getAttrFromAnything(search_result, select, property_getter, {})
contents_item[select] = renderField( contents_item[select] = renderField(
traversed_document, traversed_document,
...@@ -1475,13 +1468,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1475,13 +1468,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
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)
else: else:
# most of the complicated magic happens here - we need to resolve field names # most of the complicated magic happens here - we need to resolve field names
# given search_result. This name can unfortunately mean almost anything from # given search_result. This name can unfortunately mean almost anything from
# a key name to Python Script with variable number of input parameters. # a key name to Python Script with variable number of input parameters.
contents_item[select] = getAttrFromAnything(search_result, select, property_getter, property_hasser, {'brain': search_result}) contents_item[select] = getAttrFromAnything(search_result, select, property_getter, {'brain': search_result})
# endfor select # endfor select
REQUEST.other.pop('cell', None)
contents_list.append(contents_item) contents_list.append(contents_item)
result_dict['_embedded']['contents'] = contents_list result_dict['_embedded']['contents'] = contents_list
......
"""Test for bug 20171215-133705C
acContext creates a temporary copy of Document with changed Properties.
In related test we ensure that property is resolved before getter because
that is how it works in the old UI.
"""
foo_list = list(context.contentValues())
foo_context_list = [foo.asContext(state="Couscous") for foo in foo_list]
"""
for foo, foo_context in zip(foo_list, foo_context_list):
context.log("Foo.state {!s}, Foo.getState() {!s}, Foo.asContext().state {!s}, Foo.asContext().getState() {!s}".format(
foo.getProperty('state'), foo.getState(), getattr(foo_context, 'state'), foo_context.getState()))
"""
return foo_context_list
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>**kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>FooModule_listAsContext</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testListboxValueAsContext</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test ListBox propetry resolution before getter</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Bug 20171215-133705C - property should be resolved before getter if different and not empty</td></tr>
</thead><tbody>
<tr><td>store</td>
<td>https://softinst81338.host.vifib.net/erp5</td>
<td>base_url</td></tr>
<tal:block metal:use-macro="here/PTZuite_CommonTemplate/macros/init" />
<tr><td>open</td>
<td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Reset Successfully.</td><td></td></tr>
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=2&amp;create_line:int=0</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Created Successfully.</td><td></td></tr>
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_list_method=FooModule_listAsContext&amp;field_columns=id%7CID%0Astate%7CState</td>
<td></td></tr>
<tr><td>assertTextPresent</td>
<td>Set Successfully.</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<!-- Shortcut for full renderjs url -->
<tr><td>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_listbox_loaded" />
<!-- Test for value "Couscous" (which is obviously the correct one) instead of "current"
Why? Please read description in the bug 20171215-133705C. Custom list_method returns
a list of [Foo.asContext(state="Couscous")] and getState seems to refer to original value.
-->
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[1]/td[2]/a</td>
<td>Couscous</td></tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
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