Commit cd7f0fd6 authored by Tomáš Peterka's avatar Tomáš Peterka Committed by Tomáš Peterka

[hal_json] Add Action to delete multiple documents

/reviewed-on nexedi/erp5!655
parent 3a3811b1
...@@ -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>
...@@ -76,11 +76,11 @@ if dialog_method == 'Base_createRelation': ...@@ -76,11 +76,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=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)
form_data = None form_data = None
...@@ -97,6 +97,7 @@ try: ...@@ -97,6 +97,7 @@ try:
editable_mode = request.get('editable_mode', 1) editable_mode = request.get('editable_mode', 1)
request.set('editable_mode', 1) request.set('editable_mode', 1)
form_data = form.validate_all_to_request(request) form_data = form.validate_all_to_request(request)
request.set('editable_mode', editable_mode) request.set('editable_mode', editable_mode)
default_skin = portal.portal_skins.getDefaultSkin() default_skin = portal.portal_skins.getDefaultSkin()
allowed_styles = ("ODT", "ODS", "Hal", "HalRestricted") allowed_styles = ("ODT", "ODS", "Hal", "HalRestricted")
......
<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.'}">
<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.'}">
<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
"""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 uids of objects from
previous Listbox.
The distinction between XHTML resp. RSJS interface is that in the later, this script receives
`uids` directly whether in XHTML it is given `selection_name` and must extract the uids from
the selection.
:param form_id: {str} Form ID of the previous View's FormBox
:param dialog_id: {str} current dialog's Form ID
:param uids: {list[int]} list of "selected" uids from the previous View (only in JS UI)
:param selection_name: {str} if present then user is using XHTML UI
"""
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
REQUEST = portal.REQUEST translate = Base_translateString
REQUEST = kwargs.get("REQUEST", None) or portal.REQUEST
uids = portal.portal_selections.getSelectionCheckedUidsFor(selection_name)
if portal.portal_selections.selectionHasChanged(md5_object_uid_list, uids): if selection_name:
message = Base_translateString("Sorry, your selection has changed.") uids = portal.portal_selections.getSelectionCheckedUidsFor(selection_name)
elif uids: if portal.portal_selections.selectionHasChanged(md5_object_uid_list, uids):
# Check if there is some related objets. return context.Base_redirect(keep_items={'portal_status_message': translate("Sorry, your selection has changed.")})
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 not uids:
return context.Base_redirect(keep_items={
if object_used > 0: 'portal_status_message': translate("Please select one or more items first."),
if object_used == 1: 'portal_status_level': "warning"})
message = Base_translateString("Sorry, 1 item is in use.")
else:
message = Base_translateString("Sorry, ${count} items are in use.", if True:
mapping={'count': repr(object_used)}) # already filters out documents with relations that cannot be deleted
else: object_list = context.Folder_getDeleteObjectList(uid=uids)
object_not_deletable_len = len(uids) - len(object_list)
# some documents cannot be deleted thus we stop and warn the user
if object_not_deletable_len == 1:
return context.Base_redirect(keep_items={
'portal_status_message': translate("Sorry, 1 item is in use."),
'portal_status_level': "warning"})
elif object_not_deletable_len > 1:
return context.Base_redirect(keep_items={
'portal_status_message': translate("Sorry, ${count} items are in use.", mapping={'count': str(object_not_deletable_len)}),
'portal_status_level': "warning"})
if True:
# 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 +82,9 @@ elif uids: ...@@ -54,9 +82,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})
...@@ -70,7 +98,8 @@ elif uids: ...@@ -70,7 +98,8 @@ elif uids:
message = Base_translateString("Deleted.") message = Base_translateString("Deleted.")
# 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)
...@@ -82,18 +111,8 @@ elif uids: ...@@ -82,18 +111,8 @@ 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
if selection_name:
portal.portal_selections.setSelectionCheckedUidsFor(selection_name, ()) portal.portal_selections.setSelectionCheckedUidsFor(selection_name, ())
else:
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": str(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=\'\',dialog_id=\'\',selection_index=None,object_uid=None,selection_name=None,field_id=None,cancel_url=\'\',md5_object_uid_list=\'\', uids=(), **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) return list(
filter(lambda x: x.getRelationCountForDeletion() == 0, # only docs WITHOUT relations can be deleted
map(lambda x: x.getObject(),
context.portal_catalog(uid=uid, **kw)) # kw can contain limit, sort_on and similar runtime information
)
)
...@@ -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>
......
...@@ -112,6 +112,10 @@ ...@@ -112,6 +112,10 @@
<key> <string>title</string> </key> <key> <string>title</string> </key>
<value> <string></string> </value> <value> <string></string> </value>
</item> </item>
<item>
<key> <string>url_columns</string> </key>
<value> <string></string> </value>
</item>
</dictionary> </dictionary>
</value> </value>
</item> </item>
...@@ -272,7 +276,7 @@ ...@@ -272,7 +276,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 +289,7 @@ ...@@ -285,7 +289,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>
......
...@@ -26,8 +26,15 @@ if form is None: ...@@ -26,8 +26,15 @@ if form is None:
if form.meta_type not in ('ERP5 Form', 'Folder', 'ERP5 Folder'): if form.meta_type not in ('ERP5 Form', 'Folder', 'ERP5 Folder'):
raise RuntimeError("Cannot get Listbox field from \"{!s}\"! Supported is only ERP5 Form and (ERP5) Folder".format(form.meta_type)) raise RuntimeError("Cannot get Listbox field from \"{!s}\"! Supported is only ERP5 Form and (ERP5) Folder".format(form.meta_type))
if form.has_field('listbox'): listbox = None
return form.get_field('listbox')
if "Form" in form.meta_type and form.has_field("listbox"):
listbox = form.get_field("listbox")
elif "Folder" in form.meta_type:
listbox = getattr(form, "listbox", None)
if listbox:
return listbox
# we start with 'bottom' because most of the time # we start with 'bottom' because most of the time
# the listbox is there. # the listbox is there.
......
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