Commit 95a4470c authored by Tomáš Peterka's avatar Tomáš Peterka

[hal_json_style + erp5_core] Support Actions using Selections by requiring (checked) UIDS

-  improve introspection to support External Methods
-  pass uid to list_methods
-  pass uids to dialog_methods
-  set fix=1 for repeated dialog submission call after a failure (can be used as a confirmation dialog)
-  split Delete Action to two independent once - one for Module one for Document
-  update Folder_delete Script to work in both interfaces
parent 24017646
......@@ -78,7 +78,7 @@
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>python: object.isDeletable(check_relation=False)</string> </value>
<value> <string>python: object.isDeletable(check_relation=False) and object.getPortalType() not in portal.getPortalModuleTypeList()</string> </value>
</item>
</dictionary>
</pickle>
......
<?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>category</string> </key>
<value> <string>object_delete_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>delete_document_list</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>103.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Delete Multiple Documents</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_viewDeleteDialog</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>python: object.getPortalType() in portal.getPortalModuleTypeList()</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
"""
Generic method called when submitting a form in dialog mode.
Responsible for validating form data and redirecting to the form action.
Please note that the new UI has deprecated use of Selections. Your scripts
will no longer receive `selection_name` nor `selection` in their arguments.
"""
from Products.ERP5Type.Log import log, DEBUG, INFO, WARNING
from Products.Formulator.Errors import FormValidationError, ValidationError
from ZTUtils import make_query
import json
# XXX We should not use meta_type properly,
# XXX We need to discuss this problem.(yusei)
def isFieldType(field, type_name):
if field.meta_type == 'ProxyField':
field = field.getRecursiveTemplateField()
return field.meta_type == type_name
from Products.Formulator.Errors import FormValidationError, ValidationError
from ZTUtils import make_query
# Kato: I do not understand why we throw away REQUEST from parameters (hidden in **kw)
# and use container.REQUEST just to introduce yet another global state. Maybe because
# container.REQUEST is used in other places.
......@@ -31,6 +30,19 @@ translate = context.Base_translateString
# Make this script work alike no matter if called by a script or a request
kw.update(request_form)
# We need to know form_id! In case of a default view the form_id is empty
# thus we need to parse it out from the default action definition.
if not form_id:
# get default form from default view for given context
default_view_url = str(portal.Base_filterDuplicateActions(
portal.portal_actions.listFilteredActionsFor(traversed_document))['object_view'][0]['url'])
form_id = default_view_url.split('?', 1)[0].split("/")[-1]
# We do NOT create, use, or modify Selections!
# Modify your Scripts instead!
# kw['selection_name'] = selection_name
# request.set('selection_name', selection_name)
# Exceptions for UI
if dialog_method == 'Base_configureUI':
return context.Base_configureUI(form_id=kw['form_id'],
......@@ -70,11 +82,11 @@ if dialog_method == 'Base_createRelation':
dialog_id=dialog_id,
portal_type=kw['portal_type'],
return_url=kw['cancel_url'])
# Exception for folder delete
if dialog_method == 'Folder_delete':
return context.Folder_delete(form_id=kw['form_id'],
selection_name=kw['selection_name'],
md5_object_uid_list=kw['md5_object_uid_list'])
# NO Exception for folder delete
# if dialog_method == 'Folder_delete':
# return context.Folder_delete(form_id=kw['form_id'],
# selection_name=kw['selection_name'],
# md5_object_uid_list=kw['md5_object_uid_list'])
form = getattr(context, dialog_id)
......@@ -90,7 +102,6 @@ try:
request.set('editable_mode', 1)
form.validate_all_to_request(request)
request.set('editable_mode', editable_mode)
default_skin = context.getPortalObject().portal_skins.getDefaultSkin()
allowed_styles = ("ODT", "ODS", "Hal", "HalRestricted")
if getattr(getattr(context, dialog_method), 'pt', None) == "report_view" and \
......@@ -161,30 +172,25 @@ if len(listbox_id_list):
listbox_line_list = tuple(listbox_line_list)
kw[listbox_id] = request_form[listbox_id] = listbox_line_list
# Check if the selection changed
if hasattr(kw, 'previous_md5_object_uid_list'):
selection_list = context.portal_selections.callSelectionFor(kw['list_selection_name'], context=context)
if selection_list is not None:
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 error:
error_message = context.Base_translateString("Sorry, your selection has changed.")
# Handle selection the new way
# First check for an query in form parameters - if they are there
# that means previous view was a listbox with selected stuff so recover here
query = request_form.get("query", None)
fix = int(request_form.get("fix", "0"))
if query != "" or (query == "" and fix > 0): # force empty query when fix == 1
listbox = getattr(context, form_id).Form_getListbox()
kw['uids'] = [int(getattr(document, "uid"))
for document in context.Base_searchUsingListbox(listbox, query)]
elif query == "" and fix == 0:
return context.Base_renderMessage(translate("All documents are selected! Submit again to proceed or Cancel and narrow down your search"), WARNING)
# 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 == "object_search" :
context.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, [], ()))
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>dialog_method, dialog_id, dialog_category=\'\', update_method=None, **kw</string> </value>
<value> <string>dialog_method, dialog_id, form_id, dialog_category=\'\', update_method=None, query=\'\', **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
"""
This script factorises code required to redirect to the appropriate
page from a script. It should probably be extended, reviewed and documented
so that less code is copied and pasted in dialog scripts.
"""UI Script to redirect the user to `context` with optional custom view `form_id`.
TODO: improve API and extensively document. ERP5Site_redirect may
be redundant.
:param keep_items: is used mainly to pass "portal_status_message" to be showed to the user
the new UI supports "portal_status_level" with values "success" or "error"
"""
from ZTUtils import make_query
import json
......@@ -40,8 +37,15 @@ response.setHeader("X-Location", "urn:jio:get:%s" % context.getRelativeUrl())
# therefor we don't need to be afraid of clashes
response.setHeader("Content-type", "application/json; charset=utf-8")
portal_status_level = keep_items.pop("portal_status_level", "success")
if portal_status_level in ("warning", "error", "fatal"):
portal_status_level = "error"
if portal_status_level in ("info", "debug", "success"):
portal_status_level = "success"
result_dict = {
'portal_status_message': "%s" % keep_items.pop("portal_status_message", ""),
'portal_status_level': "%s" % portal_status_level,
'_links': {
"self": {
# XXX Include query parameters
......
......@@ -8,21 +8,29 @@ In general, it always returns a JSON reponse in HATEOAS format specification.
: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)
Only in mode == 'search'
Parameters for mode == 'search'
:param query: string-serialized Query
:param select_list: list of strings to select from search result object
:param limit: tuple(start_index, num_records) which is further passed to list_method BUT not every list_method takes it into account
:param form_relative_url: {str} relative URL of a form FIELD issuing the search (listbox/relation field...)
it can be None in case of special listboxes like List of Modules
or relative path like "portal_skins/erp5_ui_test/FooModule_viewFooList/listbox"
:param form: {str} a bit hackish way of passing 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:
Only in mode == 'traverse'
Parameters for mode == 'traverse'
Traverse renders arbitrary View. It can be a Form or a Script.
:param relative_url: string, MANDATORY for obtaining the traversed_document. Calling this script directly on an object should be
forbidden in code (but it is not now).
:param query: string, 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.
# Form
......@@ -223,15 +231,37 @@ def selectKwargsForCallable(func, initial_kwargs, kwargs_dict):
In case the function cannot state required arguments it throws an AttributeError.
"""
if not hasattr(func, 'params'):
if hasattr(func, 'params'):
# In case the func is actualy Script (Python) or ERP5 Python Script
func_param_list = [tuple(map(lambda x: x.strip(), func_param.split('='))) for func_param in func.params().split(",")]
elif hasattr(func, "func_args"):
# In case the func is an External Method
func_param_list = func.func_args
if len(func_param_list) > 0 and func_param_list[0] == "self":
func_param_list = func_param_list[1:]
func_default_list = func.func_defaults
func_param_list = [(func_param, func_default_list[i]) if len(func_default_list) >= (i + 1) else (func_param, )
for i, func_param in enumerate(func_param_list)]
else:
# TODO: cover the case of Callables
# For anything else give up in advance and just return initial guess of the callee
return initial_kwargs
func_param_list = [func_param.strip() for func_param in func.params().split(",")]
func_param_name_list = [func_param if '=' not in func_param else func_param.split('=')[0]
for func_param in func_param_list if '*' not in func_param]
# func_param_list is a list of tuples - first item is parameter name and optinal second item is the default value
func_param_name_list = [item[0] for item in func_param_list]
for func_param_name in func_param_name_list:
if func_param_name in kwargs_dict and func_param_name not in initial_kwargs:
initial_kwargs[func_param_name] = kwargs_dict.get(func_param_name)
if '*' in func_param_name:
continue
# move necessary parameters from kwargs_dict to initial_kwargs
if func_param_name not in initial_kwargs and func_param_name in kwargs_dict:
func_param_value = kwargs_dict.get(func_param_name)
if callable(func_param_value):
initial_kwargs[func_param_name] = func_param_value() # evaluate lazy attributes
else:
initial_kwargs[func_param_name] = func_param_value
# MIDDLE-DANGEROUS!
# In case of reports (later even exports) substitute None for unknown
# parameters. We suppose Python syntax for parameters!
......@@ -240,14 +270,21 @@ def selectKwargsForCallable(func, initial_kwargs, kwargs_dict):
# this way we can mimic synchronous rendering when all form field values
# were available in `kwargs_dict`. It is obviously wrong behaviour.
for func_param in func_param_list:
if "*" in func_param:
if "*" in func_param[0]:
continue
if "=" in func_param:
if len(func_param) > 1: # default value exists
continue
# now we have only mandatory parameters
func_param = func_param.strip()
func_param = func_param[0].strip()
if func_param not in initial_kwargs:
initial_kwargs[func_param] = None
# If the method does not specify **kwargs we need to remove unwanted parameters
if len(func_param_name_list) > 0 and "**" not in func_param_name_list[-1]:
initial_param_list = tuple(initial_kwargs.keys()) # copy the keys
for initial_param in initial_param_list:
if initial_param not in func_param_name_list:
del initial_kwargs[initial_param]
return initial_kwargs
......@@ -415,15 +452,24 @@ url_template_dict = {
"&relative_url=%(relative_url)s&view=%(view)s&form_id=%(form_id)s",
"traverse_template": "%(root_url)s/%(script_id)s?mode=traverse" + \
"{&relative_url,view}",
"worklist_template": "%(root_url)s/%(script_id)s?mode=worklist",
# Search template will call standard "searchValues" on a document described by `root_url`
"search_template": "%(root_url)s/%(script_id)s?mode=search" + \
"{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}",
"worklist_template": "%(root_url)s/%(script_id)s?mode=worklist",
# 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).
"custom_search_template": "%(root_url)s/%(script_id)s?mode=search" + \
"&relative_url=%(relative_url)s" \
"&form_relative_url=%(form_relative_url)s" \
"&list_method=%(list_method)s" \
"&form=%(form_id)s" \
"&default_param_json=%(default_param_json)s" \
"{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}",
# Non-editable searches suppose the search results will be rendered as-is and no template
# fields will get involved. Unfortunately, fields need to be resolved because of formatting
# all the time so we abandoned this no_editable version
"custom_search_template_no_editable": "%(root_url)s/%(script_id)s?mode=search" + \
"&relative_url=%(relative_url)s" \
"&list_method=%(list_method)s" \
......@@ -445,6 +491,12 @@ default_document_uri_template = url_template_dict["jio_get_template"]
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):
return '/'.join(portal.portal_url.getRelativeContentPath(document))
......@@ -462,8 +514,10 @@ def getFormRelativeUrl(form):
def getFieldDefault(form, field, key, value=None):
"""Get available value for `field` preferably in python-object from REQUEST or from field's default."""
if value is None:
value = (REQUEST.form.get(field.id, REQUEST.form.get(key, None)) or
field.get_value('default', request=REQUEST, REQUEST=REQUEST))
value = REQUEST.form.get(field.id, REQUEST.form.get(key, MARKER))
# use marker because default value can be intentionally empty string
if value is MARKER:
value = field.get_value('default', request=REQUEST, REQUEST=REQUEST)
if field.has_value("unicode") and field.get_value("unicode") and isinstance(value, unicode):
value = unicode(value, form.get_form_encoding())
if getattr(value, 'translate', None) is not None:
......@@ -623,8 +677,14 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
rel_form = getattr(context, listbox_form_name)
# find listbox field
listbox_form_field = filter(lambda f: f.getId() == listbox_field_name, rel_form.get_fields())[0]
rel_cache = {'form_id': REQUEST.get('form_id', MARKER), 'field_id': REQUEST.get('field_id', MARKER)}
rel_cache = {
'form_id': REQUEST.get('form_id', MARKER),
'field_id': REQUEST.get('field_id', MARKER),
'relation_form_id': REQUEST.get('relation_form_id', MARKER)
}
REQUEST.set('form_id', rel_form.id)
REQUEST.set('relation_form_id', rel_form.id)
REQUEST.set('field_id', listbox_form_field.id)
# get original definition
......@@ -771,22 +831,14 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# still in the request which is not our case because we do asynchronous rendering
if list_method is not None:
selectKwargsForCallable(list_method, list_method_query_dict, REQUEST)
# Now if the list_method does not specify **kwargs we need to remove
# unwanted parameters like "portal_type" which is everywhere
if hasattr(list_method, 'params') and "**" not in list_method.params():
_param_key_list = tuple(list_method_query_dict.keys()) # copy the keys
for param_key in _param_key_list:
if param_key not in list_method.params(): # we search in raw string
del list_method_query_dict[param_key] # but it is enough
if (True): # editable_column_list (used to be but we need
# template fields resolution (issued by existence of `form_relative_url`)
# to always kick in
if (True): # editable_column_list (we need that template fields resolution
# (issued by existence of `form_relative_url`) always kicks in
list_method_custom = url_template_dict["custom_search_template"] % {
"root_url": site_root.absolute_url(),
"script_id": script.id,
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id),
"form_id": REQUEST.get('form_id', form.id), # in case of dialogs request.form_id should be already correct
"list_method": list_method_name,
"default_param_json": urlsafe_b64encode(
json.dumps(ensureSerializable(list_method_query_dict)))
......@@ -922,7 +974,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# Following pop/push of form_id resp. dialog_id is here because of FormBox - an embedded form in a form
# Fields of forms use form_id in their TALES expressions and obviously FormBox's form_id is different
# from its parent's form
# from its parent's form. It is very important that we do not remove form_id in case of a Dialog Form.
if form.pt == "form_dialog":
previous_request_other['dialog_id'] = REQUEST.other.pop('dialog_id', None)
REQUEST.set('dialog_id', form.id)
......@@ -932,7 +984,6 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
field_errors = REQUEST.get('field_errors', {})
#hardcoded
include_action = True
if form.pt == 'form_dialog':
action_to_call = "Base_callDialogMethod"
......@@ -1007,10 +1058,27 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# some dialog actions (Print Module) use previous selection name
if REQUEST.get('selection_name', None):
addHiddenFieldToForm(response_dict, "selection_name", REQUEST.get('selection_name'))
# some dialog actions use custom cancel_url
if REQUEST.get('cancel_url', None):
addHiddenFieldToForm(response_dict, "cancel_url", REQUEST.get('cancel_url'))
# Let's support Selections!
# There are two things needed - first is to support `uid` parameter of list_function in
# a listbox (if present) in this (dialog) form
# Second is to pass `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
addHiddenFieldToForm(response_dict, "query", query or "")
else:
# In form_view we place only form_id in the request form
addHiddenFieldToForm(response_dict, 'form_id', form.id)
if REQUEST.get('relation_form_id', None):
addHiddenFieldToForm(response_dict, "relation_form_id", REQUEST.get('relation_form_id'))
if (form.pt == 'report_view'):
# reports are expected to return list of ReportSection which is a wrapper
# around a form - thus we will need to render those forms
......@@ -1203,6 +1271,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
is_site_root = False
except:
raise NotImplementedError(relative_url)
result_dict = {
'_debug': mode,
'_links': {
......@@ -1300,7 +1369,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
erp5_action_dict = portal.Base_filterDuplicateActions(
portal.portal_actions.listFilteredActionsFor(traversed_document))
for erp5_action_key in erp5_action_dict.keys():
for view_action in erp5_action_dict[erp5_action_key]:
# Try to embed the form in the result
......@@ -1311,6 +1379,16 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# so real form_id of a previous view stays untouched.
# Here we save previous form_id to `last_form_id` so it does not get overriden by `dialog_id`
last_form_id = REQUEST.get('form_id', "") if REQUEST is not None else ""
last_listbox = None
# So we can do some magic with it! Namely get previous selection (if exists) and deprecated
# selection_name which is often required (for e.g. Folder_viewWorkflowActionDialog)
try:
if last_form_id:
last_form = getattr(context, last_form_id)
last_listbox = last_form.Form_getListbox()
except AttributeError:
pass
form_id = ""
if (embedded_url is not None):
# XXX Try to fetch the form in the traversed_document of the document
......@@ -1339,10 +1417,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# 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)
# Embedded Form can be a Script or even a class method thus we mitigate here
# If our "form" is actually a Script (nothing is sure in ERP5) then execute it here
try:
if "Script" in renderer_form.meta_type:
# we suppose that the script takes only what is given in the URL params
......@@ -1363,6 +1438,18 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
return traversed_document.Base_redirect(keep_items={
'portal_status_message': status_message})
# In order not to use Selections we need to pass all search attributes to *a listbox inside form dialog*
# in case there was a listbox in the previous form. No other case!
if getattr(renderer_form, "pt", "") == "form_dialog" and last_form_id and 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`)
query_param_dict["uid"] = lazyUidList(traversed_document, last_listbox, query)
# Request is later used for method's arguments discovery so set URL params into REQUEST (just like it was sent by form)
for query_key, query_value in query_param_dict.items():
REQUEST.set(query_key, query_value)
# Embedded Form can be a Script or even a class method thus we mitigate here
renderForm(traversed_document, renderer_form, embedded_dict)
result_dict['_embedded'] = {
......@@ -1395,12 +1482,14 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# XXX This line is only optimization for shorter URL and thus is ugly
if not (form_id or last_form_id):
url_template_key = "traverse_generator"
erp5_action_list[-1]['href'] = url_template_dict[url_template_key] % {
"root_url": site_root.absolute_url(),
"script_id": script.id,
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"view": erp5_action_list[-1]['name'],
"form_id": form_id if form_id and renderer_form.pt == "form_view" else last_form_id
# add form_id for actions going from module view (form_list) and from document view (form_view)
"form_id": form_id if form_id and renderer_form.pt in ("form_view", "form_list") else last_form_id
}
if erp5_action_key == 'object_jump':
......@@ -1543,6 +1632,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# limit: [15, 16] (begin_index, num_records)
# local_roles: TODO
# selection_domain: JSON string: {region: 'foo/bar'}
# form: "Form_someName" (for search issued from dialog)
#
# Default Param JSON contains
# portal_type: list of Portal Types to include (singular form matches the
......@@ -1554,6 +1644,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
# > 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.
#
# 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":
response.setStatus(405)
......@@ -1561,6 +1655,14 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# set 'here' for field rendering which contain TALES expressions
REQUEST.set('here', traversed_document)
# a bit hack-ish (sorry)
# search issued from dialog needs previous form_id because often it just
# copies the previous search thus we need to pass it and we do not want
# to introduce another parameter to getHateos so we reuse `form`
# This is needed for example for erp5_core/Folder_viewDeleteDialog/listbox
# (see TALES expression for form_id and field_id there)
if isinstance(form, str):
REQUEST.set('form_id', form)
# in case we have custom list method
catalog_kw = {}
......@@ -1615,7 +1717,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
elif sort_order.lower().startswith("desc"):
sort_order = "DESC"
else:
# should raise an ValueError instead
# should raise a ValueError instead
log('Wrong sort order "{}" in {}! It must start with "asc" or "desc"'.format(sort_order, form_relative_url),
level=200) # error
return (sort_col, sort_order)
......@@ -1663,7 +1765,8 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
listbox_form = getattr(traversed_document, source_field.aq_parent.id)
# field TALES expression evaluated by Base_getRelatedObjectParameter requires that
REQUEST.other['form_id'] = listbox_form.id
# REQUEST.other['form_id'] = listbox_form.id
# we have to go without that - we touch REQUEST as less as possible
for select in select_list:
# See Listbox.py getValueList --> getEditableField & getColumnAliasList method
......
portal_actions | clone_document
portal_actions | create_a_document
portal_actions | delete_document
\ No newline at end of file
portal_actions | delete_document
portal_actions | delete_document_list
\ No newline at end of file
......@@ -11,6 +11,20 @@
table_template = Handlebars.compile(gadget_klass.__template_element
.getElementById("table-template")
.innerHTML);
/** Add parameters of the function to the link
*/
function addParamToLink(link, params, encode) {
var key, query = "";
for (key in params) {
if (params.hasOwnProperty(key) && params[key] !== undefined && params[key] !== null) {
query += "&" + key + "=" + params[key];
}
}
if (encode) {
return link + window.encodeURIComponent(query);
}
return link + query;
}
/** Render translated HTML of title + links
*
......@@ -19,15 +33,15 @@
* @param {Array} command_list - array of links obtained from ERP5 HATEOAS
*/
function renderLinkList(gadget, title, icon, erp5_link_list) {
return new RSVP.Queue()
.push(function () {
return gadget.getUrlParameter("extended_search")
.push(function (query) {
// obtain RJS links from ERP5 links
return RSVP.all(
erp5_link_list.map(function (erp5_link) {
return gadget.getUrlFor({
"command": 'change',
"options": {
"view": erp5_link.href,
"view": addParamToLink(erp5_link.href, {query: query}),
"page": undefined
}
});
......@@ -62,6 +76,7 @@
.declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
/////////////////////////////////////////////////////////////////
// declared methods
......
......@@ -216,7 +216,7 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
<value> <string>superkato</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>965.57861.34804.9762</string> </value>
<value> <string>966.10019.10616.58606</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1519987107.92</float>
<float>1521018417.56</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -6,46 +6,32 @@
function submitDialog(gadget, submit_action_id, is_update_method) {
var form_gadget = gadget,
action = form_gadget.state.erp5_document._embedded._view._actions.put,
form_id = form_gadget.state.erp5_document._embedded._view.form_id,
dialog_id = form_gadget.state.erp5_document._embedded._view.dialog_id,
redirect_to_parent;
return form_gadget.notifySubmitting()
return gadget.notifySubmitting()
.push(function () {
return form_gadget.getDeclaredGadget("erp5_form");
})
.push(function (erp5_form) {
return erp5_form.getContent();
return gadget.getContent();
})
.push(function (content_dict) {
var data = {},
key;
// In dialog form, dialog_id is mandatory and form_id is optional
data.dialog_id = dialog_id['default'];
if (form_id !== undefined) {
data.form_id = form_id['default'];
}
//XXX hack for redirect, defined in form
redirect_to_parent = content_dict.field_your_redirect_to_parent;
data.dialog_method = form_gadget.state.form_definition[submit_action_id];
// ERP5 expects the Action Script name in a field called "dialog_method"
// For Update Action - override the default value from "action"
content_dict.dialog_method = gadget.state.form_definition[submit_action_id];
if (is_update_method) {
data.update_method = data.dialog_method;
}
//XXX hack for redirect, difined in form
redirect_to_parent = content_dict.field_your_redirect_to_parent;
for (key in content_dict) {
if (content_dict.hasOwnProperty(key)) {
data[key] = content_dict[key];
}
content_dict.update_method = content_dict.dialog_method;
}
return form_gadget.jio_putAttachment(
form_gadget.state.jio_key,
action.href,
data
return gadget.jio_putAttachment(
gadget.state.jio_key,
action.href, // most likely points to Base_callDialogMethod
content_dict
);
})
.push(function (attachment) {
// successful JIO call will clear-out failure flag
gadget.state.fix = 0;
if (attachment.target.response.type === "application/json") {
// successful form save returns simple redirect and answer as JSON
......@@ -59,7 +45,7 @@
return form_gadget.notifySubmitted({
"message": response.portal_status_message,
"status": "success"
"status": response.portal_status_level || "success"
});
})
.push(function () {
......@@ -176,6 +162,8 @@
});
})
.push(undefined, function (error) {
/** Fail branch of the JIO call. */
gadget.state.fix = 1; // unsucessful JIO call sets persistent fail flag
if (error !== undefined && error.target !== undefined) {
var error_text = 'Encountered an unknown error. Try to resubmit',
promise_queue = new RSVP.Queue();
......@@ -249,6 +237,17 @@
dialog_button_template = Handlebars.compile(dialog_button_source);
gadget_klass
/** Dialogs send&execute actions. Actions can have failures and need
confirmation about how to behave. So far we have no possibility
of backend asking for a confirmation or modifying frontend state.
Therefor we introduce failure-flag state.fix. This flag, if set,
is sent together with re-submission of the dialog thus can act as
binary yes/no decision for the backend (pressing Cancel equals "No").
*/
.setState({
"fix": 0 // failure flag (if == 1 a server failure response was obtained)
})
/////////////////////////////////////////////////////////////////
// acquisition
/////////////////////////////////////////////////////////////////
......@@ -273,12 +272,49 @@
return declared_gadget.checkValidity();
});
}, {mutex: 'changestate'})
.declareMethod('getContent', function () {
var gadget = this,
form_id = gadget.state.erp5_document._embedded._view.form_id,
dialog_id = gadget.state.erp5_document._embedded._view.dialog_id,
query = gadget.state.erp5_document._embedded._view.query;
return this.getDeclaredGadget("erp5_form")
.push(function (declared_gadget) {
return declared_gadget.getContent();
.push(function (sub_gadget) {
return sub_gadget.getContent();
})
.push(function (content_dict) {
/** Extend form data with (many) dialog-specific items */
var data = {},
key;
// create a copy of sub_data so we do not modify them in-place
for (key in content_dict) {
if (content_dict.hasOwnProperty(key)) {
data[key] = content_dict[key];
}
}
// ERP5 expects target Script name in dialog_method field
data.dialog_method = gadget.state.form_definition.action;
// In dialog form, dialog_id is mandatory and form_id is optional
data.dialog_id = dialog_id['default'];
if (form_id !== undefined) {
data.form_id = form_id['default'];
}
// We pass query to the dialogScript so it has access to the
// document's selection from the previous view (using an DB cursor)
if (query !== undefined) {
data.query = query['default'];
}
// Send failure flag on resubmit (after a failed submission)
if (gadget.state.fix === 1) {
data.fix = 1;
}
return data;
});
}, {mutex: 'changestate'})
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
......
......@@ -216,7 +216,7 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
<value> <string>superkato</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>965.50744.39391.46916</string> </value>
<value> <string>966.9931.24463.1399</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1519729953.99</float>
<float>1521041798.1</float>
<string>UTC</string>
</tuple>
</state>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testDeleteDocumentList</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</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test RenderJS UI</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<tr><th colspan="3">Set ListBox to show the State of Foo documents (and 10 documents at once)</th></tr>
<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>
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_columns=id%7CID%0Atitle%7CTitle%0Adelivery.quantity%7CQuantity%0Asimulation_state%7CState&amp;field_lines=10</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Set Successfully.</td><td></td></tr>
<!-- Create 3 Foo objects relation, 2 with embedded documents and one empty -->
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=3&amp;big_category_related:bool=True</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=4&amp;num:int=2&amp;create_line:bool=True</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=6&amp;num:int=1</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>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module</td><td></td></tr>
<tal:block tal:define="search_query string:( title: &quot;Title 3&quot; OR id: &quot;5&quot; )">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/search_in_form_list" />
</tal:block>
<tal:block tal:define="pagination_configuration python: {'header': '(2)', 'footer': '2 Records'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><td>waitForElementPresent</td>
<td>//a[@data-i18n='Actions']</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n='Actions']</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//a[@data-i18n='Delete Multiple Documents']</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n='Delete Multiple Documents']</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/submit_dialog" />
<tr><td>waitForElementNotPresent</td>
<td>//input[@name="action_confirm"]</td><td></td></tr>
<tal:block tal:define="notification_configuration python: {'class': 'success',
'text': 'Deleted 2 documents'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tal:block tal:define="pagination_configuration python: {'header': '(2)', 'footer': '2 Records'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[1]/td[4]//a</td>
<td>deleted</td></tr>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[2]/td[4]//a</td>
<td>deleted</td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/clear_query" />
<tal:block tal:define="pagination_configuration python: {'header': '(6)', 'footer': '6 Records'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[1]/td[4]//a</td>
<td>draft</td></tr>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[3]/td[4]//a</td>
<td>deleted</td></tr>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[5]/td[4]//a</td>
<td>deleted</td></tr>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[6]/td[4]//a</td>
<td>draft</td></tr>
<tr><td>waitForElementPresent</td>
<td>//a[@data-i18n='Actions']</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n='Actions']</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//a[@data-i18n='Delete Multiple Documents']</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n='Delete Multiple Documents']</td><td></td></tr>
<tal:block tal:define="pagination_configuration python: {'header': '(6)', 'footer': '6 Records'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/submit_dialog" />
<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>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/submit_dialog" />
<tr><td>waitForElementNotPresent</td>
<td>//input[@name="action_confirm"]</td><td></td></tr>
<tal:block tal:define="notification_configuration python: {'class': 'success',
'text': 'Deleted 6 documents'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tal:block tal:define="pagination_configuration python: {'header': '(6)', 'footer': '6 Records'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/check_listbox_pagination_text" />
</tal:block>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[1]/td[4]//a</td>
<td>deleted</td></tr>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[4]/td[4]//a</td>
<td>deleted</td></tr>
<tr><td>assertText</td>
<td>//div[@data-gadget-scope="field_listbox"]//table/tbody/tr[6]/td[4]//a</td>
<td>deleted</td></tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
......@@ -57,8 +57,10 @@ def getDocumentGroupByWorkflowStateList(self, form_id='', **kw):
Base_translateString = portal.Base_translateString
wf_tool = portal.portal_workflow
selection_tool = portal.portal_selections
last_form = getattr(self, form_id)
selection_name = request['selection_name']
last_listbox = last_form.Form_getListbox()
selection_name = last_listbox.get_value('selection_name')
# guess all column name from catalog schema
possible_state_list = [column_name.split('.')[1] for column_name in
......
......@@ -45,7 +45,9 @@
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
<tuple>
<string>W: 55, 2: Unused variable \'request\' (unused-variable)</string>
</tuple>
</value>
</item>
<item>
......
"""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
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=\'\'</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_searchUsingListbox</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
"""Script to remove Documents inside a Folder.
The new UI does not make any exceptions and treat this script as a generic Dialog Form Method.
Thus it receives form_id of previous form, dialog_id of current dialog and uid of objects from
previous Listbox.
:param fix: {int} set to 1 if this action displayed warning/error and the user resubmits
"""
from ZODB.POSException import ConflictError
from Products.CMFCore.WorkflowCore import WorkflowException
portal = context.getPortalObject()
Base_translateString = portal.Base_translateString
translate = Base_translateString
REQUEST = portal.REQUEST
try:
fix = int(fix)
except ValueError:
fix = 0
uids = portal.portal_selections.getSelectionCheckedUidsFor(selection_name)
if portal.portal_selections.selectionHasChanged(md5_object_uid_list, uids):
message = Base_translateString("Sorry, your selection has changed.")
elif uids:
# Check if there is some related objets.
object_list = [x.getObject() for x in context.Folder_getDeleteObjectList(uid=uids)]
object_used = sum([x.getRelationCountForDeletion() and 1 for x in object_list])
if selection_name:
uids = portal.portal_selections.getSelectionCheckedUidsFor(selection_name)
if portal.portal_selections.selectionHasChanged(md5_object_uid_list, uids):
return context.Base_renderMessage(translate("Sorry, your selection has changed."), "warning")
if object_used > 0:
if object_used == 1:
message = Base_translateString("Sorry, 1 item is in use.")
if not uids:
return context.Base_renderMessage(translate("Please select one or more items first."), "warning")
if True: # useless indentation
# check if selected documents contain related objects because we
# cannot delete those
search_result = context.Folder_getDeleteObjectList(uid=uids)
object_list = [x.getObject() for x in context.Folder_getDeleteObjectList(uid=uids)]
object_list_len = len(object_list)
object_used_list = [x for x in object_list if x.getRelationCountForDeletion() > 0]
object_used_list_len = len(object_used_list)
if not fix and object_used_list_len > 0:
if selection_name:
# if we have selection_name then we work with old-style Selections
for x in object_used_list:
uids.remove(x.getUid())
portal.portal_selections.setSelectionCheckedUidsFor(selection_name, uids)
return context.Base_renderMessage(
Base_translateString("Unselecting ${count} out of ${total} documents with relations because they cannot be deleted",
mapping={"count": object_used_list_len, 'total': object_list_len}),
level='warning'
)
else:
message = Base_translateString("Sorry, ${count} items are in use.",
mapping={'count': repr(object_used)})
else:
# No selection_name means we are in non-XHTML interface thus notify user that re-submission
# will trigger delete and omit the undeletable documents
# if user re-confirms we receive fix=1 flag
return context.Base_renderMessage(
Base_translateString('Cannot delete ${count} out of ${total} documents because of related documents. Click "Delete" again to omit them and delete the rest',
mapping={"count": object_used_list_len, 'total': object_list_len}),
level='warning'
)
if fix and object_used_list_len > 0:
# the user re-submitted the dialog after seeing an error thus remove objects with relations and delete the rest
for x in object_used_list:
uids.remove(x.getUid())
object_list = [x.getObject() for x in context.Folder_getDeleteObjectList(uid=uids)]
if True: # useless indentation adding cyclomatic complexity
# Do not delete objects which have a workflow history
object_to_remove_list = []
......@@ -54,9 +97,9 @@ elif uids:
REQUEST=REQUEST)
except ConflictError:
raise
except Exception, message:
pass
else:
except Exception as error:
return context.Base_renderMessage(str(error), "error")
else: # in the case of no exception raised report sucess
object_ids = [x.getId() for x in object_to_remove_list]
comment = Base_translateString('Deleted objects: ${object_ids}',
mapping={'object_ids': object_ids})
......@@ -68,12 +111,13 @@ elif uids:
# no 'edit_action' transition for this container
pass
message = Base_translateString("Deleted.")
message = Base_translateString("Deleted ${count} documents", mapping={"count": len(object_list)})
# Change workflow state of others objects
# Try to call "delete_action" workflow transition on documents which defined it
# Failure of such a call is not a failure globally. The document was deleted anyway
not_deleted_count = 0
for object in object_to_delete_list:
# Hidden transition (without a message displayed)
# Hidden transition (without a message displayed)
# are not returned by getActionsFor
try:
portal.portal_workflow.doActionFor(object, 'delete_action')
......@@ -82,18 +126,9 @@ elif uids:
except:
not_deleted_count += 1
# Generate message
if not_deleted_count == 1:
message = Base_translateString("Sorry, you can not delete ${count} item.",
mapping={'count': not_deleted_count})
elif not_deleted_count > 1:
message = Base_translateString("Sorry, you can not delete ${count} items.",
mapping={'count': not_deleted_count})
qs = '?portal_status_message=%s' % message
# make sure nothing is checked after
portal.portal_selections.setSelectionCheckedUidsFor(selection_name, ())
else:
message = Base_translateString("Please select one or more items first.")
if selection_name:
portal.portal_selections.setSelectionCheckedUidsFor(selection_name, ())
return context.Base_redirect(form_id, keep_items={"portal_status_message":message})
return context.Base_redirect(form_id, keep_items={"portal_status_message": message})
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>form_id=\'\',selection_index=None,object_uid=None,selection_name=None,field_id=None,cancel_url=\'\',md5_object_uid_list=\'\'</string> </value>
<value> <string>form_id=\'\',selection_index=None,object_uid=None,selection_name=None,field_id=None,cancel_url=\'\',md5_object_uid_list=\'\', uids=(), fix=0, **kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
# XXX This is a hack which allow to delete non indexed Template
# Never call listFolderContents in a place where there could be million of
# documents!
if context.getPortalType() == 'Preference':
result = []
uid_list = kw.get('uid', [])
for i in context.listFolderContents():
if i.getUid() in uid_list:
if i.getUid() in uid:
result.append(i)
return result
else:
return context.portal_catalog(**kw)
# it is enough to search with uids because it is the most specific attribute
return context.portal_catalog(uid=uid, **kw)
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>**kw</string> </value>
<value> <string>uid, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
......@@ -272,7 +272,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>request/proxy_field_id | python: here.portal_selections.getSelectionParamsFor(\'folder_delete_selection\').get(\'field_id\', \'listbox\')</string> </value>
<value> <string>python: here.Form_getListbox(form_id=request.get(\'form_id\')).getId()</string> </value>
</item>
</dictionary>
</pickle>
......@@ -285,7 +285,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>request/proxy_form_id | python: here.portal_selections.getSelectionParamsFor(\'folder_delete_selection\')[\'form_id\']</string> </value>
<value> <string>request/form_id</string> </value>
</item>
</dictionary>
</pickle>
......
# 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"
"""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":
......@@ -11,12 +17,14 @@ def isListBox(field):
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 != 'ERP5 Form':
return None
raise RuntimeError("Cannot get Listbox field from {!s}! Supported is only ERP5 Form".format(form.meta_type))
if form.has_field('listbox'):
return form.get_field('listbox')
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>form=None</string> </value>
<value> <string>form=None, form_id=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
"""
Return HTTP Redirect with message to be displayed.
Unfortunately XHTML UI cannot distinguish between error levels - everything is a message so we ignore
most of the parameters here.
:param message: {str}
:param level: {str | int} use ERP5Type.Log levels or simply strings like "info", "warning", or "error"
"""
return context.Base_redirect(
keep_items={"portal_status_message": str(message)})
<?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>message, level="error", request=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_renderMessage</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -25,12 +25,29 @@ class _(PatchClass(ExternalMethod)):
@property
def func_defaults(self):
"""Return a tuple of default values.
The first value is for the "second" parameter (self is ommited)
Example:
componentFunction(self, form_id='', **kw)
will have func_defaults = ('', )
"""
return self._getFunction()[1]
@property
def func_code(self):
return self._getFunction()[2]
@property
def func_args(self):
"""Return list of parameter names.
Example:
componentFunction(self, form_id='', **kw)
will have func_args = ['self', 'form_id']
"""
return self._getFunction()[4]
def getFunction(self, reload=False):
return self._getFunction(reload)[0]
......@@ -74,14 +91,21 @@ class _(PatchClass(ExternalMethod)):
except AttributeError:
pass
code = f.func_code
args = getargs(code)[0]
argument_object = getargs(code)
# reconstruct back the original names
arg_list = argument_object.args[:]
if argument_object.varargs:
arg_list.append('*' + argument_object.varargs)
if argument_object.keywords:
arg_list.append('**' + argument_object.keywords)
i = isinstance(f, MethodType)
ff = f.__func__ if i else f
has_self = len(args) > i and args[i] == 'self'
has_self = len(arg_list) > i and arg_list[i] == 'self'
i += has_self
if i:
code = FuncCode(ff, i)
self._v_f = _f = (f, f.func_defaults, code, has_self)
self._v_f = _f = (f, f.func_defaults, code, has_self, arg_list)
return _f
def __call__(self, *args, **kw):
......
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