Commit 03831389 authored by Tomáš Peterka's avatar Tomáš Peterka Committed by Tomáš Peterka

[hal_json+renderjs] Bug fixes

-  [hal_json] distinguishes between REQUEST.form and REQUEST.other
-  [hal_json] byteify all keys and values before putting them into REQUEST
-  [renderjs_ui] dialog does not go to parent when validation fails
-  [hal_json] does not crash on direct traverse of a Script instead of a Dialog
-  [renderjs_ui_test] move the logging-out test to its isolated zuite so it does not cause problems
parent 1d4b96ae
...@@ -61,7 +61,6 @@ from Products.ERP5Type.Message import Message ...@@ -61,7 +61,6 @@ from Products.ERP5Type.Message import Message
from Products.ERP5Type.Utils import UpperCase from Products.ERP5Type.Utils import UpperCase
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
from collections import OrderedDict from collections import OrderedDict
from urlparse import urlparse
MARKER = [] MARKER = []
...@@ -126,6 +125,8 @@ def byteify(string): ...@@ -126,6 +125,8 @@ def byteify(string):
return {byteify(key): byteify(value) for key, value in string.iteritems()} return {byteify(key): byteify(value) for key, value in string.iteritems()}
elif isinstance(string, list): elif isinstance(string, list):
return [byteify(element) for element in string] return [byteify(element) for element in string]
elif isinstance(string, tuple):
return tuple(byteify(element) for element in string)
elif isinstance(string, unicode): elif isinstance(string, unicode):
return string.encode('utf-8') return string.encode('utf-8')
else: else:
...@@ -151,7 +152,7 @@ time_iso_re = re.compile(r'^(\d{2}):(\d{2}):(\d{2}).*$') ...@@ -151,7 +152,7 @@ time_iso_re = re.compile(r'^(\d{2}):(\d{2}):(\d{2}).*$')
def ensureDeserialized(obj): def ensureDeserialized(obj):
"""Deserialize classes serialized by our own `ensureSerializable`. """Deserialize classes serialized by our own `ensureSerializable`.
Method `biteify` must not be called on the result because it would revert out Method `byteify` must not be called on the result because it would revert out
deserialization by calling __str__ on constructed classes. deserialization by calling __str__ on constructed classes.
""" """
if isinstance(obj, dict): if isinstance(obj, dict):
...@@ -560,7 +561,7 @@ def getFieldDefault(form, field, key, value=None): ...@@ -560,7 +561,7 @@ def getFieldDefault(form, field, key, value=None):
Previously we used Formulator.Field._get_default which is (for no reason) private. Previously we used Formulator.Field._get_default which is (for no reason) private.
""" """
if value is None: if value is None:
value = REQUEST.get(key, MARKER) value = REQUEST.form.get(key, MARKER)
# use marker because default value can be intentionally empty string # use marker because default value can be intentionally empty string
if value is MARKER: if value is MARKER:
value = field.get_value('default', request=REQUEST, REQUEST=REQUEST) value = field.get_value('default', request=REQUEST, REQUEST=REQUEST)
...@@ -582,7 +583,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -582,7 +583,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# thus setting the field_id is optional and controlled by `request_field` argument # thus setting the field_id is optional and controlled by `request_field` argument
if request_field: if request_field:
previous_request_field = REQUEST.other.pop('field_id', None) previous_request_field = REQUEST.other.pop('field_id', None)
REQUEST.set('field_id', field.id) REQUEST.other['field_id'] = field.id
if meta_type is None: if meta_type is None:
meta_type = field.meta_type meta_type = field.meta_type
...@@ -1125,7 +1126,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -1125,7 +1126,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
if form.pt == 'form_dialog': if form.pt == 'form_dialog':
# overwrite "form_id" field's value because old UI does that by passing # overwrite "form_id" field's value because old UI does that by passing
# the form_id in query string and hidden fields # the form_id in query string and hidden fields
renderHiddenField(response_dict, "form_id", REQUEST.get('form_id') or form.id) renderHiddenField(response_dict, "form_id", last_form_id or REQUEST.get('form_id', '') or form.id)
# dialog_id is a mandatory field in any form_dialog # dialog_id is a mandatory field in any form_dialog
renderHiddenField(response_dict, 'dialog_id', form.id) renderHiddenField(response_dict, 'dialog_id', form.id)
# some dialog actions use custom cancel_url # some dialog actions use custom cancel_url
...@@ -1242,7 +1243,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -1242,7 +1243,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# metadata about the form (its hash and dynamic fields) # metadata about the form (its hash and dynamic fields)
renderHiddenField(response_dict, 'extra_param_json', json.dumps(extra_param_json)) renderHiddenField(response_dict, 'extra_param_json', json.dumps(extra_param_json))
for key, value in previous_request_other.items(): for key, value in byteify(previous_request_other.items()):
if value is not None: if value is not None:
REQUEST.set(key, value) REQUEST.set(key, value)
...@@ -1403,8 +1404,8 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1403,8 +1404,8 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if isinstance(extra_param_json, str): if isinstance(extra_param_json, str):
extra_param_json = ensureDeserialized(json.loads(urlsafe_b64decode(extra_param_json))) extra_param_json = ensureDeserialized(json.loads(urlsafe_b64decode(extra_param_json)))
for key, value in extra_param_json.items(): for k, v in byteify(extra_param_json.items()):
REQUEST.set(key, value) REQUEST.set(k, v)
# Add a link to the portal type if possible # Add a link to the portal type if possible
if not is_portal: if not is_portal:
...@@ -1457,37 +1458,32 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1457,37 +1458,32 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
} }
} }
if view_instance.pt == "form_dialog":
# If there is a "form_id" in the REQUEST then it means that last view was actually a form
# and we are most likely in a dialog. We save previous form into `last_form_id` ...
last_form_id = REQUEST.get('form_id', "") if REQUEST is not None else ""
# Put all query parameters (?reset:int=1&workflow_action=start_action) in request to mimic usual form display # Put all query parameters (?reset:int=1&workflow_action=start_action) in request to mimic usual form display
# Request is later used for method's arguments discovery so set URL params into REQUEST (just like it was sent by form) # Request is later used for method's arguments discovery so set URL params into REQUEST (just like it was sent by form)
for query_key, query_value in current_action['params'].items(): for query_key, query_value in byteify(current_action['params'].items()):
REQUEST.set(query_key, query_value) REQUEST.set(query_key, query_value)
# If our "form" is actually a Script (nothing is sure in ERP5) then execute it here # If our "form" is actually a Script (nothing is sure in ERP5) or anything else than Form
try: # (e.g. function or bound class method will) not have .meta_type thus be considered a Script
if "Script" in view_instance.meta_type: # then we execute it directly
if "Script" in getattr(view_instance, "meta_type", "Script"):
# we suppose that the script takes only what is given in the URL params # we suppose that the script takes only what is given in the URL params
return view_instance(**current_action['params'])
except AttributeError:
# if renderer form does not have attr meta_type then it is not a document
# but most likely bound instance method. Some form_ids do actually point to methods.
returned_value = view_instance(**current_action['params']) returned_value = view_instance(**current_action['params'])
# returned value is usually REQUEST.RESPONSE.redirect() # returned value is usually REQUEST.RESPONSE.redirect()
log('ERP5Document_getHateoas', 'HAL_JSON cannot handle returned value "{!s}" from {}({!s})'.format( log('ERP5Document_getHateoas', 'HAL_JSON cannot handle returned value "{!s}" from {}({!s})'.format(
returned_value, current_action['view_id'], current_action['params']), 100) returned_value, current_action['view_id'], current_action['params']), 100)
status_message = Base_translateString('Operation executed') status_message = Base_translateString('Operation executed.')
if isinstance(returned_value, (str, unicode)) and returned_value.startswith('http'): if isinstance(returned_value, str) and "portal_status_message=" in returned_value: # it is an URL
parsed_url = urlparse(returned_value) status_message = re.match(r'portal_status_message=([^&]*)', returned_value).group(1).replace('+', ' ')
parsed_query = parse_qs(parsed_url.query)
if len(parsed_query.get('portal_status_message', ())) > 0:
status_message = parsed_query.get('portal_status_message')[0]
return traversed_document.Base_redirect(keep_items={ return traversed_document.Base_redirect(keep_items={
'portal_status_message': status_message}) 'portal_status_message': status_message})
if view_instance.pt == "form_dialog":
# If there is a "form_id" in the REQUEST then it means that last view was actually a form
# and we are most likely in a dialog. We save previous form into `last_form_id` ...
last_form_id = REQUEST.get('form_id', "")
# Sometimes callables (form dialog's method, listbox's list method...) do not touch # Sometimes callables (form dialog's method, listbox's list method...) do not touch
# REQUEST but expect all (formerly) URL query parameters to appear in their **kw # REQUEST but expect all (formerly) URL query parameters to appear in their **kw
# thus we send extra_param_json (=rjs way of passing parameters to REQUEST) as # thus we send extra_param_json (=rjs way of passing parameters to REQUEST) as
...@@ -1721,7 +1717,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1721,7 +1717,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if isinstance(extra_param_json, str): if isinstance(extra_param_json, str):
extra_param_json = ensureDeserialized(json.loads(urlsafe_b64decode(extra_param_json))) extra_param_json = ensureDeserialized(json.loads(urlsafe_b64decode(extra_param_json)))
for key, value in extra_param_json.items(): for key, value in byteify(extra_param_json.items()):
REQUEST.set(key, value) REQUEST.set(key, value)
# in case we have custom list method # in case we have custom list method
......
<?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>dialog_id, form_id, uids, **kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Folder_doNothing</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -38,10 +38,10 @@ ...@@ -38,10 +38,10 @@
splitted_current_jio_key_list, splitted_current_jio_key_list,
command, command,
i; i;
if (is_updating) { if (is_updating || !jio_key) {
return; return;
} }
if (!jio_key || gadget.state.redirect_to_parent) { if (gadget.state.redirect_to_parent) {
return gadget.redirect({command: 'history_previous'}); return gadget.redirect({command: 'history_previous'});
} }
if (gadget.state.jio_key === jio_key) { if (gadget.state.jio_key === jio_key) {
......
...@@ -224,7 +224,7 @@ ...@@ -224,7 +224,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>966.62936.1765.16691</string> </value> <value> <string>966.63118.5474.36386</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -242,7 +242,7 @@ ...@@ -242,7 +242,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1524144844.96</float> <float>1524572973.21</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
<?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>testFormDialogWorkflowScriptDirectly</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 Form View Editable Save Action</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test Default Module View</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/PTZuite_CommonTemplate/macros/init" />
<tr>
<td>open</td>
<td>${base_url}/web_site_module/renderjs_runner/#/foo_module/1?editable=1</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//input[@name="field_my_title"]</td>
<td></td>
</tr>
<tr>
<td>assertValue</td>
<td>//input[@name="field_my_title"]</td>
<td>Title 1</td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//a[text()='Actions']</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>//a[text()='Actions']</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//a[@data-i18n='Custom Action No Dialog']</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>//a[@data-i18n='Custom Action No Dialog']</td>
<td></td>
</tr>
<tr>
<td rowspan="1" colspan="3">
Action should succeed and redirect back to the context.<br/>
It will not show messages because we do not parse the response.
</td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//input[@name="field_my_title"]</td>
<td></td>
</tr>
<tr>
<td>assertValue</td>
<td>//input[@name="field_my_title"]</td>
<td>Title 1</td>
</tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Zuite" module="Products.Zelenium.zuite"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>renderjs_ui_recover_password_zuite</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>WARNING - tests which log you out</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
##############################################################################
#
# Copyright (c) 2011 Nexedi SARL and Contributors. All Rights Reserved.
# Kazuhiko <kazuhiko@nexedi.com>
# Rafael Monnerat <rafael@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import unittest
from Products.ERP5Type.tests.ERP5TypeFunctionalTestCase import ERP5TypeFunctionalTestCase
class TestRenderJSUIRecoverPassword(ERP5TypeFunctionalTestCase):
foreground = 0
run_only = "renderjs_ui_recover_password_zuite"
def getBusinessTemplateList(self):
return (
'erp5_web_renderjs_ui',
'erp5_web_renderjs_ui_test',
'erp5_ui_test_core',
'erp5_accounting',
'erp5_test_result',
)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestRenderJSUIRecoverPassword))
return suite
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testFunctionalRJSRecoverPassword</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testFunctionalRJSRecoverPassword</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
...@@ -32,6 +32,8 @@ portal_tests/renderjs_ui_page_zuite ...@@ -32,6 +32,8 @@ portal_tests/renderjs_ui_page_zuite
portal_tests/renderjs_ui_page_zuite/** portal_tests/renderjs_ui_page_zuite/**
portal_tests/renderjs_ui_radio_field_zuite portal_tests/renderjs_ui_radio_field_zuite
portal_tests/renderjs_ui_radio_field_zuite/** portal_tests/renderjs_ui_radio_field_zuite/**
portal_tests/renderjs_ui_recover_password_zuite
portal_tests/renderjs_ui_recover_password_zuite/**
portal_tests/renderjs_ui_relation_field_zuite portal_tests/renderjs_ui_relation_field_zuite
portal_tests/renderjs_ui_relation_field_zuite/** portal_tests/renderjs_ui_relation_field_zuite/**
portal_tests/renderjs_ui_router_zuite portal_tests/renderjs_ui_router_zuite
......
...@@ -19,3 +19,4 @@ test.erp5.testFunctionalRJSLogoutTranslation ...@@ -19,3 +19,4 @@ test.erp5.testFunctionalRJSLogoutTranslation
test.erp5.testFunctionalRJSNotification test.erp5.testFunctionalRJSNotification
test.erp5.testFunctionalRJSMatrixbox test.erp5.testFunctionalRJSMatrixbox
test.erp5.testFunctionalRJSEditorGadget test.erp5.testFunctionalRJSEditorGadget
test.erp5.testFunctionalRJSRecoverPassword
\ 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