diff --git a/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py b/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py index 86f2b2b38aad7e3f1779e42b5c64ba14667d981b..433c7a63f4c3d3f9e1c3a1469e7e04839c7f98d3 100644 --- a/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py +++ b/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py @@ -78,6 +78,43 @@ def byteify(string): else: return string + +def ensureSerializable(obj): + """Ensure obj and all sub-objects are JSON serializable.""" + if isinstance(obj, dict): + for key in obj: + obj[key] = ensureSerializable(obj[key]) + # throw away date's type information and later reconstruct as Zope's DateTime + if isinstance(obj, DateTime): + return obj.ISO() + if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)): + return obj.isoformat() + # let us believe that iterables don't contain other unserializable objects + return obj + + +datetime_iso_re = re.compile(r'^\d{4}-\d{2}-\d{2} |T\d{2}:\d{2}:\d{2}.*$') +time_iso_re = re.compile(r'^(\d{2}):(\d{2}):(\d{2}).*$') +def ensureDeserialized(obj): + """Deserialize classes serialized by our own `ensureSerializable`. + + Method `biteify` must not be called on the result because it would revert out + deserialization by calling __str__ on constructed classes. + """ + if isinstance(obj, dict): + for key in obj: + obj[key] = ensureDeserialized(obj[key]) + # seems that default __str__ method is good enough + if isinstance(obj, str): + # Zope's DateTime must be good enough for everyone + if datetime_iso_re.match(obj): + return DateTime(obj) + if time_iso_re.match(obj): + match_obj = time_iso_re.match(obj) + return datetime.time(*tuple(map(int, match_obj.groups()))) + return obj + + def getProtectedProperty(document, select): """getProtectedProperty is a security-aware substitution for builtin `getattr` @@ -324,7 +361,7 @@ def getRealRelativeUrl(document): def getFormRelativeUrl(form): return portal.portal_catalog( - portal_type="ERP5 Form", + portal_type=("ERP5 Form", "ERP5 Report"), uid=form.getUid(), id=form.getId(), limit=1, @@ -359,6 +396,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key if key is None: key = field.generate_field_key(key_prefix=key_prefix) + if meta_type == "ProxyField": + # resolve the base meta_type + meta_type = field.getRecursiveTemplateField().meta_type + result = { "type": meta_type, "title": Base_translateString(field.get_value("title")), @@ -376,12 +417,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key "default": getFieldDefault(form, field, key, value), }) - if meta_type == "ProxyField": - return renderField(traversed_document, field, form, value, - meta_type=field.getRecursiveTemplateField().meta_type, - key=key, key_prefix=key_prefix, - selection_params=selection_params) - + # start the actual "switch" on field's meta_type here if meta_type in ("ListField", "RadioField", "ParallelListField", "MultiListField"): result.update({ # XXX Message can not be converted to json as is @@ -427,6 +463,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key if v)) if parameters: result["default"] = '%s?%s' % (result["default"], parameters) + return result if meta_type == "DateTimeField": @@ -474,7 +511,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key except Unauthorized: jump_reference_list = [] result.update({ - "editable": False + "editable": False }) query = url_template_dict["jio_search_template"] % { "query": make_query({"query": sql_catalog.buildQuery( @@ -528,7 +565,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key if rel_cache[key] is not MARKER: REQUEST.set(key, rel_cache[key]) - result.update({ "url": relative_url, "translated_portal_types": translated_portal_type, @@ -569,12 +605,21 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key return result if meta_type == "ListBox": - """Display list of objects with optional search/sort capabilities on columns from catalog.""" + """Display list of objects with optional search/sort capabilities on columns from catalog. + + We might be inside a ReportBox which is inside a parent form BUT we still have access to + the original REQUEST with sent POST values from the parent form. We can save those + values into our query method and reconstruct them meanwhile calling asynchronous jio.allDocs. + """ _translate = Base_translateString - column_list = [(name, _translate(title)) for name, title in field.get_value("columns")] + # column definition in ListBox own value 'columns' is superseded by dynamic + # column definition from Selection for specific Report ListBoxes; the same for editable_columns + column_list = [(name, _translate(title)) for name, title in (selection_params.get('selection_columns', []) + or field.get_value("columns"))] + editable_column_list = [(name, _translate(title)) for name, title in (selection_params.get('editable_columns', []) + or field.get_value("editable_columns"))] all_column_list = [(name, _translate(title)) for name, title in field.get_value("all_columns")] - editable_column_list = [(name, _translate(title)) for name, title in field.get_value("editable_columns")] catalog_column_list = [(name, title) for name, title in OrderedDict(column_list + all_column_list).items() if sql_catalog.isValidColumn(name)] @@ -586,25 +631,28 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key # try to get specified sortable columns and fail back to searchable fields sort_column_list = [(name, _translate(title)) - for name, title in field.get_value("sort_columns") + for name, title in (selection_params.get('selection_sort_order', []) + or field.get_value("sort_columns")) if sql_catalog.isValidColumn(name)] or search_column_list + # portal_type list can be overriden by selection too + # since it can be intentionally empty we don't override with non-empty field value + portal_type_list = selection_params.get("portal_type", field.get_value('portal_types')) # requirement: get only sortable/searchable columns which are already displayed in listbox # see https://lab.nexedi.com/nexedi/erp5/blob/HEAD/product/ERP5Form/ListBox.py#L1004 # implemented in javascript in the end # see https://lab.nexedi.com/nexedi/erp5/blob/master/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.js#L163 - - portal_types = field.get_value('portal_types') - default_params = dict(field.get_value('default_params')) + default_params = dict(field.get_value('default_params')) # default_params is a list of tuples default_params['ignore_unknown_columns'] = True - if selection_params is not None: - default_params.update(selection_params) - # How to implement pagination? - # default_params.update(REQUEST.form) - lines = field.get_value('lines') - list_method_query_dict = dict( - portal_type=[x[1] for x in portal_types], **default_params - ) + # we abandoned Selections in RJS thus we mix selection query parameters into + # listbox's default parameters + default_params.update(selection_params) + + # ListBoxes in report view has portal_type defined already in default_params + # in that case we prefer non_empty version + list_method_query_dict = default_params.copy() + if not list_method_query_dict.get("portal_type", []): + list_method_query_dict["portal_type"] = [x for x, _ in portal_type_list] list_method_custom = None # Search for non-editable documents - all reports goes here @@ -646,8 +694,11 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key "relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"), "form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id), "list_method": list_method_name, - "default_param_json": urlsafe_b64encode(json.dumps(list_method_query_dict)) + "default_param_json": urlsafe_b64encode( + json.dumps(ensureSerializable(list_method_query_dict))) } + # once we imprint `default_params` into query string of 'list method' we + # don't want them to propagate to the query as well list_method_query_dict = {} """ # We commented out this part because of backward compatibility @@ -663,7 +714,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key "script_id": script.id, "relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"), "list_method": list_method_name, - "default_param_json": urlsafe_b64encode(json.dumps(list_method_query_dict)) + "default_param_json": urlsafe_b64encode(json.dumps(ensureSerializable(list_method_query_dict))) } list_method_query_dict = {} """ @@ -696,9 +747,9 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key "sort_column_list": sort_column_list, "editable_column_list": editable_column_list, "show_anchor": field.get_value("anchor"), - "portal_type": portal_types, - "lines": lines, - "default_params": default_params, + "portal_type": portal_type_list, + "lines": field.get_value('lines'), + "default_params": ensureSerializable(default_params), "list_method": list_method_name, "show_stat": field.get_value('stat_method') != "" or len(field.get_value('stat_columns')) > 0, "show_count": field.get_value('count_method') != "", @@ -848,26 +899,84 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti } if (form.pt == 'report_view'): + # reports are expected to return list of ReportSection which is a wrapper + # around a form - thus we will need to render those forms report_item_list = [] report_result_list = [] for field in form.get_fields(): if field.getRecursiveTemplateField().meta_type == 'ReportBox': + # ReportBox.render returns a list of ReportSection classes which are + # just containers for FormId(s) usually containing one ListBox + # and its search/query parameters hidden in `selection_params` + # `path` contains relative_url of intended CONTEXT for underlaying ListBox report_item_list.extend(field.render()) - j = 0 - for report_item in report_item_list: - report_context = report_item.getObject(portal) - report_prefix = 'x%s' % j - j += 1 + # ERP5 Report document differs from a ERP5 Form in only one thing: it has + # `report_method` attached to it - thus we call it right here + if hasattr(form, 'report_method') and getattr(form, 'report_method', ""): + report_method_name = getattr(form, 'report_method') + report_method = getattr(traversed_document, report_method_name) + report_item_list.extend(report_method()) + + for report_index, report_item in enumerate(report_item_list): + report_context = report_item.getObject(traversed_document) + report_prefix = 'x%s' % report_index report_title = report_item.getTitle() # report_class = "report_title_level_%s" % report_item.getLevel() report_form = report_item.getFormId() report_result = {'_links': {}} - renderForm(traversed_document, getattr(report_context, report_item.getFormId()), - report_result, key_prefix=report_prefix, - selection_params=report_item.selection_params) + # some reports save a lot of unserializable data (datetime.datetime) and + # key "portal_type" (don't confuse with "portal_types" in ListBox) into + # report_item.selection_params thus we need to take that into account in + # ListBox field + # + # Selection Params are parameters for embedded ListBox's List Method + # and it must be passed in `default_json_param` field (might contain + # unserializable data types thus we need to take care of that + # In order not to lose information we put all ReportSection attributes + # inside the report selection params + report_form_params = report_item.selection_params.copy() \ + if report_item.selection_params is not None \ + else {} + + if report_item.selection_name: + selection_name = report_prefix + "_" + report_item.selection_name + report_form_params.update(selection_name=selection_name) + # this should load selections with correct values - since it is modifying + # global state in the backend we have nothing more to do here + # I could not find where the code stores params in selection with render + # prefix - maybe it in some `render` method where it should not be + # Of course it is ugly, terrible and should be removed! + selection_tool = context.getPortalObject().portal_selections + selection_tool.getSelectionFor(selection_name, REQUEST) + selection_tool.setSelectionParamsFor(selection_name, report_form_params) + selection_tool.setSelectionColumns(selection_name, report_item.selection_columns) + + if report_item.selection_columns: + report_form_params.update(selection_columns=report_item.selection_columns) + if report_item.selection_sort_order: + report_form_params.update(selection_sort_order=report_item.selection_sort_order) + + # Report section is just a wrapper around form thus we render it right + # we keep traversed_document because its Portal Type Class should be + # addressable by the user = have actions (object_view) attached to it + # BUT! when Report Section defines `path` that is the new context for + # form rendering and subsequent searches... + renderForm(traversed_document if not report_item.path else report_context, + getattr(report_context, report_item.getFormId()), + report_result, + key_prefix=report_prefix, + selection_params=report_form_params) # used to be only report_item.selection_params + # Report Title is important since there are more section on report page + # but often they render the same form with different data so we need to + # distinguish by the title at least. + report_result['title'] = report_title report_result_list.append(report_result) - response_dict['report_section_list'] = report_result_list + # end-if report_section + + for key, value in previous_request_other.items(): + if value is not None: + REQUEST.set(key, value) # XXX form action update, etc def renderRawField(field): @@ -910,6 +1019,7 @@ def renderRawField(field): def renderFormDefinition(form, response_dict): + """Form "definition" is configurable in Zope admin: Form -> Order.""" group_list = [] for group in form.Form_getGroupTitleAndId(): @@ -1028,19 +1138,23 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, action_dict = {} # result_dict['_relative_url'] = traversed_document.getRelativeUrl() result_dict['title'] = traversed_document.getTitle() - + # Add a link to the portal type if possible if not is_portal: - result_dict['_links']['type'] = { - "href": default_document_uri_template % { - "root_url": site_root.absolute_url(), - "relative_url": portal.portal_types[traversed_document.getPortalType()]\ - .getRelativeUrl(), - "script_id": script.id - }, - "name": Base_translateString(traversed_document.getPortalType()) - } - + # traversed_document should always have its Portal Type in ERP5 Portal Types + # thus attached actions to it so it is viewable + document_type_name = traversed_document.getPortalType() + document_type = getattr(portal.portal_types, document_type_name, None) + if document_type is not None: + result_dict['_links']['type'] = { + "href": default_document_uri_template % { + "root_url": site_root.absolute_url(), + "relative_url": document_type.getRelativeUrl(), + "script_id": script.id + }, + "name": Base_translateString(traversed_document.getPortalType()) + } + # Return info about container if not is_portal: container = traversed_document.getParentValue() @@ -1060,6 +1174,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, portal.portal_actions.listFilteredActionsFor(traversed_document)) embedded_url = None + # XXX See ERP5Type.getDefaultViewFor for erp5_action_key in erp5_action_dict.keys(): erp5_action_list = [] @@ -1151,7 +1266,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, # renderer_form = traversed_document.restrictedTraverse(form_id, None) # XXX Proxy field are not correctly handled in traversed_document of web site renderer_form = getattr(traversed_document, form_id) - # traversed_document.log(form_id) if (renderer_form is not None): embedded_dict = { '_links': { @@ -1161,19 +1275,31 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, } } # Put all query parameters (?reset:int=1&workflow_action=start_action) in request to mimic usual form display + query_param_dict = {} query_split = embedded_url.split('?', 1) if len(query_split) == 2: for query_parameter in query_split[1].split("&"): - query_key, query_value = query_parameter.split("=") - REQUEST.set(query_key, query_value) - + query_key, query_value = query_parameter.split('=') + # often + is used instead of %20 so we replace for space here + query_param_dict[query_key] = query_value.replace("+", " ") + + # set URL params into REQUEST (just like it was sent by form) + for query_key, query_value in query_param_dict.items(): + REQUEST.set(query_key, query_value) + + # unfortunatelly some people use Scripts as targets for Workflow + # transactions - thus we need to check and mitigate + if "Script" in renderer_form.meta_type: + # we suppose that the script takes only what is given in the URL params + return renderer_form(**query_param_dict) + renderForm(traversed_document, renderer_form, embedded_dict) result_dict['_embedded'] = { '_view': embedded_dict # embedded_action_key: embedded_dict } # result_dict['_links']["_view"] = {"href": embedded_url} - + # Include properties in document JSON # XXX Extract from renderer form? """ @@ -1255,7 +1381,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, else: traversed_document_portal_type = traversed_document.getPortalType() - if traversed_document_portal_type == "ERP5 Form": + if traversed_document_portal_type in ("ERP5 Form", "ERP5 Report"): renderFormDefinition(traversed_document, result_dict) response.setHeader("Cache-Control", "private, max-age=1800") response.setHeader("Vary", "Cookie,Authorization,Accept-Encoding") @@ -1278,19 +1404,18 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, "template": True } } - + # Define document action if action_dict: result_dict['_actions'] = action_dict - - + elif mode == 'search': ################################################# # Portal catalog search # # Possible call arguments example: # form_relative_url: portal_skins/erp5_web/WebSite_view/listbox - # list_method: objectValues (Script providing listing) + # list_method: "objectValues" (Script providing items) # default_param_json: <base64 encoded JSON> (Additional search params) # query: <str> (term for fulltext search) # select_list: ['int_index', 'id', 'title', ...] (column names to select) @@ -1337,9 +1462,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, "sort_on": () # default is an empty tuple } if default_param_json is not None: - catalog_kw.update(byteify(json.loads(urlsafe_b64decode(default_param_json)))) + catalog_kw.update( + ensureDeserialized( + byteify( + json.loads(urlsafe_b64decode(default_param_json))))) if query: catalog_kw["full_text"] = query + if sort_on is not None: def parseSortOn(raw_string): """Turn JSON serialized array into a tuple (col_name, order).""" @@ -1452,6 +1581,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, contents_uid, contents_relative_url, property_getter = \ getUidAndAccessorForAnything(search_result, result_index, traversed_document) + # Check if this object provides a specific URL method. + # if getattr(search_result, 'getListItemUrl', None) is not None: + # search_result.getListItemUrl(contents_uid, result_index, selection_name) + # _links.self.href is mandatory for JIO so it can create reference to the # (listbox) item alone contents_item['_links'] = { @@ -1503,7 +1636,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, # endfor select REQUEST.other.pop('cell', None) contents_list.append(contents_item) - result_dict['_embedded']['contents'] = contents_list + result_dict['_embedded']['contents'] = ensureSerializable(contents_list) # Compute statistics if the search issuer was ListBox # or in future if the stats (SUM) are required by JIO call @@ -1562,7 +1695,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, traversed_document, editable_field_dict[key], listbox_form, value, key=editable_field_dict[key].id + '__sum') if len(contents_stat_list) > 0: - result_dict['_embedded']['sum'] = contents_stat_list + result_dict['_embedded']['sum'] = ensureSerializable(contents_stat_list) # We should cleanup the selection if it exists in catalog params BUT # we cannot because it requires escalated Permission.'modifyPortal' so diff --git a/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py b/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py index c208b4a89ec85f56c31e53294795477277ada374..93a89c365432efae65c8d8465bada53ab75a11e2 100644 --- a/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py +++ b/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py @@ -9,8 +9,11 @@ from functools import wraps from ZPublisher.HTTPRequest import HTTPRequest from ZPublisher.HTTPResponse import HTTPResponse +import base64 +import DateTime import StringIO import json +import re import urllib def changeSkin(skin_name): @@ -639,6 +642,42 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): self.assertEqual(result_dict['_embedded']['_view']['_actions']['put']['method'], 'POST') + @simulate('Base_getRequestUrl', '*args, **kwargs', + 'return "http://example.org/bar"') + @simulate('Base_getRequestHeader', '*args, **kwargs', + 'return "application/hal+json"') + @changeSkin('Hal') + def test_getHateoasDocument_listbox_vs_relation_inconsistency(self): + """Purpose of this test is to point to inconsistencies in search-enabled field rendering. + + ListBox gets its Portal Types in `portal_type` as list of tuples whether + Relation Input receives `portal_types` and `translated_portal_types` + """ + document = self._makeDocument() + # Drop editable permission + document.manage_permission('Modify portal content', [], 0) + document.Foo_view.listbox.ListBox_setPropertyList( + field_title = 'Foo Lines', + field_list_method = 'objectValues', + field_portal_types = 'Foo Line | Foo Line', + ) + fake_request = do_fake_request("GET") + result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas( + REQUEST=fake_request, + mode="traverse", + relative_url=document.getRelativeUrl(), + view="view") + self.assertEquals(fake_request.RESPONSE.status, 200) + self.assertEquals(fake_request.RESPONSE.getHeader('Content-Type'), + "application/hal+json" + ) + result_dict = json.loads(result) + # ListBox rendering of allowed Portal Types + self.assertEqual(result_dict['_embedded']['_view']['listbox']['portal_type'], [['Foo Line', 'Foo Line']]) + # Relation Input rendering of allowed Portal Types + self.assertEqual(result_dict['_embedded']['_view']['my_foo_category_title']['portal_types'], ['Category']) + self.assertEqual(result_dict['_embedded']['_view']['my_foo_category_title']['translated_portal_types'], ['Category']) + @simulate('Base_getRequestUrl', '*args, **kwargs', 'return "http://example.org/bar"') @simulate('Base_getRequestHeader', '*args, **kwargs', @@ -712,6 +751,71 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): self.assertFalse(result_dict['_embedded']['_view'].has_key('_actions')) + @simulate('Base_getRequestUrl', '*args, **kwargs', + 'return "http://example.org/bar"') + @simulate('Base_getRequestHeader', '*args, **kwargs', + 'return "application/hal+json"') + @changeSkin('Hal') + def test_getHateoasDocument_listbox_list_method_params(self): + """Ensure that `list_method` of ListBox receives specified parameters.""" + document = self._makeDocument() + document.manage_permission('Modify portal content', [], 0) + # pass custom list method which expect input arguments + document.Foo_view.listbox.ListBox_setPropertyList( + field_title = 'Foo Lines', + field_list_method = 'Foo_listWithInputParams', + field_portal_types = 'Foo Line | Foo Line', + field_columns = 'id|ID\ntitle|Title\nquantity|Quantity\nstart_date|Date\ncatalog.uid|Uid') + + now = DateTime.DateTime() + tomorrow = now + 1 + + fake_request = do_fake_request("GET", data=( + ('start_date', now.ISO()), + ('stop_date', tomorrow.ISO())) + ) + # I tried to implement the standard way (see `data` param in do_fake_request) + # but for some reason it does not work...so we hack our way around + fake_request.set('start_date', now.ISO()) + fake_request.set('stop_date', tomorrow.ISO()) + result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas( + REQUEST=fake_request, + mode="traverse", + relative_url=document.getRelativeUrl(), + form=document.restrictedTraverse('portal_skins/erp5_ui_test/Foo_view'), + view="view" + ) + + self.assertEquals(fake_request.RESPONSE.status, 200) + self.assertEquals(fake_request.RESPONSE.getHeader('Content-Type'), + "application/hal+json" + ) + result_dict = json.loads(result) + list_method_template = \ + result_dict['_embedded']['_view']['listbox']['list_method_template'] + # default_param_json must not be empty because our custom list method + # specifies input parameters - they need to be filled from REQUEST + self.assertIn('default_param_json', list_method_template) + default_param_json = json.loads( + base64.b64decode( + re.search(r'default_param_json=([^\{&]+)', + list_method_template).group(1))) + self.assertIn("start_date", default_param_json) + self.assertEqual(default_param_json["start_date"], now.ISO()) + self.assertIn("stop_date", default_param_json) + self.assertEqual(default_param_json["stop_date"], tomorrow.ISO()) + # reset listbox properties to defaults + document.Foo_view.listbox.ListBox_setPropertyList( + field_title = 'Foo Lines', + field_list_method = 'objectValues', + field_portal_types = 'Foo Line | Foo Line', + field_stat_method = 'portal_catalog', + field_stat_columns = 'quantity | Foo_statQuantity', + field_editable = 1, + field_columns = 'id|ID\ntitle|Title\nquantity|Quantity\nstart_date|Date\ncatalog.uid|Uid', + field_editable_columns = 'id|ID\ntitle|Title\nquantity|quantity\nstart_date|Date', + field_search_columns = 'id|ID\ntitle|Title\nquantity|Quantity\nstart_date|Date',) + @simulate('Base_getRequestUrl', '*args, **kwargs', 'return "http://example.org/bar"') @simulate('Base_getRequestHeader', '*args, **kwargs', @@ -1208,7 +1312,6 @@ class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin): self.assertEquals(fake_request.RESPONSE.status, 405) self.assertEquals(result, "") - @simulate('Base_getRequestUrl', '*args, **kwargs', 'return "http://example.org/bar"') @simulate('Base_getRequestHeader', '*args, **kwargs', @@ -1347,6 +1450,7 @@ class TestERP5Document_getHateoas_mode_worklist(ERP5HALJSONStyleSkinsMixin): self.assertEqual(result_dict['_debug'], "worklist") + class TestERP5Document_getHateoas_translation(ERP5HALJSONStyleSkinsMixin): code_string = "\ from Products.CMFCore.utils import getToolByName\n\ @@ -1428,8 +1532,7 @@ return msg" code_string) @createIndexedDocument() @changeSkin('Hal') - def test_getHateoasWorklist_default_view_translation(self, **kw): - # self._makeDocument() + def test_getHateoasWorklist_default_view_translation(self, document): fake_request = do_fake_request("GET") result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas( REQUEST=fake_request, @@ -1476,7 +1579,7 @@ class TestERP5Action_getHateoas(ERP5HALJSONStyleSkinsMixin): @changeSkin('Hal') def test_getHateoasDialog_dialog_failure(self, document): """Test an dialog on Foo object with empty required for a failure. - + Expected behaviour is response Http 400 with field errors. """ fake_request = do_fake_request("POST") diff --git a/bt5/erp5_ui_test/ActionTemplateItem/portal_types/Foo/view_hidden_positive_only_quantity.xml b/bt5/erp5_ui_test/ActionTemplateItem/portal_types/Foo/view_hidden_positive_only_quantity.xml new file mode 100644 index 0000000000000000000000000000000000000000..b217ce7577b29b4609e4bf50504bc9874cd59c84 --- /dev/null +++ b/bt5/erp5_ui_test/ActionTemplateItem/portal_types/Foo/view_hidden_positive_only_quantity.xml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="ActionInformation" module="Products.CMFCore.ActionInformation"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + <item> + <key> <string>categories</string> </key> + <value> + <tuple> + <string>action_type/object_view</string> + </tuple> + </value> + </item> + <item> + <key> <string>category</string> </key> + <value> <string>object_view</string> </value> + </item> + <item> + <key> <string>condition</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string>Form with hidden quantity field with external validator asserting positiveness of the value. Used to test behaviour of errors on hidden fields.</string> </value> + </item> + <item> + <key> <string>icon</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>view_hidden_positive_only_quantity</string> </value> + </item> + <item> + <key> <string>permissions</string> </key> + <value> + <tuple> + <string>View</string> + </tuple> + </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Action Information</string> </value> + </item> + <item> + <key> <string>priority</string> </key> + <value> <float>10.0</float> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string>View Hidden Positive-Only Quantity</string> </value> + </item> + <item> + <key> <string>visible</string> </key> + <value> <int>1</int> </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="Expression" module="Products.CMFCore.Expression"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>text</string> </key> + <value> <string>string: ${object_url}/Foo_viewHiddenErrorneousField</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_listWithInputParams.py b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_listWithInputParams.py new file mode 100644 index 0000000000000000000000000000000000000000..c26098bd1cda09003781622dde126ea68df9d29b --- /dev/null +++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_listWithInputParams.py @@ -0,0 +1,11 @@ +"""Foo_listWithInputParams is here only to test passing parameters from REQUEST via introspection in RenderJS UI. + +We expect DateTime parameters thus they have to undergo a serialization/deserialization process. +""" + +from DateTime import DateTime + +assert isinstance(start_date, DateTime), "start_date is instance of {!s} instead of DateTime!".format(type(start_date)) +assert isinstance(stop_date, DateTime), "stop_date is instance of {!s} instead of DateTime!".format(type(stop_date)) + +return context.listFolder(portal_type='Foo Line') diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_listWithInputParams.xml b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_listWithInputParams.xml new file mode 100644 index 0000000000000000000000000000000000000000..d00f94c94c8e3bdcd8b77044913240471e082348 --- /dev/null +++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_listWithInputParams.xml @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Python Script" module="erp5.portal_type"/> + </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>start_date, stop_date=None, **kwargs</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>Foo_listWithInputParams</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Python Script</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField.xml b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField.xml new file mode 100644 index 0000000000000000000000000000000000000000..38b65a388e0dc85e5a3dfee0356350d39833176d --- /dev/null +++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField.xml @@ -0,0 +1,149 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="ERP5 Form" module="erp5.portal_type"/> + </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/> + </value> + </item> + </dictionary> + </state> + </object> + </value> + </item> + <item> + <key> <string>_objects</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>action</string> </key> + <value> <string>Base_edit</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string>Display some integers field for selenium tests</string> </value> + </item> + <item> + <key> <string>edit_order</string> </key> + <value> + <list/> + </value> + </item> + <item> + <key> <string>encoding</string> </key> + <value> <string>UTF-8</string> </value> + </item> + <item> + <key> <string>enctype</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>group_list</string> </key> + <value> + <list> + <string>left</string> + <string>right</string> + <string>center</string> + <string>bottom</string> + <string>hidden</string> + </list> + </value> + </item> + <item> + <key> <string>groups</string> </key> + <value> + <dictionary> + <item> + <key> <string>bottom</string> </key> + <value> + <list/> + </value> + </item> + <item> + <key> <string>center</string> </key> + <value> + <list/> + </value> + </item> + <item> + <key> <string>hidden</string> </key> + <value> + <list/> + </value> + </item> + <item> + <key> <string>left</string> </key> + <value> + <list> + <string>my_quantity</string> + <string>read_only_quantity</string> + </list> + </value> + </item> + <item> + <key> <string>right</string> </key> + <value> + <list/> + </value> + </item> + </dictionary> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>Foo_viewHiddenErrorneousField</string> </value> + </item> + <item> + <key> <string>method</string> </key> + <value> <string>POST</string> </value> + </item> + <item> + <key> <string>name</string> </key> + <value> <string>Foo_view</string> </value> + </item> + <item> + <key> <string>pt</string> </key> + <value> <string>form_view</string> </value> + </item> + <item> + <key> <string>row_length</string> </key> + <value> <int>4</int> </value> + </item> + <item> + <key> <string>stored_encoding</string> </key> + <value> <string>UTF-8</string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string>Foo</string> </value> + </item> + <item> + <key> <string>unicode_mode</string> </key> + <value> <int>0</int> </value> + </item> + <item> + <key> <string>update_action</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField/my_quantity.xml b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField/my_quantity.xml new file mode 100644 index 0000000000000000000000000000000000000000..ff762c7317833b1074d7f68baa51aba9a59581ba --- /dev/null +++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField/my_quantity.xml @@ -0,0 +1,271 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="IntegerField" module="Products.Formulator.StandardFields"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>id</string> </key> + <value> <string>my_quantity</string> </value> + </item> + <item> + <key> <string>message_values</string> </key> + <value> + <dictionary> + <item> + <key> <string>external_validator_failed</string> </key> + <value> <string>The input failed the external validator.</string> </value> + </item> + <item> + <key> <string>integer_out_of_range</string> </key> + <value> <string>The integer you entered was out of range.</string> </value> + </item> + <item> + <key> <string>not_integer</string> </key> + <value> <string>You did not enter an integer.</string> </value> + </item> + <item> + <key> <string>required_not_found</string> </key> + <value> <string>Input is required but no input given.</string> </value> + </item> + </dictionary> + </value> + </item> + <item> + <key> <string>overrides</string> </key> + <value> + <dictionary> + <item> + <key> <string>alternate_name</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>css_class</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>default</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_maxwidth</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_width</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>editable</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>enabled</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>end</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>external_validator</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>extra</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>hidden</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>required</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>start</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>whitespace_preserve</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </value> + </item> + <item> + <key> <string>tales</string> </key> + <value> + <dictionary> + <item> + <key> <string>alternate_name</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>css_class</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>default</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_maxwidth</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_width</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>editable</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>enabled</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>end</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>external_validator</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>extra</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>hidden</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>required</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>start</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>whitespace_preserve</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </value> + </item> + <item> + <key> <string>values</string> </key> + <value> + <dictionary> + <item> + <key> <string>alternate_name</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>css_class</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>default</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_maxwidth</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_width</string> </key> + <value> <int>20</int> </value> + </item> + <item> + <key> <string>editable</string> </key> + <value> <int>1</int> </value> + </item> + <item> + <key> <string>enabled</string> </key> + <value> <int>1</int> </value> + </item> + <item> + <key> <string>end</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>external_validator</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + <item> + <key> <string>extra</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>hidden</string> </key> + <value> <int>1</int> </value> + </item> + <item> + <key> <string>input_type</string> </key> + <value> <string>text</string> </value> + </item> + <item> + <key> <string>required</string> </key> + <value> <int>1</int> </value> + </item> + <item> + <key> <string>start</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string>Quantity</string> </value> + </item> + <item> + <key> <string>whitespace_preserve</string> </key> + <value> <int>0</int> </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="Method" module="Products.Formulator.MethodField"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>method_name</string> </key> + <value> <string>Validator_positiveNumber</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField/read_only_quantity.xml b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField/read_only_quantity.xml new file mode 100644 index 0000000000000000000000000000000000000000..b2b39b4c8abf1b88871ba94038578f9786dee514 --- /dev/null +++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/Foo_viewHiddenErrorneousField/read_only_quantity.xml @@ -0,0 +1,277 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="IntegerField" module="Products.Formulator.StandardFields"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>id</string> </key> + <value> <string>read_only_quantity</string> </value> + </item> + <item> + <key> <string>message_values</string> </key> + <value> + <dictionary> + <item> + <key> <string>external_validator_failed</string> </key> + <value> <string>The input failed the external validator.</string> </value> + </item> + <item> + <key> <string>integer_out_of_range</string> </key> + <value> <string>The integer you entered was out of range.</string> </value> + </item> + <item> + <key> <string>not_integer</string> </key> + <value> <string>You did not enter an integer.</string> </value> + </item> + <item> + <key> <string>required_not_found</string> </key> + <value> <string>Input is required but no input given.</string> </value> + </item> + </dictionary> + </value> + </item> + <item> + <key> <string>overrides</string> </key> + <value> + <dictionary> + <item> + <key> <string>alternate_name</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>css_class</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>default</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_maxwidth</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_width</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>editable</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>enabled</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>end</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>external_validator</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>extra</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>hidden</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>required</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>start</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>whitespace_preserve</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </value> + </item> + <item> + <key> <string>tales</string> </key> + <value> + <dictionary> + <item> + <key> <string>alternate_name</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>css_class</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>default</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_maxwidth</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_width</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>editable</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>enabled</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>end</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>external_validator</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>extra</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>hidden</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>required</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>start</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>whitespace_preserve</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </value> + </item> + <item> + <key> <string>values</string> </key> + <value> + <dictionary> + <item> + <key> <string>alternate_name</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>css_class</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>default</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_maxwidth</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>display_width</string> </key> + <value> <int>20</int> </value> + </item> + <item> + <key> <string>editable</string> </key> + <value> <int>0</int> </value> + </item> + <item> + <key> <string>enabled</string> </key> + <value> <int>1</int> </value> + </item> + <item> + <key> <string>end</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>external_validator</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>extra</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>hidden</string> </key> + <value> <int>0</int> </value> + </item> + <item> + <key> <string>input_type</string> </key> + <value> <string>text</string> </value> + </item> + <item> + <key> <string>required</string> </key> + <value> <int>0</int> </value> + </item> + <item> + <key> <string>start</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string>Read-Only Quantity</string> </value> + </item> + <item> + <key> <string>whitespace_preserve</string> </key> + <value> <int>0</int> </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <tuple> + <tuple> + <string>Products.Formulator.TALESField</string> + <string>TALESMethod</string> + </tuple> + <none/> + </tuple> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_text</string> </key> + <value> <string>python: here.getQuantity()</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_ui_test/bt/template_action_path_list b/bt5/erp5_ui_test/bt/template_action_path_list index ab3a5738406368a4fd069d3857eba657b6c263ea..b5da64f9515b3e76ad564ea04f9857bd3f65b95c 100644 --- a/bt5/erp5_ui_test/bt/template_action_path_list +++ b/bt5/erp5_ui_test/bt/template_action_path_list @@ -30,6 +30,7 @@ Foo | view_duration_field Foo | view_formbox Foo | view_formbox_dialog Foo | view_formbox_fooline +Foo | view_hidden_positive_only_quantity Foo | view_listbox Foo | view_multiple_listbox Foo | view_planning diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.js b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.js index 3439ba5aa9a53cf7a929709f6e4957521fcde367..e3ef5f7501440f5a8d104523cb341ab50ea26308 100644 --- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.js +++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.js @@ -171,12 +171,29 @@ form_definition = this.state.form_definition, rendered_document = erp5_document._embedded._view, group_list = form_definition.group_list, - form_gadget = this; + form_gadget = this, + tmp; if (modification_dict.hasOwnProperty('hash')) { form_gadget.props.gadget_list = []; } - + /* Update or remove h3 element based on value of `title` */ + if (modification_dict.hasOwnProperty('title')) { + tmp = this.element.querySelector("h3"); + if (modification_dict.title) { + if (tmp === null) { + // create new title element for existing title + tmp = document.createElement("h3"); + this.element.insertBefore(tmp, this.element.firstChild); + } + tmp.textContent = modification_dict.title; + } + if (modification_dict.title === null || modification_dict.title === "") { + // user tends to remove the title + if (tmp !== null) {tmp.remove(); } + } + tmp = undefined; + } return new RSVP.Queue() .push(function () { return RSVP.all(group_list.map(function (group) { diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.xml b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.xml index d69449eb580f739bbbc8295fca57736cb739a294..08a8d321921a26d2908b897d70597e2395052934 100644 --- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.xml +++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_form_js.xml @@ -230,7 +230,7 @@ </item> <item> <key> <string>serial</string> </key> - <value> <string>963.11788.48702.26146</string> </value> + <value> <string>964.25533.41108.47530</string> </value> </item> <item> <key> <string>state</string> </key> @@ -248,7 +248,7 @@ </tuple> <state> <tuple> - <float>1514393621.04</float> + <float>1515496577.67</float> <string>UTC</string> </tuple> </state> diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.js b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.js index ae07bd2cf7865316da8ac546234b4d300c0d417e..666ce8b3091b5f9edb17680188d5baa6eacead2f 100644 --- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.js +++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.js @@ -5,6 +5,7 @@ /** Return true if `field` resembles non-empty and non-editable field. */ function isGoodNonEditableField(field) { + if (field === undefined || field === null) {return false; } // ListBox and FormBox should always get a chance to render because they // can contain editable fields if (field.type === "ListBox") {return true; } @@ -47,6 +48,7 @@ .declareMethod('render', function (options) { var state_dict = { jio_key: options.jio_key, + title: options.title, view: options.view, editable: options.editable, erp5_document: options.erp5_document, @@ -80,6 +82,7 @@ form_options.erp5_document = gadget.state.erp5_document; form_options.form_definition = gadget.state.form_definition; form_options.view = gadget.state.view; + form_options.title = gadget.state.title; form_options.jio_key = gadget.state.jio_key; form_options.editable = 0; // because for editable=1 there is a special // page template 'pt_form_editable'. Once it is @@ -96,7 +99,7 @@ gadget.getUrlFor({command: 'selection_previous'}), gadget.getUrlFor({command: 'selection_next'}), gadget.getUrlFor({command: 'change', options: {page: "tab"}}), - gadget.state.erp5_document._links.action_object_jio_report ? + gadget.state.erp5_document._links.action_object_jio_report || gadget.state.erp5_document._links.action_object_print ? gadget.getUrlFor({command: 'change', options: {page: "export"}}) : "", calculatePageTitle(gadget, gadget.state.erp5_document) diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.xml b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.xml index 6faaffb918e176c16e67182068ba87a7747769a2..f071236f7e5e6b9011ca014dc05455b4356dfa65 100644 --- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.xml +++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_form_view_js.xml @@ -230,7 +230,7 @@ </item> <item> <key> <string>serial</string> </key> - <value> <string>964.44232.19748.18107</string> </value> + <value> <string>964.45882.29366.36147</string> </value> </item> <item> <key> <string>state</string> </key> @@ -248,7 +248,7 @@ </tuple> <state> <tuple> - <float>1515406785.95</float> + <float>1515593717.52</float> <string>UTC</string> </tuple> </state> diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.js b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.js index 20ebf6c4892f11c62de7529af4ab52addd9e3185..9d574c365f860e32c468e489117527ad07eb281f 100644 --- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.js +++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.js @@ -33,7 +33,7 @@ }; return form_gadget.render({erp5_document: erp5_document, form_definition: form_definition, - editable: 0}); + editable: 0, title: report_section.title}); }); } diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.xml b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.xml index 24108d8631828d5fbd784ed6d9060d546c16b902..c6ef7ce655d6e1b044ef8efc1ffabd51ae06189f 100644 --- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.xml +++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_pt_report_view_js.xml @@ -230,7 +230,7 @@ </item> <item> <key> <string>serial</string> </key> - <value> <string>961.16421.12334.2201</string> </value> + <value> <string>962.56167.53905.31470</string> </value> </item> <item> <key> <string>state</string> </key> @@ -248,7 +248,7 @@ </tuple> <state> <tuple> - <float>1502116518.17</float> + <float>1508400391.84</float> <string>UTC</string> </tuple> </state> diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_notification_zuite/testHiddenFieldError.xml b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_notification_zuite/testHiddenFieldError.xml new file mode 100644 index 0000000000000000000000000000000000000000..301f6b3fbd163db60db701f45562e0f4aae23c25 --- /dev/null +++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_notification_zuite/testHiddenFieldError.xml @@ -0,0 +1,58 @@ +<?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>testHiddenFieldError</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> diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_notification_zuite/testHiddenFieldError.zpt b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_notification_zuite/testHiddenFieldError.zpt new file mode 100644 index 0000000000000000000000000000000000000000..32685a7e4d5df752d528d241f76311e5fb45c3fb --- /dev/null +++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_notification_zuite/testHiddenFieldError.zpt @@ -0,0 +1,51 @@ +<html> +<head><title>Test Invoices Report Skin Allowance</title></head> +<body> +<table cellpadding="1" cellspacing="1" border="1"> +<thead> +<tr><th rowspan="1" colspan="4"> +Check that user gets notified if there is an error on a hidden field. +</th></tr> +</thead> + +<tbody> +<tal:block metal:use-macro="here/PTZuite_CommonTemplate/macros/init" /> +<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/1/?editable=1</td><td></td></tr> + +<!-- Originaly the field was required and we tested here an empty value. Problem is that Firefox + evaluates numerical rule before required value wheras Chrome does it in the opposite direction --> + +<!-- Put negative quantity so the external validator will not pass external test in the next view --> +<tr><td>waitForElementPresent</td> + <td>//input[@name="field_my_quantity"]</td><td></td></tr> +<tr><td>type</td> + <td>//input[@name="field_my_quantity"]</td> + <td>-20</td></tr> +<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/save" /> + +<!-- Let the external validator throw an error - this time we test explicitely + for a notification with the error --> +<tr><td>waitForElementPresent</td> + <td>//a[@data-i18n="Views"]</td><td></td></tr> +<tr><td>click</td> + <td>//a[@data-i18n="Views"]</td><td></td></tr> +<tr><td>waitForElementPresent</td> + <td>//a[@data-i18n="View Hidden Positive-Only Quantity"]</td><td></td></tr> +<tr><td>click</td> + <td>//a[@data-i18n="View Hidden Positive-Only Quantity"]</td><td></td></tr> +<tr><td>waitForElementPresent</td> + <td>//button[@data-i18n='Save']</td><td></td></tr> +<tr><td>click</td> + <td>//button[@data-i18n='Save']</td><td></td></tr> +<tr><td>waitForTextPresent</td> + <td>The input failed the external validator.</td><td></td></tr> + +</tbody> +</table> +</body> +</html> \ No newline at end of file