Commit 4c273cae authored by Tomáš Peterka's avatar Tomáš Peterka Committed by Tomáš Peterka

[hal_json_style] Refactor for clarity and bug fixes

parent 14ebe535
"""Hello. This will be long because this godly script does almost everything. """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 REQUEST: HttpRequest holding GET and/or POST data
:param response: :param response:
:param view: either "view" or absolute URL of an ERP5 Action :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 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 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 query: string-serialized Query
:param select_list: list of strings to select from search result object :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 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...) :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 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" 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' Parameters for mode == 'form'
:param 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. 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 :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). 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 # Form
When handling form, we can expect field values to be stored in REQUEST.form in two forms 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: ...@@ -56,11 +62,13 @@ if REQUEST is None:
if response is None: if response is None:
response = REQUEST.RESPONSE response = REQUEST.RESPONSE
def isFieldType(field, type_name): def isFieldType(field, type_name):
if field.meta_type == 'ProxyField': if field.meta_type == 'ProxyField':
field = field.getRecursiveTemplateField() field = field.getRecursiveTemplateField()
return field.meta_type == type_name return field.meta_type == type_name
def toBasicTypes(obj): def toBasicTypes(obj):
"""Ensure that obj contains only basic types.""" """Ensure that obj contains only basic types."""
if obj is None: if obj is None:
...@@ -79,7 +87,8 @@ def toBasicTypes(obj): ...@@ -79,7 +87,8 @@ def toBasicTypes(obj):
log('Cannot convert {!s} to basic types {!s}'.format(type(obj), obj), level=100) log('Cannot convert {!s} to basic types {!s}'.format(type(obj), obj), level=100)
return obj return obj
def addHiddenFieldToForm(form, name, value):
def renderHiddenField(form, name, value):
if form == {}: if form == {}:
form['_embedded'] = {} form['_embedded'] = {}
form['_embedded']['_view'] = {} form['_embedded']['_view'] = {}
...@@ -90,7 +99,7 @@ def addHiddenFieldToForm(form, name, value): ...@@ -90,7 +99,7 @@ def addHiddenFieldToForm(form, name, value):
field_dict = form field_dict = form
field_dict[name] = { field_dict[name] = {
"type": "StringField", "type": "StringField", # must be string field because only this gets send when non-editable
"key": name, "key": name,
"default": value, "default": value,
"editable": 0, "editable": 0,
...@@ -101,6 +110,7 @@ def addHiddenFieldToForm(form, name, value): ...@@ -101,6 +110,7 @@ def addHiddenFieldToForm(form, name, value):
"required": 1, "required": 1,
} }
# http://stackoverflow.com/a/13105359 # http://stackoverflow.com/a/13105359
def byteify(string): def byteify(string):
if isinstance(string, dict): if isinstance(string, dict):
...@@ -440,12 +450,12 @@ url_template_dict = { ...@@ -440,12 +450,12 @@ url_template_dict = {
"form_action": "%(traversed_document_url)s/%(action_id)s", "form_action": "%(traversed_document_url)s/%(action_id)s",
"traverse_generator": "%(root_url)s/%(script_id)s?mode=traverse" + \ "traverse_generator": "%(root_url)s/%(script_id)s?mode=traverse" + \
"&relative_url=%(relative_url)s&view=%(view)s", "&relative_url=%(relative_url)s&view=%(view)s",
"traverse_generator_non_view": "%(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&form_id=%(form_id)s",
"traverse_generator_with_parameter": "%(root_url)s/%(script_id)s?mode=traverse" + \
"&relative_url=%(relative_url)s&view=%(view)s&extra_param_json=%(extra_param_json)s", "&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" + \ "traverse_template": "%(root_url)s/%(script_id)s?mode=traverse" + \
"{&relative_url,view}", "{&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" + \ "search_template": "%(root_url)s/%(script_id)s?mode=search" + \
"{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}", "{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}",
"worklist_template": "%(root_url)s/%(script_id)s?mode=worklist", "worklist_template": "%(root_url)s/%(script_id)s?mode=worklist",
...@@ -455,6 +465,9 @@ url_template_dict = { ...@@ -455,6 +465,9 @@ url_template_dict = {
"&list_method=%(list_method)s" \ "&list_method=%(list_method)s" \
"&default_param_json=%(default_param_json)s" \ "&default_param_json=%(default_param_json)s" \
"{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}", "{&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" + \ "custom_search_template_no_editable": "%(root_url)s/%(script_id)s?mode=search" + \
"&relative_url=%(relative_url)s" \ "&relative_url=%(relative_url)s" \
"&list_method=%(list_method)s" \ "&list_method=%(list_method)s" \
...@@ -480,6 +493,35 @@ def getRealRelativeUrl(document): ...@@ -480,6 +493,35 @@ def getRealRelativeUrl(document):
return '/'.join(portal.portal_url.getRelativeContentPath(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): def getFormRelativeUrl(form):
return portal.portal_catalog( return portal.portal_catalog(
portal_type=("ERP5 Form", "ERP5 Report"), portal_type=("ERP5 Form", "ERP5 Report"),
...@@ -491,10 +533,15 @@ def getFormRelativeUrl(form): ...@@ -491,10 +533,15 @@ def getFormRelativeUrl(form):
def getFieldDefault(form, field, key, value=None): 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: if value is None:
value = (REQUEST.form.get(field.id, REQUEST.form.get(key, None)) or value = REQUEST.get(key, MARKER)
field.get_value('default', request=REQUEST, REQUEST=REQUEST)) # 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): if field.has_value("unicode") and field.get_value("unicode") and isinstance(value, unicode):
value = unicode(value, form.get_form_encoding()) value = unicode(value, form.get_form_encoding())
if getattr(value, 'translate', None) is not None: if getattr(value, 'translate', None) is not None:
...@@ -770,7 +817,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -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 # portal_type list can be overriden by selection too
# since it can be intentionally empty we don't override with non-empty field value # 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')) 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 # 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 # see https://lab.nexedi.com/nexedi/erp5/blob/HEAD/product/ERP5Form/ListBox.py#L1004
# implemented in javascript in the end # implemented in javascript in the end
...@@ -905,12 +951,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -905,12 +951,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
formbox_context = traversed_document formbox_context = traversed_document
if field.get_value('context_method_id'): if field.get_value('context_method_id'):
# harness acquisition and call the method right away # 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" embedded_document['_debug'] = "Different context"
# get embedded form definition
embeded_form = getattr(formbox_context, field.get_value('formbox_target_id')) embedded_form = getattr(formbox_context, field.get_value('formbox_target_id'))
# renderForm mutates `embedded_document` therefor no return/assignment # 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 # fix editability which is hard-coded to 0 in `renderForm` implementation
embedded_document['form_id']['editable'] = field.get_value("editable") embedded_document['form_id']['editable'] = field.get_value("editable")
...@@ -944,7 +991,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -944,7 +991,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
""" """
Render a `form` in plain python dict. 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. Any other REQUEST mingling are at the responsability of the callee.
:param selection_params: holds parameters to construct ERP5Form.Selection instance :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 ...@@ -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 # 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 # 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": if form.pt == "form_dialog":
previous_request_other['dialog_id'] = REQUEST.other.pop('dialog_id', None) previous_request_other['dialog_id'] = REQUEST.other.pop('dialog_id', None)
REQUEST.set('dialog_id', form.id) REQUEST.set('dialog_id', form.id)
...@@ -967,7 +1014,6 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -967,7 +1014,6 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
field_errors = REQUEST.get('field_errors', {}) field_errors = REQUEST.get('field_errors', {})
#hardcoded
include_action = True include_action = True
if form.pt == 'form_dialog': if form.pt == 'form_dialog':
action_to_call = "Base_callDialogMethod" action_to_call = "Base_callDialogMethod"
...@@ -989,6 +1035,14 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -989,6 +1035,14 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
"method": form.method, "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 # Form traversed_document
response_dict['_links']['traversed_document'] = { response_dict['_links']['traversed_document'] = {
"href": default_document_uri_template % { "href": default_document_uri_template % {
...@@ -1026,25 +1080,25 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -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) 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): if field_errors.has_key(field.id):
response_dict[field.id]["error_text"] = field_errors[field.id].error_text 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. # 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 Edit handler uses form_id to recover the submitted form and to control its
# Form Dialog handler uses 'dialog_id' instead and 'form_id' # properties like editability
# - Some dialog actions (e.g. Print) uses form_id to obtain previous view form if form.pt == 'form_dialog':
if (form.pt == 'form_dialog'):
addHiddenFieldToForm(response_dict, 'dialog_id', form.id)
# overwrite "form_id" field's value because old UI does that by passing # overwrite "form_id" field's value because old UI does that by passing
# the form_id in query string and hidden fields # the form_id in query string and hidden fields
if REQUEST.get('form_id', None): renderHiddenField(response_dict, "form_id", REQUEST.get('form_id') or form.id)
addHiddenFieldToForm(response_dict, "form_id", REQUEST.get('form_id')) # dialog_id is a mandatory field in any form_dialog
# some dialog actions (Print Module) use previous selection name renderHiddenField(response_dict, 'dialog_id', form.id)
if REQUEST.get('selection_name', None): # some dialog actions use custom cancel_url
addHiddenFieldToForm(response_dict, "selection_name", REQUEST.get('selection_name')) if REQUEST.get('cancel_url', None):
renderHiddenField(response_dict, "cancel_url", REQUEST.get('cancel_url'))
else: else:
# In form_view we place only form_id in the request form # 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'): if (form.pt == 'report_view'):
# reports are expected to return list of ReportSection which is a wrapper # 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 ...@@ -1135,52 +1189,27 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
response_dict['report_section_list'] = report_result_list response_dict['report_section_list'] = report_result_list
# end-if report_section # 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(): for key, value in previous_request_other.items():
if value is not None: if value is not None:
REQUEST.set(key, value) 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): 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 = [] group_list = []
for group in form.Form_getGroupTitleAndId(): for group in form.Form_getGroupTitleAndId():
...@@ -1188,7 +1217,7 @@ def renderFormDefinition(form, response_dict): ...@@ -1188,7 +1217,7 @@ def renderFormDefinition(form, response_dict):
field_list = [] field_list = []
for field in form.get_fields_in_group(group['goid'], include_disabled=1): 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)) group_list.append((group['gid'], field_list))
...@@ -1294,22 +1323,26 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1294,22 +1323,26 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
) )
response.setStatus(401) response.setStatus(401)
return "" return ""
elif mime_type != traversed_document.Base_handleAcceptHeader([mime_type]): elif mime_type != traversed_document.Base_handleAcceptHeader([mime_type]):
response.setStatus(406) response.setStatus(406)
return "" return ""
elif (mode == 'root') or (mode == 'traverse'): 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"): if (REQUEST is not None) and (REQUEST.other['method'] != "GET"):
response.setStatus(405) response.setStatus(405)
return "" return ""
# Default properties shared by all ERP5 Document and Site # Default properties shared by all ERP5 Document and Site
action_dict = {} current_action = {} # current action parameters (context, script, URL params)
# result_dict['_relative_url'] = traversed_document.getRelativeUrl() 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() result_dict['title'] = traversed_document.getTitle()
# Add a link to the portal type if possible # 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, ...@@ -1342,65 +1375,44 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
"name": Base_translateString(container.getTitle()), "name": Base_translateString(container.getTitle()),
} }
# Extract embedded form in the document view # Find current action URL and extract embedded view
embedded_url = None
erp5_action_dict = portal.Base_filterDuplicateActions( erp5_action_dict = portal.Base_filterDuplicateActions(
portal.portal_actions.listFilteredActionsFor(traversed_document)) portal.portal_actions.listFilteredActionsFor(traversed_document))
for erp5_action_key in erp5_action_dict.keys(): for erp5_action_key in erp5_action_dict.keys():
for view_action in erp5_action_dict[erp5_action_key]: for view_action in erp5_action_dict[erp5_action_key]:
# Try to embed the form in the result # Try to embed the form in the result
if (view == view_action['id']): if (view == view_action['id']):
embedded_url = '%s' % view_action['url'] 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
# `form_id` should be actually called `dialog_id` in case of form dialogs # which should be a "ERP5 Form" but in reality can be anything
# so real form_id of a previous view stays untouched. if current_action.get('view_id', ''):
# Here we save previous form_id to `last_form_id` so it does not get overriden by `dialog_id` view_instance = getattr(traversed_document, current_action['view_id'])
last_form_id = REQUEST.get('form_id', "") if REQUEST is not None else "" if (view_instance is not None):
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):
embedded_dict = { embedded_dict = {
'_links': { '_links': {
'self': { '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 # Put all query parameters (?reset:int=1&workflow_action=start_action) in request to mimic usual form display
query_param_dict = {} # Request is later used for method's arguments discovery so set URL params into REQUEST (just like it was sent by form)
query_split = embedded_url.split('?', 1) for query_key, query_value in current_action['params'].items():
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.set(query_key, query_value) 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: 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 # 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: except AttributeError:
# if renderer form does not have attr meta_type then it is not a document # 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. # 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() # returned value is usually REQUEST.RESPONSE.redirect()
log('ERP5Document_getHateoas', 'HAL_JSON cannot handle returned value "{!s}" from {}({!s})'.format( log('ERP5Document_getHateoas', 'HAL_JSON cannot handle returned value "{!s}" from {}({!s})'.format(
returned_value, form_id, query_param_dict), 100) returned_value, current_action['view_id'], current_action['params']), 100)
status_message = Base_translateString('Operation executed') status_message = Base_translateString('Operation executed')
if isinstance(returned_value, (str, unicode)) and returned_value.startswith('http'): if isinstance(returned_value, (str, unicode)) and returned_value.startswith('http'):
parsed_url = urlparse(returned_value) parsed_url = urlparse(returned_value)
...@@ -1437,14 +1449,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -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 # select correct URL template based on action_type and form page template
url_template_key = "traverse_generator" url_template_key = "traverse_generator"
if erp5_action_key not in ("view", "object_view", "object_jio_view"): 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_action"
url_template_key = "traverse_generator_non_view" # but when we do not have the last form id we do not pass is of course
# XXX This line is only optimization for shorter URL and thus is ugly if not (current_action.get('view_id', '') or last_form_id):
if not (form_id or last_form_id):
url_template_key = "traverse_generator" url_template_key = "traverse_generator"
erp5_action_list[-1]['href'] = url_template_dict[url_template_key] % { erp5_action_list[-1]['href'] = url_template_dict[url_template_key] % {
"root_url": site_root.absolute_url(), "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"), "relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"view": erp5_action_list[-1]['name'], "view": erp5_action_list[-1]['name'],
"form_id": form_id if form_id and renderer_form.pt == "form_view" else last_form_id "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, ...@@ -1662,7 +1673,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
elif sort_order.lower().startswith("desc"): elif sort_order.lower().startswith("desc"):
sort_order = "DESC" sort_order = "DESC"
else: 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), log('Wrong sort order "{}" in {}! It must start with "asc" or "desc"'.format(sort_order, form_relative_url),
level=200) # error level=200) # error
return (sort_col, sort_order) return (sort_col, sort_order)
...@@ -1685,7 +1696,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1685,7 +1696,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# #
# for k, v in catalog_kw.items(): # for k, v in catalog_kw.items():
# REQUEST.set(k, v) # REQUEST.set(k, v)
search_result_iterable = callable_list_method(**catalog_kw) search_result_iterable = callable_list_method(**catalog_kw)
# Cast to list if only one element is provided # 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, ...@@ -1721,9 +1731,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if 'selection' not in catalog_kw: if 'selection' not in catalog_kw:
catalog_kw['selection'] = context.getPortalObject().portal_selections.getSelectionFor(selection_name, REQUEST) 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: for select in select_list:
# See Listbox.py getValueList --> getEditableField & getColumnAliasList method # See Listbox.py getValueList --> getEditableField & getColumnAliasList method
# In short: there are Form Field definitions which names start with # 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, ...@@ -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 # 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, {}) 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, traversed_document,
editable_field_dict[select], editable_field_dict[select],
listbox_form, listbox_form,
...@@ -1830,107 +1839,70 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -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. # a key name to Python Script with variable number of input parameters.
contents_item[select] = getAttrFromAnything(search_result, select, property_getter, {'brain': search_result}) 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 # By default, we won't be generating views in the URL
generate_view = False url_parameter_dict = None
url_parameter_dict = {}
if select in url_column_dict: if select in url_column_dict:
# Check if we get URL parameters using listbox field `url_columns` # Check if we get URL parameters using listbox field `url_columns`
url_column_method = getattr(search_result, url_column_dict[select], None) try:
# If there is empty url_column_method, do nothing and continue. This url_column_method = getattr(search_result, url_column_dict[select])
# will create no URL in these cases # Result of `url_column_method` must be a dictionary in the format
if url_column_method is None: # {'command': <command_name, ex: 'raw', 'push_history'>,
continue # 'options': {'url': <Absolute URL>, 'jio_key': <Relative URL of object>, 'view': <id of the view>}}
url_parameter_dict = url_column_method( url_parameter_dict = url_column_method(url_dict=True,
url_dict=True, brain=search_result,
brain=search_result, selection=catalog_kw['selection'],
selection=catalog_kw['selection'], selection_name=catalog_kw['selection_name'],
selection_name=catalog_kw['selection_name'], column_id=select)
column_id=select) except AttributeError as e:
# Since, now we are using url_columns for both XHTML UI and renderJS UI, if url_column_dict[select]:
# its normal to get string as a result of the `url_column_method` log("Invalid URL method {!s} on column {}".format(url_column_dict[select], select), level=800)
# 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
elif getattr(search_result, 'getListItemUrlDict', None) is not None: elif getattr(search_result, 'getListItemUrlDict', None) is not None:
# Check if we can get URL result from the brain # Check if we can get URL result from the brain
try: try:
url_parameter_dict = search_result.getListItemUrlDict( url_parameter_dict = search_result.getListItemUrlDict(
select, select, result_index, catalog_kw['selection_name']
result_index, )
catalog_kw['selection_name']
)
except (ConflictError, RuntimeError): except (ConflictError, RuntimeError):
raise raise
except: except:
log('could not evaluate the url method getListItemUrlDict with %r' % search_result, log('could not evaluate the url method getListItemUrlDict with %r' % search_result,
level=800) 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 # 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 # view_kw. These parameters are required to create url at hateoas side
# using the URL template as necessary # using the URL template as necessary
if 'view_kw' in contents_item[select]['url_value']: if 'view_kw' not in url_parameter_dict:
generate_view = True contents_item[select]['url_value'] = url_parameter_dict
else:
# Get extra parameters either from url_result_dict or from brain # 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', {}) extra_url_param_dict = url_parameter_dict['view_kw'].get('extra_param_json', {})
if generate_view:
url_template_id = 'traverse_generator' url_template_id = 'traverse_generator'
if extra_url_param_dict: if extra_url_param_dict:
url_template_id = 'traverse_generator_with_parameter' url_template_id = 'traverse_generator_action'
contents_item[select]['url_value']['options']['view'] =\ contents_item[select]['url_value'] = {
url_template_dict[url_template_id] % { 'command': url_parameter_dict['command'],
"root_url": site_root.absolute_url(), 'options': {
"script_id": script.id, 'jio_key': url_parameter_dict.get('options', {}).get('jio_key', url_parameter_dict['view_kw']['jio_key']),
"relative_url": contents_item[select]['url_value']['view_kw']['jio_key'].replace("/", "%2F"), 'editable': url_parameter_dict.get('options', {}).get('editable', None),
"view": contents_item[select]['url_value']['view_kw']['view'], 'view': url_template_dict[url_template_id] % {
"extra_param_json": urlsafe_b64encode( "root_url": site_root.absolute_url(),
json.dumps(ensureSerializable(extra_url_param_dict))) "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 # endfor select
REQUEST.other.pop('cell', None) REQUEST.other.pop('cell', None)
contents_list.append(contents_item) contents_list.append(contents_item)
...@@ -1943,30 +1915,23 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -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 source_field_meta_type = source_field.getRecursiveTemplateField().meta_type
# Lets mingle with editability of fields here # Lets mingle with editability of fields here
# Original Listbox.py modifies editability during field rendering (method `render`) # Original Listbox.py modifies editability during field rendering (method `render`),
# which we cannot use here so we overwrite result's own editability # 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": 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")) editable_column_set = set(name for name, _ in source_field.get_value("editable_columns"))
for line in result_dict['_embedded']['contents']: for line in result_dict['_embedded']['contents']:
for select in line: for select in line:
# forbid editability only for fields not specified in editable_columns # forbid editability only for fields not specified in editable_columns
if select in editable_column_set: if select in editable_column_set:
continue continue
if (isinstance(line[select], dict) and if isinstance(line[select], dict) and 'field_gadget_param' in line[select]:
line[select].get('field_gadget_param', None) is not None): line[select]['field_gadget_param']['editable'] = False
if line[select]['field_gadget_param'].get('editable'):
line[select]['field_gadget_param']['editable'] = False
if source_field is not None and source_field_meta_type == "ListBox": if source_field is not None and source_field_meta_type == "ListBox":
# Trigger count method if exist # Trigger count method if exist
# XXX No need to count if no pagination # XXX No need to count if no pagination
count_method = source_field.get_value('count_method') or None count_method = source_field.get_value('count_method')
count_method_name = count_method.getMethodName() if count_method is not None else "" if count_method != "" and count_method.getMethodName() != list_method:
# 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_kw = dict(catalog_kw) count_kw = dict(catalog_kw)
# Drop not needed parameters # Drop not needed parameters
count_kw.pop('selection', None) count_kw.pop('selection', None)
...@@ -1974,7 +1939,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=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("sort_on", None)
count_kw.pop("limit", None) count_kw.pop("limit", None)
try: 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) count_method_result = count_method(REQUEST=REQUEST, **count_kw)
result_dict['_embedded']['count'] = ensureSerializable(count_method_result[0][0]) result_dict['_embedded']['count'] = ensureSerializable(count_method_result[0][0])
except AttributeError as error: except AttributeError as error:
...@@ -1983,7 +1948,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -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 # and just pass. This also ensures we have compatibilty with how old
# UI behave in these cases. # UI behave in these cases.
log('Invalid count method %s' % error, level=800) log('Invalid count method %s' % error, level=800)
pass
contents_stat_list = [] contents_stat_list = []
# in case the search was issued by listbox we can provide results of # 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, ...@@ -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_method = source_field.get_value('stat_method')
stat_columns = source_field.get_value('stat_columns') stat_columns = source_field.get_value('stat_columns')
contents_stat = {} contents_stat = {}
if len(stat_columns) > 0: if len(stat_columns) > 0:
# prefer stat per column (follow original ListBox.py implementation) # prefer stat per column (follow original ListBox.py implementation)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment