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 @@ ...@@ -78,7 +78,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>text</string> </key> <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> </item>
</dictionary> </dictionary>
</pickle> </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. Generic method called when submitting a form in dialog mode.
Responsible for validating form data and redirecting to the form action. 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.ERP5Type.Log import log, DEBUG, INFO, WARNING
from Products.Formulator.Errors import FormValidationError, ValidationError
from ZTUtils import make_query
import json import json
# XXX We should not use meta_type properly,
# XXX We need to discuss this problem.(yusei)
def isFieldType(field, type_name): def isFieldType(field, type_name):
if field.meta_type == 'ProxyField': if field.meta_type == 'ProxyField':
field = field.getRecursiveTemplateField() field = field.getRecursiveTemplateField()
return field.meta_type == type_name return field.meta_type == type_name
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) # 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 # and use container.REQUEST just to introduce yet another global state. Maybe because
# container.REQUEST is used in other places. # container.REQUEST is used in other places.
...@@ -31,6 +30,19 @@ translate = context.Base_translateString ...@@ -31,6 +30,19 @@ translate = context.Base_translateString
# Make this script work alike no matter if called by a script or a request # Make this script work alike no matter if called by a script or a request
kw.update(request_form) 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 # Exceptions for UI
if dialog_method == 'Base_configureUI': if dialog_method == 'Base_configureUI':
return context.Base_configureUI(form_id=kw['form_id'], return context.Base_configureUI(form_id=kw['form_id'],
...@@ -70,11 +82,11 @@ if dialog_method == 'Base_createRelation': ...@@ -70,11 +82,11 @@ if dialog_method == 'Base_createRelation':
dialog_id=dialog_id, dialog_id=dialog_id,
portal_type=kw['portal_type'], portal_type=kw['portal_type'],
return_url=kw['cancel_url']) return_url=kw['cancel_url'])
# Exception for folder delete # NO Exception for folder delete
if dialog_method == 'Folder_delete': # if dialog_method == 'Folder_delete':
return context.Folder_delete(form_id=kw['form_id'], # return context.Folder_delete(form_id=kw['form_id'],
selection_name=kw['selection_name'], # selection_name=kw['selection_name'],
md5_object_uid_list=kw['md5_object_uid_list']) # md5_object_uid_list=kw['md5_object_uid_list'])
form = getattr(context, dialog_id) form = getattr(context, dialog_id)
...@@ -90,7 +102,6 @@ try: ...@@ -90,7 +102,6 @@ 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 = context.getPortalObject().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 \
...@@ -161,30 +172,25 @@ if len(listbox_id_list): ...@@ -161,30 +172,25 @@ 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 = request_form.get("query", None)
if selection_list is not None: fix = int(request_form.get("fix", "0"))
object_uid_list = map(lambda x:x.getObject().getUid(), selection_list) if query != "" or (query == "" and fix > 0): # force empty query when fix == 1
error = context.portal_selections.selectionHasChanged(kw['previous_md5_object_uid_list'], object_uid_list) listbox = getattr(context, form_id).Form_getListbox()
if error: kw['uids'] = [int(getattr(document, "uid"))
error_message = context.Base_translateString("Sorry, your selection has changed.") 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 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) 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. # Remove empty values for make_query.
clean_kw = dict((k, v) for k, v in kw.items() if v not in (None, [], ())) clean_kw = dict((k, v) for k, v in kw.items() if v not in (None, [], ()))
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <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>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
......
""" """UI Script to redirect the user to `context` with optional custom view `form_id`.
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.
TODO: improve API and extensively document. ERP5Site_redirect may :param keep_items: is used mainly to pass "portal_status_message" to be showed to the user
be redundant. the new UI supports "portal_status_level" with values "success" or "error"
""" """
from ZTUtils import make_query from ZTUtils import make_query
import json import json
...@@ -40,8 +37,15 @@ response.setHeader("X-Location", "urn:jio:get:%s" % context.getRelativeUrl()) ...@@ -40,8 +37,15 @@ response.setHeader("X-Location", "urn:jio:get:%s" % context.getRelativeUrl())
# therefor we don't need to be afraid of clashes # therefor we don't need to be afraid of clashes
response.setHeader("Content-type", "application/json; charset=utf-8") 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 = { result_dict = {
'portal_status_message': "%s" % keep_items.pop("portal_status_message", ""), 'portal_status_message': "%s" % keep_items.pop("portal_status_message", ""),
'portal_status_level': "%s" % portal_status_level,
'_links': { '_links': {
"self": { "self": {
# XXX Include query parameters # XXX Include query parameters
......
portal_actions | clone_document portal_actions | clone_document
portal_actions | create_a_document portal_actions | create_a_document
portal_actions | delete_document portal_actions | delete_document
\ No newline at end of file portal_actions | delete_document_list
\ No newline at end of file
...@@ -11,6 +11,20 @@ ...@@ -11,6 +11,20 @@
table_template = Handlebars.compile(gadget_klass.__template_element table_template = Handlebars.compile(gadget_klass.__template_element
.getElementById("table-template") .getElementById("table-template")
.innerHTML); .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 /** Render translated HTML of title + links
* *
...@@ -19,15 +33,15 @@ ...@@ -19,15 +33,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": addParamToLink(erp5_link.href, {query: query}),
"page": undefined "page": undefined
} }
}); });
...@@ -62,6 +76,7 @@ ...@@ -62,6 +76,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
......
...@@ -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.10019.10616.58606</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>1521018417.56</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -6,46 +6,32 @@ ...@@ -6,46 +6,32 @@
function submitDialog(gadget, submit_action_id, is_update_method) { function submitDialog(gadget, submit_action_id, is_update_method) {
var form_gadget = gadget, var form_gadget = gadget,
action = form_gadget.state.erp5_document._embedded._view._actions.put, 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; redirect_to_parent;
return form_gadget.notifySubmitting() return gadget.notifySubmitting()
.push(function () { .push(function () {
return form_gadget.getDeclaredGadget("erp5_form"); return gadget.getContent();
})
.push(function (erp5_form) {
return erp5_form.getContent();
}) })
.push(function (content_dict) { .push(function (content_dict) {
var data = {}, //XXX hack for redirect, defined in form
key; redirect_to_parent = content_dict.field_your_redirect_to_parent;
// 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'];
}
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) { if (is_update_method) {
data.update_method = data.dialog_method; content_dict.update_method = content_dict.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];
}
} }
return form_gadget.jio_putAttachment( return gadget.jio_putAttachment(
form_gadget.state.jio_key, gadget.state.jio_key,
action.href, action.href, // most likely points to Base_callDialogMethod
data content_dict
); );
}) })
.push(function (attachment) { .push(function (attachment) {
// successful JIO call will clear-out failure flag
gadget.state.fix = 0;
if (attachment.target.response.type === "application/json") { if (attachment.target.response.type === "application/json") {
// successful form save returns simple redirect and answer as JSON // successful form save returns simple redirect and answer as JSON
...@@ -59,7 +45,7 @@ ...@@ -59,7 +45,7 @@
return form_gadget.notifySubmitted({ return form_gadget.notifySubmitted({
"message": response.portal_status_message, "message": response.portal_status_message,
"status": "success" "status": response.portal_status_level || "success"
}); });
}) })
.push(function () { .push(function () {
...@@ -176,6 +162,8 @@ ...@@ -176,6 +162,8 @@
}); });
}) })
.push(undefined, function (error) { .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) { if (error !== undefined && error.target !== undefined) {
var error_text = 'Encountered an unknown error. Try to resubmit', var error_text = 'Encountered an unknown error. Try to resubmit',
promise_queue = new RSVP.Queue(); promise_queue = new RSVP.Queue();
...@@ -249,6 +237,17 @@ ...@@ -249,6 +237,17 @@
dialog_button_template = Handlebars.compile(dialog_button_source); dialog_button_template = Handlebars.compile(dialog_button_source);
gadget_klass 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 // acquisition
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -273,12 +272,49 @@ ...@@ -273,12 +272,49 @@
return declared_gadget.checkValidity(); return declared_gadget.checkValidity();
}); });
}, {mutex: 'changestate'}) }, {mutex: 'changestate'})
.declareMethod('getContent', function () { .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") return this.getDeclaredGadget("erp5_form")
.push(function (declared_gadget) { .push(function (sub_gadget) {
return declared_gadget.getContent(); 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'}) }, {mutex: 'changestate'})
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// declared methods // declared methods
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
......
...@@ -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.50744.39391.46916</string> </value> <value> <string>966.9931.24463.1399</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>1519729953.99</float> <float>1521041798.1</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </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): ...@@ -57,8 +57,10 @@ def getDocumentGroupByWorkflowStateList(self, form_id='', **kw):
Base_translateString = portal.Base_translateString Base_translateString = portal.Base_translateString
wf_tool = portal.portal_workflow wf_tool = portal.portal_workflow
selection_tool = portal.portal_selections 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 # guess all column name from catalog schema
possible_state_list = [column_name.split('.')[1] for column_name in possible_state_list = [column_name.split('.')[1] for column_name in
......
...@@ -45,7 +45,9 @@ ...@@ -45,7 +45,9 @@
<item> <item>
<key> <string>text_content_warning_message</string> </key> <key> <string>text_content_warning_message</string> </key>
<value> <value>
<tuple/> <tuple>
<string>W: 55, 2: Unused variable \'request\' (unused-variable)</string>
</tuple>
</value> </value>
</item> </item>
<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 ZODB.POSException import ConflictError
from Products.CMFCore.WorkflowCore import WorkflowException from Products.CMFCore.WorkflowCore import WorkflowException
portal = context.getPortalObject() portal = context.getPortalObject()
Base_translateString = portal.Base_translateString Base_translateString = portal.Base_translateString
translate = Base_translateString
REQUEST = portal.REQUEST REQUEST = portal.REQUEST
try:
fix = int(fix)
except ValueError:
fix = 0
uids = portal.portal_selections.getSelectionCheckedUidsFor(selection_name) if selection_name:
if portal.portal_selections.selectionHasChanged(md5_object_uid_list, uids): uids = portal.portal_selections.getSelectionCheckedUidsFor(selection_name)
message = Base_translateString("Sorry, your selection has changed.") if portal.portal_selections.selectionHasChanged(md5_object_uid_list, uids):
elif uids: return context.Base_renderMessage(translate("Sorry, your selection has changed."), "warning")
# 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 object_used > 0: if not uids:
if object_used == 1: return context.Base_renderMessage(translate("Please select one or more items first."), "warning")
message = Base_translateString("Sorry, 1 item is in use.")
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: else:
message = Base_translateString("Sorry, ${count} items are in use.", # No selection_name means we are in non-XHTML interface thus notify user that re-submission
mapping={'count': repr(object_used)}) # will trigger delete and omit the undeletable documents
else: # 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 # Do not delete objects which have a workflow history
object_to_remove_list = [] object_to_remove_list = []
...@@ -54,9 +97,9 @@ elif uids: ...@@ -54,9 +97,9 @@ elif uids:
REQUEST=REQUEST) REQUEST=REQUEST)
except ConflictError: except ConflictError:
raise raise
except Exception, message: except Exception as error:
pass return context.Base_renderMessage(str(error), "error")
else: else: # in the case of no exception raised report sucess
object_ids = [x.getId() for x in object_to_remove_list] object_ids = [x.getId() for x in object_to_remove_list]
comment = Base_translateString('Deleted objects: ${object_ids}', comment = Base_translateString('Deleted objects: ${object_ids}',
mapping={'object_ids': object_ids}) mapping={'object_ids': object_ids})
...@@ -68,12 +111,13 @@ elif uids: ...@@ -68,12 +111,13 @@ elif uids:
# no 'edit_action' transition for this container # no 'edit_action' transition for this container
pass 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 not_deleted_count = 0
for object in object_to_delete_list: for object in object_to_delete_list:
# Hidden transition (without a message displayed) # Hidden transition (without a message displayed)
# are not returned by getActionsFor # are not returned by getActionsFor
try: try:
portal.portal_workflow.doActionFor(object, 'delete_action') portal.portal_workflow.doActionFor(object, 'delete_action')
...@@ -82,18 +126,9 @@ elif uids: ...@@ -82,18 +126,9 @@ elif uids:
except: except:
not_deleted_count += 1 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 # make sure nothing is checked after
portal.portal_selections.setSelectionCheckedUidsFor(selection_name, ()) if selection_name:
else: portal.portal_selections.setSelectionCheckedUidsFor(selection_name, ())
message = Base_translateString("Please select one or more items first.")
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 @@ ...@@ -50,7 +50,7 @@
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <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>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
......
# XXX This is a hack which allow to delete non indexed Template # XXX This is a hack which allow to delete non indexed Template
# Never call listFolderContents in a place where there could be million of # Never call listFolderContents in a place where there could be million of
# documents! # documents!
if context.getPortalType() == 'Preference': if context.getPortalType() == 'Preference':
result = [] result = []
uid_list = kw.get('uid', [])
for i in context.listFolderContents(): for i in context.listFolderContents():
if i.getUid() in uid_list: if i.getUid() in uid:
result.append(i) result.append(i)
return result return result
else: 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 @@ ...@@ -50,7 +50,7 @@
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <key> <string>_params</string> </key>
<value> <string>**kw</string> </value> <value> <string>uid, **kw</string> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
......
...@@ -272,7 +272,7 @@ ...@@ -272,7 +272,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>_text</string> </key> <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> </item>
</dictionary> </dictionary>
</pickle> </pickle>
...@@ -285,7 +285,7 @@ ...@@ -285,7 +285,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>_text</string> </key> <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> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
# Return first listbox in a form that is enabled and not hidden """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" 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): def isListBox(field):
if field.meta_type == "ListBox": if field.meta_type == "ListBox":
...@@ -11,12 +17,14 @@ def isListBox(field): ...@@ -11,12 +17,14 @@ def isListBox(field):
return True return True
return False return False
if form_id is not None:
form = getattr(context, form_id)
if form is None: if form is None:
form = context form = context
if form.meta_type != 'ERP5 Form': 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'): if form.has_field('listbox'):
return form.get_field('listbox') return form.get_field('listbox')
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
</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>
......
"""
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)): ...@@ -25,12 +25,29 @@ class _(PatchClass(ExternalMethod)):
@property @property
def func_defaults(self): 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] return self._getFunction()[1]
@property @property
def func_code(self): def func_code(self):
return self._getFunction()[2] 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): def getFunction(self, reload=False):
return self._getFunction(reload)[0] return self._getFunction(reload)[0]
...@@ -74,14 +91,21 @@ class _(PatchClass(ExternalMethod)): ...@@ -74,14 +91,21 @@ class _(PatchClass(ExternalMethod)):
except AttributeError: except AttributeError:
pass pass
code = f.func_code 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) i = isinstance(f, MethodType)
ff = f.__func__ if i else f 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 i += has_self
if i: if i:
code = FuncCode(ff, 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 return _f
def __call__(self, *args, **kw): 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