From 4c273cae1f05f12dde59aab92cbbdf18d9328886 Mon Sep 17 00:00:00 2001 From: Tomas Peterka <tomas.peterka@nexedi.com> Date: Fri, 20 Apr 2018 11:49:39 +0200 Subject: [PATCH] [hal_json_style] Refactor for clarity and bug fixes --- .../ERP5Document_getHateoas.py | 419 ++++++++---------- 1 file changed, 192 insertions(+), 227 deletions(-) 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 32636efa52..27e5624ebe 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 @@ -1,29 +1,35 @@ """Hello. This will be long because this godly script does almost everything. -In general, it always returns a JSON response in HATEOAS format specification. +It **always** return a JSON reponse in HATEOAS format specification. :param REQUEST: HttpRequest holding GET and/or POST data :param response: :param view: either "view" or absolute URL of an ERP5 Action :param mode: {str} help to decide what user wants from us "form" | "search" ... :param relative_url: an URL of `traversed_document` to operate on (it must have an object_view) +:param portal_status_message: {str} message to be displayed on the user +:param portal_status_level: {str|int} severity of the message using ERP5Type.Log levels or their names like 'info', 'warn', 'error' -Only in mode == 'search' +Parameters for mode == 'search' :param query: string-serialized Query :param select_list: list of strings to select from search result object :param limit: tuple(start_index, num_records) which is further passed to list_method BUT not every list_method takes it into account :param form_relative_url: {str} relative URL of a form FIELD issuing the search (listbox/relation field...) it can be None in case of special listboxes like List of Modules or relative path like "portal_skins/erp5_ui_test/FooModule_viewFooList/listbox" +:param default_param_json: {str} BASE64 encoded JSON with parameters intended for the list_method + :param .form_id: In case of page_template = "form" it will be similar to form_relative_url with the exception that it contains + only the form name (e.g. FooModule_viewFooList). In case of dialogs it points to the previous form which is + often more important than the dialog form. -Only in mode == 'form' -:param form: +Parameters for mode == 'form' +:param form: {instace} of a form - obviously this call can be only internal (Script-to-Script) -Only in mode == 'traverse' +Parameters for mode == 'traverse' Traverse renders arbitrary View. It can be a Form or a Script. :param relative_url: string, MANDATORY for obtaining the traversed_document. Calling this script directly on an object should be forbidden in code (but it is not now). - +:param view: {str} mandatory. the view reference as defined on a Portal Type (e.g. "view" or "publish_view") # Form When handling form, we can expect field values to be stored in REQUEST.form in two forms @@ -56,11 +62,13 @@ if REQUEST is None: if response is None: response = REQUEST.RESPONSE + def isFieldType(field, type_name): if field.meta_type == 'ProxyField': field = field.getRecursiveTemplateField() return field.meta_type == type_name + def toBasicTypes(obj): """Ensure that obj contains only basic types.""" if obj is None: @@ -79,7 +87,8 @@ def toBasicTypes(obj): log('Cannot convert {!s} to basic types {!s}'.format(type(obj), obj), level=100) return obj -def addHiddenFieldToForm(form, name, value): + +def renderHiddenField(form, name, value): if form == {}: form['_embedded'] = {} form['_embedded']['_view'] = {} @@ -90,7 +99,7 @@ def addHiddenFieldToForm(form, name, value): field_dict = form field_dict[name] = { - "type": "StringField", + "type": "StringField", # must be string field because only this gets send when non-editable "key": name, "default": value, "editable": 0, @@ -101,6 +110,7 @@ def addHiddenFieldToForm(form, name, value): "required": 1, } + # http://stackoverflow.com/a/13105359 def byteify(string): if isinstance(string, dict): @@ -440,12 +450,12 @@ url_template_dict = { "form_action": "%(traversed_document_url)s/%(action_id)s", "traverse_generator": "%(root_url)s/%(script_id)s?mode=traverse" + \ "&relative_url=%(relative_url)s&view=%(view)s", - "traverse_generator_non_view": "%(root_url)s/%(script_id)s?mode=traverse" + \ - "&relative_url=%(relative_url)s&view=%(view)s&form_id=%(form_id)s", - "traverse_generator_with_parameter": "%(root_url)s/%(script_id)s?mode=traverse" + \ + "traverse_generator_action": "%(root_url)s/%(script_id)s?mode=traverse" + \ "&relative_url=%(relative_url)s&view=%(view)s&extra_param_json=%(extra_param_json)s", "traverse_template": "%(root_url)s/%(script_id)s?mode=traverse" + \ "{&relative_url,view}", + + # Search template will call standard "searchValues" on a document described by `root_url` "search_template": "%(root_url)s/%(script_id)s?mode=search" + \ "{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}", "worklist_template": "%(root_url)s/%(script_id)s?mode=worklist", @@ -455,6 +465,9 @@ url_template_dict = { "&list_method=%(list_method)s" \ "&default_param_json=%(default_param_json)s" \ "{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}", + # Non-editable searches suppose the search results will be rendered as-is and no template + # fields will get involved. Unfortunately, fields need to be resolved because of formatting + # all the time so we abandoned this no_editable version "custom_search_template_no_editable": "%(root_url)s/%(script_id)s?mode=search" + \ "&relative_url=%(relative_url)s" \ "&list_method=%(list_method)s" \ @@ -480,6 +493,35 @@ def getRealRelativeUrl(document): return '/'.join(portal.portal_url.getRelativeContentPath(document)) +def parseActionUrl(url): + """Parse usual ERP5 Action URL into components: ~root, context~, view_id, param_dict, url. + + :param url: {str} is expected to be in form https://<site_root>/context/view_id?optional=params + """ + param_dict = {} + url_and_params = url.split(site_root.absolute_url())[-1].split('?') + _, script = url_and_params[0].strip("/ ").rsplit('/', 1) + if len(url_and_params) > 1: + for param in url_and_params[1].split('&'): + param_name, param_value = param.split('=') + if "+" in param_value: + param_value = param_value.replace("+", " ") + if ":" in param_name: + param_name, param_type = param_name.split(":") + if param_type == "int": + param_value = int(param_value) + elif param_type == "bool": + param_value = True if param_value.lower() in ("true", "1") else False + else: + raise ValueError("Cannot convert param {}={} to type {}. Feel free to add implemetation at the position of this exception.".format( + param_name, param_value, param_type)) + param_dict[param_name] = param_value + return { + 'view_id': script, + 'params': param_dict, + 'url': url + } + def getFormRelativeUrl(form): return portal.portal_catalog( portal_type=("ERP5 Form", "ERP5 Report"), @@ -491,10 +533,15 @@ def getFormRelativeUrl(form): def getFieldDefault(form, field, key, value=None): - """Get available value for `field` preferably in python-object from REQUEST or from field's default.""" + """Get available value for `field` preferably in python-object from REQUEST or from field's default. + + Previously we used Formulator.Field._get_default which is (for no reason) private. + """ if value is None: - value = (REQUEST.form.get(field.id, REQUEST.form.get(key, None)) or - field.get_value('default', request=REQUEST, REQUEST=REQUEST)) + value = REQUEST.get(key, MARKER) + # use marker because default value can be intentionally empty string + if value is MARKER: + value = field.get_value('default', request=REQUEST, REQUEST=REQUEST) if field.has_value("unicode") and field.get_value("unicode") and isinstance(value, unicode): value = unicode(value, form.get_form_encoding()) if getattr(value, 'translate', None) is not None: @@ -770,7 +817,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key # 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 @@ -905,12 +951,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key formbox_context = traversed_document if field.get_value('context_method_id'): # harness acquisition and call the method right away - formbox_context = getattr(traversed_document, field.get_value('context_method_id'))() + formbox_context = getattr(traversed_document, field.get_value('context_method_id'))( + field=field, REQUEST=REQUEST) embedded_document['_debug'] = "Different context" - - embeded_form = getattr(formbox_context, field.get_value('formbox_target_id')) + # get embedded form definition + embedded_form = getattr(formbox_context, field.get_value('formbox_target_id')) # renderForm mutates `embedded_document` therefor no return/assignment - renderForm(formbox_context, embeded_form, embedded_document, key_prefix=key) + renderForm(formbox_context, embedded_form, embedded_document, key_prefix=key) # fix editability which is hard-coded to 0 in `renderForm` implementation embedded_document['form_id']['editable'] = field.get_value("editable") @@ -944,7 +991,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti """ Render a `form` in plain python dict. - This function sets varibles 'here' and 'form_id' resp. 'dialog_id' for forms resp. form dialogs to REQUEST. + This function sets variables 'here' and 'form_id' resp. 'dialog_id' for forms resp. form dialogs to REQUEST. Any other REQUEST mingling are at the responsability of the callee. :param selection_params: holds parameters to construct ERP5Form.Selection instance @@ -957,7 +1004,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti # Following pop/push of form_id resp. dialog_id is here because of FormBox - an embedded form in a form # Fields of forms use form_id in their TALES expressions and obviously FormBox's form_id is different - # from its parent's form + # from its parent's form. It is very important that we do not remove form_id in case of a Dialog Form. if form.pt == "form_dialog": previous_request_other['dialog_id'] = REQUEST.other.pop('dialog_id', None) REQUEST.set('dialog_id', form.id) @@ -967,7 +1014,6 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti field_errors = REQUEST.get('field_errors', {}) - #hardcoded include_action = True if form.pt == 'form_dialog': action_to_call = "Base_callDialogMethod" @@ -989,6 +1035,14 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti "method": form.method, } } + + if form.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 = extra_param_json.pop("form_id", "") or REQUEST.get("form_id", "") + except AttributeError: + pass + # Form traversed_document response_dict['_links']['traversed_document'] = { "href": default_document_uri_template % { @@ -1026,25 +1080,25 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti response_dict[field.id] = renderField(traversed_document, field, form, key_prefix=key_prefix, selection_params=selection_params, extra_param_json=extra_param_json) if field_errors.has_key(field.id): response_dict[field.id]["error_text"] = field_errors[field.id].error_text - except AttributeError: + except AttributeError as error: # Do not crash if field configuration is wrong. - pass + log("Field {} rendering failed because of {!s}".format(field.id, error), level=800) - # Form Edit handler uses form_id to recover the submitted form. - # Form Dialog handler uses 'dialog_id' instead and 'form_id' - # - Some dialog actions (e.g. Print) uses form_id to obtain previous view form - if (form.pt == 'form_dialog'): - addHiddenFieldToForm(response_dict, 'dialog_id', form.id) + # Form Edit handler uses form_id to recover the submitted form and to control its + # properties like editability + if form.pt == 'form_dialog': # overwrite "form_id" field's value because old UI does that by passing # the form_id in query string and hidden fields - if REQUEST.get('form_id', None): - addHiddenFieldToForm(response_dict, "form_id", REQUEST.get('form_id')) - # some dialog actions (Print Module) use previous selection name - if REQUEST.get('selection_name', None): - addHiddenFieldToForm(response_dict, "selection_name", REQUEST.get('selection_name')) + renderHiddenField(response_dict, "form_id", REQUEST.get('form_id') or form.id) + # dialog_id is a mandatory field in any form_dialog + renderHiddenField(response_dict, 'dialog_id', form.id) + # some dialog actions use custom cancel_url + if REQUEST.get('cancel_url', None): + renderHiddenField(response_dict, "cancel_url", REQUEST.get('cancel_url')) + else: # In form_view we place only form_id in the request form - addHiddenFieldToForm(response_dict, 'form_id', form.id) + renderHiddenField(response_dict, 'form_id', form.id) if (form.pt == 'report_view'): # reports are expected to return list of ReportSection which is a wrapper @@ -1135,52 +1189,27 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti response_dict['report_section_list'] = report_result_list # end-if report_section + if form.pt == "form_dialog": + # Insert hash of current values into the form so scripts can see whether data has + # changed if they provide multi-step process + if form_data: + extra_param_json["form_hash"] = form.hash_validated_data(form_data) + + # extra_param_json is a special field in forms (just like form_id). extra_param_json field holds JSON + # metadata about the form (its hash and dynamic fields) + renderHiddenField(response_dict, 'extra_param_json', json.dumps(extra_param_json)) + 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): - meta_type = field.meta_type - - return { - "meta_type": field.meta_type - } - - - if meta_type == "MethodField": - result = { - "meta_type": field.meta_type - } - else: - result = { - "meta_type": field.meta_type, - "_values": field.values, - # XXX TALES expression is not JSON serializable by default - # "_tales": field.tales - "_overrides": field.overrides - } - if meta_type == "ProxyField": - result['_delegated_list'] = field.delegated_list -# try: -# result['_delegated_list'].pop('list_method') -# except KeyError: -# pass - - # XXX ListMethod is not JSON serialized by default - try: - result['_values'].pop('list_method') - except KeyError: - pass - try: - result['_overrides'].pop('list_method') - except KeyError: - pass - return result - def renderFormDefinition(form, response_dict): - """Form "definition" is configurable in Zope admin: Form -> Order.""" + """Form "definition" is configurable in Zope admin: Form -> Order. + + We add some known constants inside Forms such as form_id and into + Dialog Form such as dialog_id. + """ group_list = [] for group in form.Form_getGroupTitleAndId(): @@ -1188,7 +1217,7 @@ def renderFormDefinition(form, response_dict): field_list = [] for field in form.get_fields_in_group(group['goid'], include_disabled=1): - field_list.append((field.id, renderRawField(field))) + field_list.append((field.id, {'meta_type': field.meta_type})) group_list.append((group['gid'], field_list)) @@ -1294,22 +1323,26 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ) response.setStatus(401) return "" - + elif mime_type != traversed_document.Base_handleAcceptHeader([mime_type]): response.setStatus(406) return "" - - + elif (mode == 'root') or (mode == 'traverse'): - ################################################# - # Raw document - ################################################# + ## + # Render ERP Document with a `view` specified + # `view` contains view's name and we extract view's URL (we suppose form ${object_url}/Form_view) + # which after expansion gives https://<site-root>/context/view_id?optional=params + if (REQUEST is not None) and (REQUEST.other['method'] != "GET"): response.setStatus(405) return "" + # Default properties shared by all ERP5 Document and Site - action_dict = {} - # result_dict['_relative_url'] = traversed_document.getRelativeUrl() + current_action = {} # current action parameters (context, script, URL params) + action_dict = {} # actions available on current `traversed_document` + last_form_id = None # will point to the previous form so we can obtain previous selection + result_dict['title'] = traversed_document.getTitle() # Add a link to the portal type if possible @@ -1342,65 +1375,44 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, "name": Base_translateString(container.getTitle()), } - # Extract embedded form in the document view - embedded_url = None - + # Find current action URL and extract embedded view erp5_action_dict = portal.Base_filterDuplicateActions( portal.portal_actions.listFilteredActionsFor(traversed_document)) - for erp5_action_key in erp5_action_dict.keys(): for view_action in erp5_action_dict[erp5_action_key]: # Try to embed the form in the result if (view == view_action['id']): - embedded_url = '%s' % view_action['url'] - - # `form_id` should be actually called `dialog_id` in case of form dialogs - # so real form_id of a previous view stays untouched. - # Here we save previous form_id to `last_form_id` so it does not get overriden by `dialog_id` - last_form_id = REQUEST.get('form_id', "") if REQUEST is not None else "" - form_id = "" - if (embedded_url is not None): - # XXX Try to fetch the form in the traversed_document of the document - # Of course, this code will completely crash in many cases (page template - # instead of form, unexpected action TALES expression). Happy debugging. - # renderer_form_relative_url = view_action['url'][len(portal.absolute_url()):] - form_id = embedded_url.split('?', 1)[0].split("/")[-1] - # 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) - if (renderer_form is not None): + current_action = parseActionUrl('%s' % view_action['url']) # current action/view being rendered + # If we have current action definition we are able to render embedded view + # which should be a "ERP5 Form" but in reality can be anything + if current_action.get('view_id', ''): + view_instance = getattr(traversed_document, current_action['view_id']) + if (view_instance is not None): embedded_dict = { '_links': { 'self': { - 'href': embedded_url + 'href': current_action['url'] } } } # 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('=') - # 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 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(): REQUEST.set(query_key, query_value) - # Embedded Form can be a Script or even a class method thus we mitigate here + + # If our "form" is actually a Script (nothing is sure in ERP5) then execute it here try: - if "Script" in renderer_form.meta_type: + if "Script" in view_instance.meta_type: # we suppose that the script takes only what is given in the URL params - return renderer_form(**query_param_dict) + 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 = renderer_form(**query_param_dict) + returned_value = view_instance(**current_action['params']) # returned value is usually REQUEST.RESPONSE.redirect() log('ERP5Document_getHateoas', 'HAL_JSON cannot handle returned value "{!s}" from {}({!s})'.format( - returned_value, form_id, query_param_dict), 100) + returned_value, current_action['view_id'], current_action['params']), 100) status_message = Base_translateString('Operation executed') if isinstance(returned_value, (str, unicode)) and returned_value.startswith('http'): parsed_url = urlparse(returned_value) @@ -1437,14 +1449,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, # select correct URL template based on action_type and form page template url_template_key = "traverse_generator" if erp5_action_key not in ("view", "object_view", "object_jio_view"): - # previous view's form_id required almost everything but other views - url_template_key = "traverse_generator_non_view" - # XXX This line is only optimization for shorter URL and thus is ugly - if not (form_id or last_form_id): + url_template_key = "traverse_generator_action" + # but when we do not have the last form id we do not pass is of course + if not (current_action.get('view_id', '') or last_form_id): url_template_key = "traverse_generator" erp5_action_list[-1]['href'] = url_template_dict[url_template_key] % { "root_url": site_root.absolute_url(), - "script_id": script.id, + "script_id": script.id, # this script (ERP5Document_getHateoas) "relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"), "view": erp5_action_list[-1]['name'], "form_id": form_id if form_id and renderer_form.pt == "form_view" else last_form_id @@ -1662,7 +1673,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, elif sort_order.lower().startswith("desc"): sort_order = "DESC" else: - # should raise an ValueError instead + # should raise a ValueError instead log('Wrong sort order "{}" in {}! It must start with "asc" or "desc"'.format(sort_order, form_relative_url), level=200) # error return (sort_col, sort_order) @@ -1685,7 +1696,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, # # for k, v in catalog_kw.items(): # REQUEST.set(k, v) - search_result_iterable = callable_list_method(**catalog_kw) # Cast to list if only one element is provided @@ -1721,9 +1731,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, if 'selection' not in catalog_kw: catalog_kw['selection'] = context.getPortalObject().portal_selections.getSelectionFor(selection_name, REQUEST) - # field TALES expression evaluated by Base_getRelatedObjectParameter requires that - REQUEST.other['form_id'] = listbox_form.id - for select in select_list: # See Listbox.py getValueList --> getEditableField & getColumnAliasList method # In short: there are Form Field definitions which names start with @@ -1817,7 +1824,9 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, # 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, {}) - contents_item[select] = renderField( + # If the contents_item has field rendering in it, better is to add an + # extra layer of abstraction to not get conflicts + contents_item[select]['field_gadget_param'] = renderField( traversed_document, editable_field_dict[select], listbox_form, @@ -1830,107 +1839,70 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, # a key name to Python Script with variable number of input parameters. contents_item[select] = getAttrFromAnything(search_result, select, property_getter, {'brain': search_result}) - # If the contents_item has field rendering in it, better is to add an - # extra layer of abstraction to not get conflicts - if isinstance(contents_item[select], dict): - contents_item[select] = { - 'field_gadget_param': contents_item[select], - } - # By default, we won't be generating views in the URL - generate_view = False - url_parameter_dict = {} + url_parameter_dict = None if select in url_column_dict: # Check if we get URL parameters using listbox field `url_columns` - url_column_method = getattr(search_result, url_column_dict[select], None) - # If there is empty url_column_method, do nothing and continue. This - # will create no URL in these cases - if url_column_method is None: - continue - url_parameter_dict = url_column_method( - url_dict=True, - brain=search_result, - selection=catalog_kw['selection'], - selection_name=catalog_kw['selection_name'], - column_id=select) - # Since, now we are using url_columns for both XHTML UI and renderJS UI, - # its normal to get string as a result of the `url_column_method` - # function call. In that cases, we will do nothing. Take note, the - # result of `url_column_method` function call which is usable here should - # be dictionary in the format :- - # { - # 'command': <command_name, ex: 'raw', 'push_history'>, - # 'options': { - # 'url': <Absolute URL>, - # 'jio_key': <Relative URL of object>, - # 'view': <id of the view>, - # } - # } - if isinstance(url_parameter_dict, str): - continue + try: + url_column_method = getattr(search_result, url_column_dict[select]) + # Result of `url_column_method` must be a dictionary in the format + # {'command': <command_name, ex: 'raw', 'push_history'>, + # 'options': {'url': <Absolute URL>, 'jio_key': <Relative URL of object>, 'view': <id of the view>}} + url_parameter_dict = url_column_method(url_dict=True, + brain=search_result, + selection=catalog_kw['selection'], + selection_name=catalog_kw['selection_name'], + column_id=select) + except AttributeError as e: + if url_column_dict[select]: + log("Invalid URL method {!s} on column {}".format(url_column_dict[select], select), level=800) elif getattr(search_result, 'getListItemUrlDict', None) is not None: # Check if we can get URL result from the brain try: url_parameter_dict = search_result.getListItemUrlDict( - select, - result_index, - catalog_kw['selection_name'] - ) + select, result_index, catalog_kw['selection_name'] + ) except (ConflictError, RuntimeError): raise except: log('could not evaluate the url method getListItemUrlDict with %r' % search_result, level=800) - continue - - else: - # Continue in case there is no url_column_dict or brain to get URL - continue - - url_result_dict = { - select: url_parameter_dict - } - - # If contents item don't have `field_gadget_param` in it, then add it - # to default - if not isinstance(contents_item[select], dict): - contents_item[select] = { - 'default': contents_item[select], - } - - contents_item[select].update({'url_value': url_result_dict[select]}) - - if contents_item[select]['url_value']: + if isinstance(url_parameter_dict, dict): + # We need to put URL into rendered field so just ensure it is a dict + if not isinstance(contents_item[select], dict): + contents_item[select] = { + 'default': contents_item[select], + 'editable': False + } # We should be generating view if there is extra params for view in # view_kw. These parameters are required to create url at hateoas side # using the URL template as necessary - if 'view_kw' in contents_item[select]['url_value']: - generate_view = True + if 'view_kw' not in url_parameter_dict: + contents_item[select]['url_value'] = url_parameter_dict + else: # Get extra parameters either from url_result_dict or from brain - extra_url_param_dict = contents_item[select]['url_value']['view_kw'].get('extra_param_json', {}) - - if generate_view: + extra_url_param_dict = url_parameter_dict['view_kw'].get('extra_param_json', {}) url_template_id = 'traverse_generator' if extra_url_param_dict: - url_template_id = 'traverse_generator_with_parameter' - - contents_item[select]['url_value']['options']['view'] =\ - url_template_dict[url_template_id] % { - "root_url": site_root.absolute_url(), - "script_id": script.id, - "relative_url": contents_item[select]['url_value']['view_kw']['jio_key'].replace("/", "%2F"), - "view": contents_item[select]['url_value']['view_kw']['view'], - "extra_param_json": urlsafe_b64encode( - json.dumps(ensureSerializable(extra_url_param_dict))) + url_template_id = 'traverse_generator_action' + + contents_item[select]['url_value'] = { + 'command': url_parameter_dict['command'], + 'options': { + 'jio_key': url_parameter_dict.get('options', {}).get('jio_key', url_parameter_dict['view_kw']['jio_key']), + 'editable': url_parameter_dict.get('options', {}).get('editable', None), + 'view': url_template_dict[url_template_id] % { + "root_url": site_root.absolute_url(), + "script_id": script.id, + "relative_url": url_parameter_dict['view_kw']['jio_key'].replace("/", "%2F"), + "view": url_parameter_dict['view_kw']['view'], + "extra_param_json": urlsafe_b64encode( + json.dumps(ensureSerializable(extra_url_param_dict))) + } } - - # Its better to remove the 'view_kw' from the dictionary as it doesn't - # serve any purpose further in the result_dict - if 'view_kw' in contents_item[select]['url_value']: - del contents_item[select]['url_value']['view_kw'] - + } # endfor select REQUEST.other.pop('cell', None) contents_list.append(contents_item) @@ -1943,30 +1915,23 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, source_field_meta_type = source_field.getRecursiveTemplateField().meta_type # Lets mingle with editability of fields here - # Original Listbox.py modifies editability during field rendering (method `render`) - # which we cannot use here so we overwrite result's own editability + # Original Listbox.py modifies editability during field rendering (method `render`), + # which is done on frontend side, so we overwrite result's own editability if source_field is not None and source_field_meta_type == "ListBox": - # XXX TODO: should take into account "editable_columns" from listbox own selection editable_column_set = set(name for name, _ in source_field.get_value("editable_columns")) for line in result_dict['_embedded']['contents']: for select in line: # forbid editability only for fields not specified in editable_columns if select in editable_column_set: continue - if (isinstance(line[select], dict) and - line[select].get('field_gadget_param', None) is not None): - if line[select]['field_gadget_param'].get('editable'): - line[select]['field_gadget_param']['editable'] = False + if isinstance(line[select], dict) and 'field_gadget_param' in line[select]: + line[select]['field_gadget_param']['editable'] = False if source_field is not None and source_field_meta_type == "ListBox": # Trigger count method if exist # XXX No need to count if no pagination - count_method = source_field.get_value('count_method') or None - count_method_name = count_method.getMethodName() if count_method is not None else "" - - # Only try to get count method results in case method name exists, in all - # other cases, just pass. - if count_method_name != "" and count_method_name != list_method: + count_method = source_field.get_value('count_method') + if count_method != "" and count_method.getMethodName() != list_method: count_kw = dict(catalog_kw) # Drop not needed parameters count_kw.pop('selection', None) @@ -1974,7 +1939,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, count_kw.pop("sort_on", None) count_kw.pop("limit", None) try: - count_method = getattr(traversed_document, count_method_name) + count_method = getattr(traversed_document, count_method.getMethodName()) count_method_result = count_method(REQUEST=REQUEST, **count_kw) result_dict['_embedded']['count'] = ensureSerializable(count_method_result[0][0]) except AttributeError as error: @@ -1983,7 +1948,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, # and just pass. This also ensures we have compatibilty with how old # UI behave in these cases. log('Invalid count method %s' % error, level=800) - pass contents_stat_list = [] # in case the search was issued by listbox we can provide results of @@ -1992,6 +1956,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, stat_method = source_field.get_value('stat_method') stat_columns = source_field.get_value('stat_columns') + contents_stat = {} if len(stat_columns) > 0: # prefer stat per column (follow original ListBox.py implementation) -- 2.30.9