Commit 1d4b96ae authored by Tomáš Peterka's avatar Tomáš Peterka Committed by Tomáš Peterka

[hal_json_style+renderjs_ui] Support Actions on multiple objects

-  introspect any dialog method for `uids` argument
-  introspect any list_method inside dialog for `uid` argument
-  module-level Object JIO Actions are templated and they expect `query` parameter
-  use `form_id` to obtain previous search result
-  warn user when applying an action on unrestricted set of documents
parent 2f41d248
...@@ -97,8 +97,7 @@ try: ...@@ -97,8 +97,7 @@ try:
request.set('editable_mode', 1) request.set('editable_mode', 1)
form.validate_all_to_request(request) form.validate_all_to_request(request)
request.set('editable_mode', editable_mode) request.set('editable_mode', editable_mode)
default_skin = portal.portal_skins.getDefaultSkin()
default_skin = context.getPortalObject().portal_skins.getDefaultSkin()
allowed_styles = ("ODT", "ODS", "Hal", "HalRestricted") allowed_styles = ("ODT", "ODS", "Hal", "HalRestricted")
if getattr(getattr(context, dialog_method), 'pt', None) == "report_view" and \ if getattr(getattr(context, dialog_method), 'pt', None) == "report_view" and \
request.get('your_portal_skin', default_skin) not in allowed_styles: request.get('your_portal_skin', default_skin) not in allowed_styles:
...@@ -106,8 +105,8 @@ try: ...@@ -106,8 +105,8 @@ try:
# Form is OK, it's just this field - style so we return back form-wide error # Form is OK, it's just this field - style so we return back form-wide error
# for which we don't have support out-of-the-box thus we manually craft it # for which we don't have support out-of-the-box thus we manually craft it
# XXX TODO: Form-wide validation errors # XXX TODO: Form-wide validation errors
return context.Base_renderMessage( return context.Base_renderForm(dialog_id,
translate('Only ODT, ODS, Hal and HalRestricted skins are allowed for reports '\ message=translate('Only ODT, ODS, Hal and HalRestricted skins are allowed for reports '\
'in Preferences - User Interface - Report Style'), 'in Preferences - User Interface - Report Style'),
level=WARNING) level=WARNING)
...@@ -168,42 +167,43 @@ if len(listbox_id_list): ...@@ -168,42 +167,43 @@ if len(listbox_id_list):
listbox_line_list = tuple(listbox_line_list) listbox_line_list = tuple(listbox_line_list)
kw[listbox_id] = request_form[listbox_id] = listbox_line_list kw[listbox_id] = request_form[listbox_id] = listbox_line_list
# Handle selection the new way
# Check if the selection changed # First check for an query in form parameters - if they are there
if hasattr(kw, 'previous_md5_object_uid_list'): # that means previous view was a listbox with selected stuff so recover here
selection_list = context.portal_selections.callSelectionFor(kw['list_selection_name'], context=context) query = extra_param.get("query", None)
if selection_list is not None: select_all = int(extra_param.pop("_select_all", "0"))
object_uid_list = map(lambda x:x.getObject().getUid(), selection_list)
error = context.portal_selections.selectionHasChanged(kw['previous_md5_object_uid_list'], object_uid_list) if query != "" or (query == "" and select_all > 0):
if error: listbox = getattr(context, form_id).Form_getListbox()
error_message = context.Base_translateString("Sorry, your selection has changed.") if listbox is not None:
kw['uids'] = [int(getattr(document, "uid"))
for document in context.Base_searchUsingListbox(listbox, query)]
else:
log('Action {} should not specify `uids` as its parameters when it does not take object list from the previous view!'.format(dialog_method), level=ERROR)
elif query == "" and select_all == 0 and dialog_method != update_method: # do not interrupt on UPDATE
return context.Base_renderForm(
dialog_id,
message=translate("All documents are selected! Submit again to proceed or Cancel and narrow down your search."),
level=WARNING,
keep_items={'_select_all': 1},
query=query)
# The old way was to set inquire kw for "list_selection_name" and update
# it with kw["uids"] which means a long URL to call this script
# if dialog_category is object_search, then edit the selection # if dialog_category is object_search, then edit the selection
if dialog_category == "object_search" : if dialog_category == "object_search" :
context.portal_selections.setSelectionParamsFor(kw['selection_name'], kw) portal.portal_selections.setSelectionParamsFor(kw['selection_name'], kw)
# if we have checked line in listbox, modify the selection
listbox_uid = kw.get('listbox_uid', None)
# In some cases, the listbox exists, is editable, but the selection name
# has no meaning, for example fast input dialogs.
# In such cases, we must not try to update a non-existing selection.
if listbox_uid is not None and kw.has_key('list_selection_name'):
uids = kw.get('uids')
selected_uids = context.portal_selections.updateSelectionCheckedUidList(
kw['list_selection_name'],
listbox_uid, uids)
# Remove empty values for make_query.
clean_kw = dict((k, v) for k, v in kw.items() if v not in (None, [], ()))
# Add rest of extra param into arguments of the target method # Add rest of extra param into arguments of the target method
kw.update(extra_param) kw.update(extra_param)
# Finally we will call the Dialog Method # Finally we will call the Dialog Method
# Handle deferred style, unless we are executing the update action # Handle deferred style, unless we are executing the update action
if dialog_method != update_method and clean_kw.get('deferred_style', 0): if dialog_method != update_method and kw.get('deferred_style', 0):
clean_kw['deferred_portal_skin'] = clean_kw.get('portal_skin', None) kw['deferred_portal_skin'] = kw.get('portal_skin', None)
# XXX Hardcoded Deferred style name # XXX Hardcoded Deferred style name
clean_kw['portal_skin'] = 'Deferred' kw['portal_skin'] = 'Deferred'
page_template = getattr(getattr(context, dialog_method), 'pt', None) page_template = getattr(getattr(context, dialog_method), 'pt', None)
...@@ -211,21 +211,19 @@ if dialog_method != update_method and clean_kw.get('deferred_style', 0): ...@@ -211,21 +211,19 @@ if dialog_method != update_method and clean_kw.get('deferred_style', 0):
# Limit Reports in Deferred style to known working styles # Limit Reports in Deferred style to known working styles
if request_form.get('your_portal_skin', None) not in ("ODT", "ODS"): if request_form.get('your_portal_skin', None) not in ("ODT", "ODS"):
# RJS own validation - deferred option works here only with ODS/ODT skins # RJS own validation - deferred option works here only with ODS/ODT skins
return context.Base_renderMessage( return context.Base_renderForm(dialog_id,
translate('Deferred reports are possible only with preference '\ message=translate('Deferred reports are possible only with preference '\
'"Report Style" set to "ODT" or "ODS"'), '"Report Style" set to "ODT" or "ODS"'),
level=WARNING) level=WARNING)
# If the action form has report_view as it's method, it # If the action form has report_view as it's method, it
if page_template != 'report_view': if page_template != 'report_view':
# use simple wrapper # use simple wrapper
clean_kw['deferred_style_dialog_method'] = dialog_method kw['deferred_style_dialog_method'] = dialog_method
kw['deferred_style_dialog_method'] = dialog_method kw['deferred_style_dialog_method'] = dialog_method
request.set('deferred_style_dialog_method', dialog_method) request.set('deferred_style_dialog_method', dialog_method)
dialog_method = 'Base_activateSimpleView' dialog_method = 'Base_activateSimpleView'
url_params_string = make_query(clean_kw)
# Never redirect in JSON style - do as much as possible here. # Never redirect in JSON style - do as much as possible here.
# At this point the 'dialog_method' should point to a form (if we are in report) # At this point the 'dialog_method' should point to a form (if we are in report)
# if we are not in Deferred mode - then it points to `Base_activateSimpleView` # if we are not in Deferred mode - then it points to `Base_activateSimpleView`
...@@ -234,11 +232,11 @@ if True: ...@@ -234,11 +232,11 @@ if True:
if dialog_method != update_method: if dialog_method != update_method:
# When we are not executing the update action, we have to change the skin # When we are not executing the update action, we have to change the skin
# manually, # manually,
if 'portal_skin' in clean_kw: if 'portal_skin' in kw:
new_skin_name = clean_kw['portal_skin'] new_skin_name = kw['portal_skin']
context.getPortalObject().portal_skins.changeSkin(new_skin_name) portal.portal_skins.changeSkin(new_skin_name)
request.set('portal_skin', new_skin_name) request.set('portal_skin', new_skin_name)
deferred_portal_skin = clean_kw.get('deferred_portal_skin') deferred_portal_skin = kw.get('deferred_portal_skin')
if deferred_portal_skin: if deferred_portal_skin:
# has to be either ODS or ODT because only those contain `form_list` # has to be either ODS or ODT because only those contain `form_list`
request.set('deferred_portal_skin', deferred_portal_skin) request.set('deferred_portal_skin', deferred_portal_skin)
...@@ -258,16 +256,20 @@ if True: ...@@ -258,16 +256,20 @@ if True:
# with the content of REQUEST.URL # with the content of REQUEST.URL
request.set('URL', '%s/%s' % (context.absolute_url(), dialog_method)) request.set('URL', '%s/%s' % (context.absolute_url(), dialog_method))
# Only in case we are not updating but executing - then proceed with direct
# execution based on Skin selection
if dialog_method != update_method:
# RJS: If we are in deferred mode - call the form directly and return # RJS: If we are in deferred mode - call the form directly and return
# dialog method is now `Base_activateSimpleView` - the only script in # dialog method is now `Base_activateSimpleView` - the only script in
# deferred portal_skins folder # deferred portal_skins folder
if clean_kw.get('deferred_style', 0): if kw.get('deferred_style', 0):
return dialog_form(**kw) # deferred form should return redirect with a message return dialog_form(**kw) # deferred form should return redirect with a message
# RJS: If skin selection is different than Hal* then ERP5Document_getHateoas # RJS: If skin selection is different than Hal* then ERP5Document_getHateoas
# does not exist and we call form method directly # does not exist and we call form method directly
# If update_method was clicked and the target is the original dialog form then we must not call dialog_form directly because it returns HTML # If update_method was clicked and the target is the original dialog for
if clean_kw.get("portal_skin", context.getPortalObject().portal_skins.getDefaultSkin()) not in ("Hal", "HalRestricted", "View"): # then we must not call dialog_form directly because it returns HTML
if kw.get("portal_skin", portal.portal_skins.getDefaultSkin()) not in ("Hal", "HalRestricted", "View"):
return dialog_form(**kw) return dialog_form(**kw)
# dialog_form can be anything from a pure python function, class method to ERP5 Form or Python Script # dialog_form can be anything from a pure python function, class method to ERP5 Form or Python Script
......
...@@ -33,6 +33,9 @@ Traverse renders arbitrary View. It can be a Form or a Script. ...@@ -33,6 +33,9 @@ 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") :param view: {str} mandatory. the view reference as defined on a Portal Type (e.g. "view" or "publish_view")
:param query: {str} optional, is a remaining from the search on a previous view. Query is used to replace selections.
It provides complete information together with listbox configuration so we are able to pass a list of UIDs
to methods which require it. This allows dialogs to show selection from previous view.
:param extra_param_json: {str} BASE64 encoded JSON with parameters for getHateoas script. Content will be put to the REQUEST so :param extra_param_json: {str} BASE64 encoded JSON with parameters for getHateoas script. Content will be put to the REQUEST so
it is accessible to all Scripts and TALES expressions. If view contains embedded **dialog** form then it is accessible to all Scripts and TALES expressions. If view contains embedded **dialog** form then
fields will be added to that form to preserve the values for the next step. fields will be added to that form to preserve the values for the next step.
...@@ -458,6 +461,8 @@ url_template_dict = { ...@@ -458,6 +461,8 @@ url_template_dict = {
"&relative_url=%(relative_url)s&view=%(view)s", "&relative_url=%(relative_url)s&view=%(view)s",
"traverse_generator_action": "%(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", "&relative_url=%(relative_url)s&view=%(view)s&extra_param_json=%(extra_param_json)s",
"traverse_generator_action_module": "%(root_url)s/%(script_id)s?mode=traverse" + \
"&relative_url=%(relative_url)s&view=%(view)s&extra_param_json=%(extra_param_json)s{&query}",
"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}",
...@@ -465,10 +470,15 @@ url_template_dict = { ...@@ -465,10 +470,15 @@ url_template_dict = {
"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",
# Custom search comes with Listboxes where "list_method" is specified. We pass even listbox's
# own URL so the search can resolve template fields for proper rendering/formatting/editability
# of the results (because they will be backed up with real documents).
# :param extra_param_json: contains mainly previous form id to replicate previous search (it is a replacement for Selections)
"custom_search_template": "%(root_url)s/%(script_id)s?mode=search" + \ "custom_search_template": "%(root_url)s/%(script_id)s?mode=search" + \
"&relative_url=%(relative_url)s" \ "&relative_url=%(relative_url)s" \
"&form_relative_url=%(form_relative_url)s" \ "&form_relative_url=%(form_relative_url)s" \
"&list_method=%(list_method)s" \ "&list_method=%(list_method)s" \
"&extra_param_json=%(extra_param_json)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 # Non-editable searches suppose the search results will be rendered as-is and no template
...@@ -495,6 +505,12 @@ default_document_uri_template = url_template_dict["jio_get_template"] ...@@ -495,6 +505,12 @@ default_document_uri_template = url_template_dict["jio_get_template"]
Base_translateString = context.getPortalObject().Base_translateString Base_translateString = context.getPortalObject().Base_translateString
def lazyUidList(traversed_document, listbox, query):
"""Clojure providing lazy list of UIDs selected from a previous Listbox."""
return lambda: [int(getattr(document, "uid"))
for document in traversed_document.Base_searchUsingListbox(listbox, query)]
def getRealRelativeUrl(document): def getRealRelativeUrl(document):
return '/'.join(portal.portal_url.getRelativeContentPath(document)) return '/'.join(portal.portal_url.getRelativeContentPath(document))
...@@ -871,7 +887,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -871,7 +887,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id), "form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id),
"list_method": list_method_name, "list_method": list_method_name,
"default_param_json": urlsafe_b64encode( "default_param_json": urlsafe_b64encode(
json.dumps(ensureSerializable(list_method_query_dict))) json.dumps(ensureSerializable(list_method_query_dict))),
# in case of a dialog the form_id points to previous form, otherwise current form
"extra_param_json": urlsafe_b64encode(
json.dumps(ensureSerializable({"form_id": REQUEST.get('form_id', form.id)})))
} }
# once we imprint `default_params` into query string of 'list method' we # once we imprint `default_params` into query string of 'list method' we
# don't want them to propagate to the query as well # don't want them to propagate to the query as well
...@@ -1044,6 +1063,19 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -1044,6 +1063,19 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# If there is a "form_id" in the REQUEST then it means that last view was actually a form # 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` ... # 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", "") last_form_id = extra_param_json.pop("form_id", "") or REQUEST.get("form_id", "")
last_listbox = None
# ... so we can do some magic with it (especially embedded listbox if exists)!
try:
if last_form_id:
last_form = getattr(context, last_form_id)
last_listbox = last_form.Form_getListbox()
# In order not to use Selections we need to pass all search attributes to *a listbox inside the form dialog*
# in case there was a listbox in the previous form. No other case!
if last_listbox:
# If a Lisbox's list_method takes `uid` as input parameter then it will be ready in the request. But the actual
# computation is too expensive so we make it lazy (and evaluate any callable at `selectKwargsForCallable`)
# UID will be used in (Listbox's|RelationField's) list_method as a constraint if defined in parameters
REQUEST.set("uid", lazyUidList(traversed_document, last_listbox, query))
except AttributeError: except AttributeError:
pass pass
...@@ -1100,6 +1132,18 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -1100,6 +1132,18 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
if REQUEST.get('cancel_url', None): if REQUEST.get('cancel_url', None):
renderHiddenField(response_dict, "cancel_url", REQUEST.get('cancel_url')) renderHiddenField(response_dict, "cancel_url", REQUEST.get('cancel_url'))
# Let's support Selections!
# There are two things needed
# - `uid` parameter of list_method of listbox (if present) in this dialog
# - `uids` or `brain` to a Dialog Form Action Method
#
# We can serialize `uids` into a hidden form field but we cannot do it with brain so we
# simply put "query" in a hidden field and construct `brain` from it in Base_callDialogMethod
dialog_method_kwargs = selectKwargsForCallable(getattr(traversed_document, form.action), {}, {'brain': None, 'uids': None})
if 'uids' in dialog_method_kwargs:
# If we do not have "query" in the REQUEST but the Dialog Method requires uids
# then we still should inject empty "query" in the dialog call
extra_param_json["query"] = query or REQUEST.get("query", "")
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
renderHiddenField(response_dict, 'form_id', form.id) renderHiddenField(response_dict, 'form_id', form.id)
...@@ -1194,11 +1238,6 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -1194,11 +1238,6 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# end-if report_section # end-if report_section
if form.pt == "form_dialog": 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 # 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) # metadata about the form (its hash and dynamic fields)
renderHiddenField(response_dict, 'extra_param_json', json.dumps(extra_param_json)) renderHiddenField(response_dict, 'extra_param_json', json.dumps(extra_param_json))
...@@ -1480,11 +1519,18 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1480,11 +1519,18 @@ 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"
# Modify Actions on Module - they need access to current form_id and runtime
# information such as `query` in case they operate on selections!
if erp5_action_key not in ("view", "object_view", "object_jio_view"): if erp5_action_key not in ("view", "object_view", "object_jio_view"):
url_template_key = "traverse_generator_action" url_template_key = "traverse_generator_action"
if traversed_document.getPortalType() in portal.getPortalModuleTypeList():
url_template_key = "traverse_generator_action_module"
erp5_action_list[-1]['templated'] = True
# but when we do not have the last form id we do not pass is of course # 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): if not (current_action.get('view_id', '') or last_form_id):
url_template_key = "traverse_generator" url_template_key = "traverse_generator"
if 'templated' in erp5_action_list[-1]:
del erp5_action_list[-1]['templated']
# some dialogs need previous form_id when rendering to pass UID to embedded Listbox # some dialogs need previous form_id when rendering to pass UID to embedded Listbox
extra_param_json['form_id'] = current_action['view_id'] \ extra_param_json['form_id'] = current_action['view_id'] \
...@@ -1651,6 +1697,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1651,6 +1697,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# > Method 'search' is used for getting related objects as well which are # > Method 'search' is used for getting related objects as well which are
# > not backed up by a ListBox thus the value resolution would have to be # > not backed up by a ListBox thus the value resolution would have to be
# > there anyway. It is better to use one code for all in this case. # > there anyway. It is better to use one code for all in this case.
#
# How do you deal with old-style Selections?
# > We simply do not use them. All Document selection is handled via passing
# > "query" parameter to Base_callDialogMethod or introspecting list_methods.
################################################# #################################################
if REQUEST.other['method'] != "GET": if REQUEST.other['method'] != "GET":
response.setStatus(405) response.setStatus(405)
......
...@@ -630,7 +630,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): ...@@ -630,7 +630,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_embedded']['_view']['listbox']['editable_column_list'], [['id', 'ID'], ['title', 'Title'], ['quantity', 'quantity'], ['start_date', 'Date']]) self.assertEqual(result_dict['_embedded']['_view']['listbox']['editable_column_list'], [['id', 'ID'], ['title', 'Title'], ['quantity', 'quantity'], ['start_date', 'Date']])
self.assertEqual(result_dict['_embedded']['_view']['listbox']['sort_column_list'], [['id', 'ID'], ['title', 'Title'], ['quantity', 'Quantity'], ['start_date', 'Date']]) self.assertEqual(result_dict['_embedded']['_view']['listbox']['sort_column_list'], [['id', 'ID'], ['title', 'Title'], ['quantity', 'Quantity'], ['start_date', 'Date']])
self.assertEqual(result_dict['_embedded']['_view']['listbox']['list_method_template'], self.assertEqual(result_dict['_embedded']['_view']['listbox']['list_method_template'],
'%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_ui_test/Foo_view/listbox&list_method=objectValues&default_param_json=eyJwb3J0YWxfdHlwZSI6IFsiRm9vIExpbmUiXSwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId())) '%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_ui_test/Foo_view/listbox&list_method=objectValues&extra_param_json=eyJmb3JtX2lkIjogIkZvb192aWV3In0=&default_param_json=eyJwb3J0YWxfdHlwZSI6IFsiRm9vIExpbmUiXSwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId()))
self.assertEqual(result_dict['_embedded']['_view']['listbox']['domain_root_list'], [['foo_category', 'FooCat'], ['foo_domain', 'FooDomain'], ['not_existing_domain', 'NotExisting']]) self.assertEqual(result_dict['_embedded']['_view']['listbox']['domain_root_list'], [['foo_category', 'FooCat'], ['foo_domain', 'FooDomain'], ['not_existing_domain', 'NotExisting']])
NBSP_prefix = u'\xA0' * 4 NBSP_prefix = u'\xA0' * 4
self.assertEqual(result_dict['_embedded']['_view']['listbox']['domain_dict'], {'foo_domain': [['a', 'a'], ['%sa1' % NBSP_prefix, 'a/a1'], ['%sa2' % NBSP_prefix, 'a/a2'], ['b', 'b']], 'foo_category': [['a', 'a'], ['a/a1', 'a/a1'], ['a/a2', 'a/a2'], ['b', 'b']]}) self.assertEqual(result_dict['_embedded']['_view']['listbox']['domain_dict'], {'foo_domain': [['a', 'a'], ['%sa1' % NBSP_prefix, 'a/a1'], ['%sa2' % NBSP_prefix, 'a/a2'], ['b', 'b']], 'foo_category': [['a', 'a'], ['a/a1', 'a/a1'], ['a/a2', 'a/a2'], ['b', 'b']]})
...@@ -910,7 +910,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): ...@@ -910,7 +910,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['editable_column_list'], [['time', 'Time'], ['comment', 'Comment'], ['error_message', 'Error Message']]) self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['editable_column_list'], [['time', 'Time'], ['comment', 'Comment'], ['error_message', 'Error Message']])
self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['sort_column_list'], []) self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['sort_column_list'], [])
self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['list_method_template'], self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['list_method_template'],
'%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_core/Base_viewWorkflowHistory/listbox&list_method=Base_getWorkflowHistoryItemList&default_param_json=eyJ3b3JrZmxvd19pZCI6ICJmb29fd29ya2Zsb3ciLCAiY2hlY2tlZF9wZXJtaXNzaW9uIjogIlZpZXciLCAid29ya2Zsb3dfdGl0bGUiOiAiRm9vIFdvcmtmbG93IiwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId())) '%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_core/Base_viewWorkflowHistory/listbox&list_method=Base_getWorkflowHistoryItemList&extra_param_json=eyJmb3JtX2lkIjogIkJhc2Vfdmlld1dvcmtmbG93SGlzdG9yeSJ9&default_param_json=eyJ3b3JrZmxvd19pZCI6ICJmb29fd29ya2Zsb3ciLCAiY2hlY2tlZF9wZXJtaXNzaW9uIjogIlZpZXciLCAid29ya2Zsb3dfdGl0bGUiOiAiRm9vIFdvcmtmbG93IiwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId()))
@simulate('Base_getRequestUrl', '*args, **kwargs', @simulate('Base_getRequestUrl', '*args, **kwargs',
...@@ -961,6 +961,25 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): ...@@ -961,6 +961,25 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['group_list'][0][1][0], ['my_id', {'meta_type': 'ProxyField'}]) self.assertEqual(result_dict['group_list'][0][1][0], ['my_id', {'meta_type': 'ProxyField'}])
self.assertEqual(result_dict['_debug'], "traverse") self.assertEqual(result_dict['_debug'], "traverse")
@simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs',
'return "application/hal+json"')
@changeSkin('Hal')
def test_getHateoasForm_action_module(self):
fake_request = do_fake_request("GET")
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(REQUEST=fake_request, mode="traverse", relative_url="foo_module", 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)
for object_jio_action in result_dict['_links']['action_object_jio_action']:
# the link is a template
self.assertTrue(object_jio_action['templated'])
self.assertTrue("{&" in object_jio_action['href'])
class TestERP5Document_getHateoas_mode_search(ERP5HALJSONStyleSkinsMixin): class TestERP5Document_getHateoas_mode_search(ERP5HALJSONStyleSkinsMixin):
@simulate('Base_getRequestHeader', '*args, **kwargs', @simulate('Base_getRequestHeader', '*args, **kwargs',
......
<?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_jio_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_jio_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>empty_mass_action</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>Empty Mass Action</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}/Folder_doNothingDialog</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
from Products.ERP5Type.Log import log
log("Folder method received dialog_id, form_id, uids and {!s}".format(kwargs.keys()))
return context.Base_redirect(form_id, keep_items={"portal_status_message": "Did nothing."})
<?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>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Folder_doNothing</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></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>
<string>listbox</string>
</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/>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Folder_doNothingDialog</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>Folder_doNothingDialog</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_dialog</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>Do Massively Nothing</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>Folder_doNothingDialog</string> </value>
</item>
<item>
<key> <string>update_action_title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>listbox</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>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>listbox</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>FooModule_viewFooList</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>request/form_id</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
""" """
Used to set properties for Listbox Used to set properties for Listbox
""" """
from Products.ERP5Type.Log import log
field = context
d = dict( d = dict(
field_title = 'Foos', field_title = 'Foos',
field_description = '', field_description = '',
...@@ -55,8 +56,20 @@ d = dict( ...@@ -55,8 +56,20 @@ d = dict(
d.update(context.REQUEST) d.update(context.REQUEST)
d.update(kw) d.update(kw)
# If the listbox is a proxy field, replace it with just a listbox
# before it was always failing on "form_id was not transferred"
if field.meta_type == "ProxyField":
form = field.aq_parent
field_id = field.getId()
field_title = field.getTitle()
log("Replacing ProxyField with a Listbox for {!s}.{!s}".format(form, context))
form.manage_delObjects(field_id)
form.manage_addField(field_id, field_title, 'ListBox')
field = getattr(form, 'listbox')
#context.log('ListBox_setPropertyList', 'kw = %r, d = %r' % (kw, d,)) #context.log('ListBox_setPropertyList', 'kw = %r, d = %r' % (kw, d,))
r = context.form.validate(d) r = field.form.validate(d)
context.manage_edit_xmlrpc(r) field.manage_edit_xmlrpc(r)
return 'Set Successfully.' return 'Set Successfully.'
...@@ -10,6 +10,7 @@ Foo Line | view_contentlist ...@@ -10,6 +10,7 @@ Foo Line | view_contentlist
Foo Line | view_dynamic_matrixbox Foo Line | view_dynamic_matrixbox
Foo Line | view_matrixbox Foo Line | view_matrixbox
Foo Module | do_nothing_report_jio Foo Module | do_nothing_report_jio
Foo Module | empty_mass_action
Foo Module | list Foo Module | list
Foo Module | list_ui Foo Module | list_ui
Foo Module | search Foo Module | search
...@@ -22,7 +23,6 @@ Foo | dummy_multicheckboxfield_report ...@@ -22,7 +23,6 @@ Foo | dummy_multicheckboxfield_report
Foo | dummy_multilistfield_report Foo | dummy_multilistfield_report
Foo | dummy_radiofield_report Foo | dummy_radiofield_report
Foo | dummy_report Foo | dummy_report
Foo | fail_dialog_action_jio
Foo | list Foo | list
Foo | select_bar Foo | select_bar
Foo | view Foo | view
......
/*global window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray */ /*global window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray */
/*jslint nomen: true, indent: 2, maxerr: 3 */ /*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray) { (function (window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray) {
"use strict"; "use strict";
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -19,15 +19,15 @@ ...@@ -19,15 +19,15 @@
* @param {Array} command_list - array of links obtained from ERP5 HATEOAS * @param {Array} command_list - array of links obtained from ERP5 HATEOAS
*/ */
function renderLinkList(gadget, title, icon, erp5_link_list) { function renderLinkList(gadget, title, icon, erp5_link_list) {
return new RSVP.Queue() return gadget.getUrlParameter("extended_search")
.push(function () { .push(function (query) {
// obtain RJS links from ERP5 links // obtain RJS links from ERP5 links
return RSVP.all( return RSVP.all(
erp5_link_list.map(function (erp5_link) { erp5_link_list.map(function (erp5_link) {
return gadget.getUrlFor({ return gadget.getUrlFor({
"command": 'change', "command": 'change',
"options": { "options": {
"view": erp5_link.href, "view": UriTemplate.parse(erp5_link.href).expand({query: query}),
"page": undefined "page": undefined
} }
}); });
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
.declareAcquiredMethod("translateHtml", "translateHtml") .declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("getUrlFor", "getUrlFor") .declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("updateHeader", "updateHeader") .declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// declared methods // declared methods
...@@ -105,4 +106,4 @@ ...@@ -105,4 +106,4 @@
}); });
}); });
}(window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray)); }(window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray));
\ No newline at end of file \ No newline at end of file
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>965.57861.34804.9762</string> </value> <value> <string>966.29724.45106.17220</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1519987107.92</float> <float>1523021357.19</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
/*global window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray */ /*global window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray */
/*jslint nomen: true, indent: 2, maxerr: 3 */ /*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray) { (function (window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray) {
"use strict"; "use strict";
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -19,15 +19,15 @@ ...@@ -19,15 +19,15 @@
* @param {Array} command_list - array of links obtained from ERP5 HATEOAS * @param {Array} command_list - array of links obtained from ERP5 HATEOAS
*/ */
function renderLinkList(gadget, title, icon, erp5_link_list) { function renderLinkList(gadget, title, icon, erp5_link_list) {
return new RSVP.Queue() return gadget.getUrlParameter("extended_search")
.push(function () { .push(function (query) {
// obtain RJS links from ERP5 links // obtain RJS links from ERP5 links
return RSVP.all( return RSVP.all(
erp5_link_list.map(function (erp5_link) { erp5_link_list.map(function (erp5_link) {
return gadget.getUrlFor({ return gadget.getUrlFor({
"command": 'change', "command": 'change',
"options": { "options": {
"view": erp5_link.href, "view": UriTemplate.parse(erp5_link.href).expand({query: query}),
"page": undefined "page": undefined
} }
}); });
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
.declareAcquiredMethod("jio_getAttachment", "jio_getAttachment") .declareAcquiredMethod("jio_getAttachment", "jio_getAttachment")
.declareAcquiredMethod("translateHtml", "translateHtml") .declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("getUrlFor", "getUrlFor") .declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
.declareAcquiredMethod("updateHeader", "updateHeader") .declareAcquiredMethod("updateHeader", "updateHeader")
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -100,4 +101,4 @@ ...@@ -100,4 +101,4 @@
}); });
}); });
}(window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray)); }(window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray));
\ No newline at end of file \ No newline at end of file
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>965.24987.20289.62754</string> </value> <value> <string>966.34409.25650.52155</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1518184046.23</float> <float>1522842712.42</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -103,6 +103,7 @@ ...@@ -103,6 +103,7 @@
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
.declareAcquiredMethod("redirect", "redirect") .declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("getUrlFor", "getUrlFor") .declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
.declareAcquiredMethod("updateHeader", "updateHeader") .declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("translate", "translate") .declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("translateHtml", "translateHtml") .declareAcquiredMethod("translateHtml", "translateHtml")
...@@ -135,6 +136,8 @@ ...@@ -135,6 +136,8 @@
.declareMethod('render', function (options) { .declareMethod('render', function (options) {
var gadget = this; var gadget = this;
// copy out wanted items from options and pass it to `changeState` // copy out wanted items from options and pass it to `changeState`
return gadget.getUrlParameter('extended_search')
.push(function (extended_search) {
return gadget.changeState({ return gadget.changeState({
jio_key: options.jio_key, jio_key: options.jio_key,
view: options.view, view: options.view,
...@@ -144,10 +147,12 @@ ...@@ -144,10 +147,12 @@
erp5_form: options.erp5_form || {}, erp5_form: options.erp5_form || {},
// editable: true, // ignore global editable state (be always editable) // editable: true, // ignore global editable state (be always editable)
has_update_action: Boolean(options.form_definition.update_action), has_update_action: Boolean(options.form_definition.update_action),
// pass extended_search from previous view in case any gadget is curious
extended_search: extended_search,
// XXX Hack of ERP5 how to express redirect to parent after success // XXX Hack of ERP5 how to express redirect to parent after success
redirect_to_parent: options.erp5_document._embedded._view.field_your_redirect_to_parent !== undefined redirect_to_parent: options.erp5_document._embedded._view.field_your_redirect_to_parent !== undefined
}); });
});
}) })
.onStateChange(function (modification_dict) { .onStateChange(function (modification_dict) {
...@@ -224,7 +229,11 @@ ...@@ -224,7 +229,11 @@
form_options.view = form_gadget.state.view; form_options.view = form_gadget.state.view;
form_options.jio_key = form_gadget.state.jio_key; form_options.jio_key = form_gadget.state.jio_key;
form_options.editable = true; // dialog is always editable form_options.editable = true; // dialog is always editable
// this might cause problems if the listbox in the dialog is not curious
// about the previous search
if (form_gadget.state.extended_search) {
form_options.form_definition.extended_search = form_gadget.state.extended_search;
}
return erp5_form.render(form_options); return erp5_form.render(form_options);
}) })
.push(function () { .push(function () {
......
...@@ -136,12 +136,6 @@ ...@@ -136,12 +136,6 @@
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent> <persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value> </value>
</item> </item>
<item>
<key> <string>processing_status_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAU=</string> </persistent>
</value>
</item>
</dictionary> </dictionary>
</value> </value>
</item> </item>
...@@ -163,7 +157,7 @@ ...@@ -163,7 +157,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>romain</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -185,8 +179,8 @@ ...@@ -185,8 +179,8 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1406903475.67</float> <float>1523969253.3</float>
<string>GMT</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
</object> </object>
...@@ -216,7 +210,7 @@ ...@@ -216,7 +210,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -230,7 +224,7 @@ ...@@ -230,7 +224,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>965.50744.39391.46916</string> </value> <value> <string>966.62936.1765.16691</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +242,7 @@ ...@@ -248,7 +242,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1519729953.99</float> <float>1524144844.96</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
...@@ -260,61 +254,4 @@ ...@@ -260,61 +254,4 @@
</tuple> </tuple>
</pickle> </pickle>
</record> </record>
<record id="5" aka="AAAAAAAAAAU=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>detect_converted_file</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>romain</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_processing_state</string> </key>
<value> <string>converted</string> </value>
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>0.0.0.0</string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1406902506.19</float>
<string>GMT</string>
</tuple>
</state>
</object>
</value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData> </ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testSelectAll</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test RenderJS UI Module Action Selecting All</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><th rowspan="1" colspan="3">Test RenderJS UI Module Action Selecting All</th></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<!-- Clean Up -->
<tr><td>open</td>
<td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Reset Successfully.</td><td></td></tr>
<!-- Shortcut for full renderjs url -->
<tr><td>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=10&amp;num:int=5</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Created Successfully.</td><td></td></tr>
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=20&amp;num:int=5</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Created Successfully.</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[@data-role="header"]//a[@data-i18n="Actions"]</td><td></td></tr>
<tr><td>click</td>
<td>//div[@data-role="header"]//a[@data-i18n="Actions"]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//ul[@data-role="listview"]//a[@data-i18n="Empty Mass Action"]</td><td></td></tr>
<tr><td>click</td>
<td>//ul[@data-role="listview"]//a[@data-i18n="Empty Mass Action"]</td><td></td></tr>
<tal:block tal:define="pagination_configuration python: {'header': '(1 - 3 / 10)', 'footer': 'Records 1 - 3 / 10'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><th colspan="3">Check that sort and pagination work</th></tr>
<tr><td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/thead/tr/th//a[@data-i18n="ID"]</td><td></td></tr>
<tr><td>click</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/thead/tr/th//a[@data-i18n="ID"]</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="pagination_configuration python: {'header': '(1 - 3 / 10)', 'footer': 'Records 1 - 3 / 10'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><td>waitForText</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tbody/tr[1]/td[1]//p</td>
<td>24</td></tr>
<tr><td>waitForText</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tbody/tr[2]/td[1]//p</td>
<td>23</td></tr>
<tr><td>waitForText</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tbody/tr[3]/td[1]//p</td>
<td>22</td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/a[@data-i18n="Next"]</td><td></td></tr>
<tr><td>click</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/a[@data-i18n="Next"]</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="pagination_configuration python: {'header': '(4 - 6 / 10)', 'footer': 'Records 4 - 6 / 10'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><td>waitForText</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tbody/tr[1]/td[1]//p</td>
<td>21</td></tr>
<tr><td>waitForText</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tbody/tr[2]/td[1]//p</td>
<td>20</td></tr>
<tr><td>waitForText</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tbody/tr[3]/td[1]//p</td>
<td>14</td></tr>
<tr><th rowspan="1" colspan="3">Updating the dialog must not trigger warning about all selected</th></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/update_dialog" />
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="notification_configuration python: {'class': 'success', 'text': 'Data received.'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tr><th rowspan="1" colspan="3">Submitting, however, must warn the user</th></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/submit_dialog" />
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="notification_configuration python: {'class': 'error', 'text': 'All documents are selected! Submit again to proceed or Cancel and narrow down your search.'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tr><th rowspan="1" colspan="3">Second submission must work as advertised</th></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/submit_dialog" />
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="notification_configuration python: {'class': 'success', 'text': 'Did nothing.'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tr><th colspan="3">However search will avoid warning</th></tr>
<tal:block tal:define='search_query string:( title: "Title 1%" )'>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/search_in_form_list" />
</tal:block>
<tal:block tal:define="pagination_configuration python: {'header': '(1 - 3 / 5)', 'footer': 'Records 1 - 3 / 5'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><td>waitForElementPresent</td>
<td>//div[@data-role="header"]//a[@data-i18n="Actions"]</td><td></td></tr>
<tr><td>click</td>
<td>//div[@data-role="header"]//a[@data-i18n="Actions"]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//ul[@data-role="listview"]//a[@data-i18n="Empty Mass Action"]</td><td></td></tr>
<tr><td>click</td>
<td>//ul[@data-role="listview"]//a[@data-i18n="Empty Mass Action"]</td><td></td></tr>
<tr><th colspan="3">Make sure that search stays</th></tr>
<tal:block tal:define="pagination_configuration python: {'header': '(1 - 3 / 5)', 'footer': 'Records 1 - 3 / 5'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><th colspan="3">Even when paginating</th></tr>
<tr><td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/a[@data-i18n="Next"]</td><td></td></tr>
<tr><td>click</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/a[@data-i18n="Next"]</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="pagination_configuration python: {'header': '(4 - 5 / 5)', 'footer': 'Records 4 - 5 / 5'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><th rowspan="1" colspan="3">Second submission must work as advertised</th></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/submit_dialog" />
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="notification_configuration python: {'class': 'success', 'text': 'Did nothing.'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
</tbody></table>
</body>
</html>
\ No newline at end of file
"""Return an Iterator over database result from `form_id`'s listbox and optional `query`.
This script is intended to be used only internally.
"""
form = getattr(context, form_id)
listbox = form.Form_getListbox()
return context.Base_searchUsingListbox(listbox, query, sort_on, limit)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>form_id, query=\'\', sort_on=(), limit=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_searchUsingFormIdAndQuery</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
"""Extract search settings from a listbox and issue search using `query`.
Listbox is a search-capable component but searching using it is not straightforward. This
script solves exactly that.
Returns an iterable (most likely SearchResult instance depending on list_method definition)
"""
list_method_kwargs = dict(listbox.get_value('default_params')) or {}
# Listbox contraints portal types
portal_types = listbox.get_value('portal_types')
if portal_types:
if "portal_type" in list_method_kwargs:
if isinstance(list_method_kwargs['portal_type'], (str, unicode)):
list_method_kwargs['portal_type'] = [list_method_kwargs['portal_type'], ]
else:
list_method_kwargs['portal_type'] = []
list_method_kwargs['portal_type'].extend(portal_type_name for portal_type_name, _ in portal_types)
# query is provided by the caller because it is a runtime information
if query:
list_method_kwargs.update(full_text=query) # second overwrite the query
if limit:
list_method_kwargs.update(limit=limit)
if sort_on:
list_method_kwargs.update(sort_on=sort_on)
list_method_name = listbox.get_value('list_method').getMethodName()
list_method = getattr(context, list_method_name) # get the list_method with correct context
return list_method(**list_method_kwargs)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>listbox, query=\'\', sort_on=(), limit=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_searchUsingListbox</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
# Return first listbox in a form that is enabled and not hidden
# Christophe Dumez <christophe@nexedi.com>
# This script should be used to detect a listbox without having to name it "listbox"
if form is None:
form=context
if form.meta_type != 'ERP5 Form':
return None
# XXX We should not use meta_type properly,
# XXX We need to discuss this problem.(yusei)
def isListBox(field):
if field.meta_type=='ListBox':
return True
elif field.meta_type=='ProxyField':
template_field = field.getRecursiveTemplateField()
if template_field.meta_type=='ListBox':
return True
return False
# we start with 'bottom' because most of the time
# the listbox is there.
for group in ('bottom', 'center', 'left', 'right'):
for field in form.get_fields_in_group(group):
if isListBox(field) and not(field['hidden']) and field['enabled']:
return field
"""Return first listbox in a form that is enabled and not hidden
This script should be used to detect a listbox without having to name it "listbox".
:param form: {Form} optional Form instance instead of calling this script directly on a Form
:param form_id: {str} if specified the Script must be called on currently traversed document
Christophe Dumez <christophe@nexedi.com>
"""
def isListBox(field):
if field.meta_type == "ListBox":
return True
elif field.meta_type == "ProxyField":
template_field = field.getRecursiveTemplateField()
if template_field.meta_type == "ListBox":
return True
return False
if form_id is not None:
form = getattr(context, form_id)
if form is None:
form = context
if form.meta_type not in ('ERP5 Form', 'Folder', 'ERP5 Folder'):
raise RuntimeError("Cannot get Listbox field from \"{!s}\"! Supported is only ERP5 Form and (ERP5) Folder".format(form.meta_type))
if form.has_field('listbox'):
return form.get_field('listbox')
# we start with 'bottom' because most of the time
# the listbox is there.
for group in ('bottom', 'center', 'left', 'right'):
for field in form.get_fields_in_group(group):
if (isListBox(field) and
not field.get_value('hidden') and
field.get_value('enabled')):
return field
...@@ -50,11 +50,11 @@ ...@@ -50,11 +50,11 @@
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <key> <string>_params</string> </key>
<value> <string>form=None</string> </value> <value> <string>form=None, form_id=None</string> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>ERP5Site_getListbox</string> </value> <value> <string>Form_getListbox</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
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