Commit 2fdd5a01 authored by Tomáš Peterka's avatar Tomáš Peterka

[hal_json_style + renderjs_ui] Support Actions on multiple documents

-  [product] allow instrospecion of External Methods
-  [hal json] introspect form dialog methods for `uids` parameter to inject list of selected UIDS from the previous view
-  [hal_json] introspect listbox's list_method for `uid` parameter to inject list of selected UIDS from the previous view
-  [hal_json] add meta field `extra_param_json` into every dialog form to hold script's constant elsewhere than in fields
-  [hal_json] add dynamic field's definition into form definition so they do not need to be hardcoded in javascript
-  [product] add ability of Form to hash its data using `hash_validated_data`
-  [renderjs_ui] move form submit&response handling into one place - Page Form
-  [hal_json] Warn user about performing an action on unrestricted set of documents
-  [hal_json] Form Action failure returns full form with a message instead of just a message
-  [hal_json] Supports keep_items in Base_renderForm for Form Dialogs (where it matters)
parent 3b9d23bf
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_button</string>
<string>action_type/object_jio_button</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_button</string> </value>
<value> <string>object_jio_button</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
<string>action_type/object_jio_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
<value> <string>object_jio_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
<string>action_type/object_jio_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
<value> <string>object_jio_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_button</string>
<string>action_type/object_jio_button</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_button</string> </value>
<value> <string>object_jio_button</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_report</string>
<string>action_type/object_jio_report</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_report</string> </value>
<value> <string>object_jio_report</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_report</string>
<string>action_type/object_jio_report</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_report</string> </value>
<value> <string>object_jio_report</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_button</string>
<string>action_type/object_jio_button</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_button</string> </value>
<value> <string>object_jio_button</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_report</string>
<string>action_type/object_jio_report</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_report</string> </value>
<value> <string>object_jio_report</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_exchange</string>
<string>action_type/object_jio_exchange</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_exchange</string> </value>
<value> <string>object_jio_exchange</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_report</string>
<string>action_type/object_jio_report</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_report</string> </value>
<value> <string>object_jio_report</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_button</string>
<string>action_type/object_jio_button</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_button</string> </value>
<value> <string>object_jio_button</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
<string>action_type/object_jio_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
<value> <string>object_jio_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -16,13 +16,13 @@
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
<string>action_type/object_jio_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
<value> <string>object_jio_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
......
......@@ -295,7 +295,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>context/AccountingTransactionModule_getLedgerItemList</string> </value>
<value> <string>python: context.AccountingTransactionModule_getLedgerItemList()</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -280,7 +280,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>context/AccountingTransactionModule_getTranslatedPortalTypeItemList</string> </value>
<value> <string>python: context.AccountingTransactionModule_getTranslatedPortalTypeItemList()</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -175,7 +175,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: list(context.getPortalItemTypeList()) + (\'Payment Transaction Group\', )</string> </value>
<value> <string>python: list(context.getPortalItemTypeList()) + [\'Payment Transaction Group\', ]</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -4,9 +4,13 @@ portal = context.getPortalObject()
countMessage = portal.portal_activities.countMessage
invoice_type_list = portal.getPortalInvoiceTypeList()
portal.portal_selections.updateSelectionCheckedUidList(selection_name, listbox_uid, uids)
selection_uid_list = portal.portal_selections.getSelectionCheckedUidsFor(
if selection_name:
portal.portal_selections.updateSelectionCheckedUidList(selection_name, listbox_uid, uids)
selection_uid_list = portal.portal_selections.getSelectionCheckedUidsFor(
selection_name)
else:
selection_uid_list = uids
if selection_uid_list:
object_list = [brain.getObject() for brain in portal.portal_catalog(uid=selection_uid_list)]
else:
......@@ -20,10 +24,9 @@ portal.portal_selections.setSelectionParamsFor('accounting_create_related_paymen
# XXX prevent to call this on the whole module:
if len(object_list) >= 1000:
return context.Base_redirect(
form_id,
keep_items={'portal_status_message': translateString(
'Refusing to process more than 1000 objects, check your selection.')})
return context.Base_renderMessage(
translateString('Refusing to process more than 1000 objects, check your selection.'),
'warning')
tag = 'payment_creation_%s' % random.randint(0, 1000)
activated = 0
......@@ -51,6 +54,7 @@ if not activated:
'No invoice in your selection.')})
# activate something on the folder
# Kato: Why?
context.activate(after_tag=tag).getTitle()
return context.Base_redirect(
......
from Products.ERP5Type.Message import translateString
from zExceptions import Redirect
portal = context.getPortalObject()
countMessage = portal.portal_activities.countMessage
portal.portal_selections.updateSelectionCheckedUidList(selection_name, listbox_uid, uids)
selection_uid_list = portal.portal_selections.getSelectionCheckedUidsFor(
if selection_name:
portal.portal_selections.updateSelectionCheckedUidList(selection_name, listbox_uid, uids)
selection_uid_list = portal.portal_selections.getSelectionCheckedUidsFor(
selection_name)
else:
selection_uid_list = uids
if selection_uid_list:
object_list = [brain.getObject() for brain in portal.portal_catalog(uid=selection_uid_list)]
else:
......@@ -24,9 +28,12 @@ for obj in object_list:
obj = obj.getObject()
if countMessage(path=obj.getPath(),
method_id='AccountingTransaction_createReversalTransaction'):
raise Redirect, "%s/view?portal_status_message=%s" % (
context.absolute_url(), translateString(
'Reversal creation already in progress, abandon.'))
return context.Base_redirect(form_id,
abort_transaction=True,
keep_items={
"portal_status_message": translateString('Reversal creation already in progress, abandon.'),
"portal_status_level": 'error'
})
obj.activate(tag=tag).AccountingTransaction_createReversalTransaction(
cancellation_amount=cancellation_amount,
date=date,
......@@ -34,11 +41,11 @@ for obj in object_list:
activated += 1
if not activated:
return context.Base_redirect(form_id,
keep_items=dict(portal_status_message=
translateString('No valid transaction in your selection.')))
return context.Base_renderMessage(
translateString('No valid transaction in your selection.'), 'error')
# activate something on the folder
# Kato: ehm ... why?
context.activate(after_tag=tag).getTitle()
return context.Base_redirect(form_id,
......
......@@ -114,6 +114,7 @@ params['select_dict'] = select_dict
if not params.get('accounting_transaction.section_uid'):
params.setdefault('group_by', ('uid',))
# Kato: Be explicit! You have no idea what is in the **params!
if stat:
return context.countFolder(**params)
return context.searchFolder(**params)
......@@ -8,11 +8,13 @@ psm = Base_translateString('Nothing matches.')
request = container.REQUEST
# update selected uids
portal.portal_selections.updateSelectionCheckedUidList(
if list_selection_name:
portal.portal_selections.updateSelectionCheckedUidList(
list_selection_name, uids=uids, listbox_uid=listbox_uid, REQUEST=request)
uids = portal.portal_selections.getSelectionCheckedUidsFor(list_selection_name)
uids = portal.portal_selections.getSelectionCheckedUidsFor(list_selection_name)
# XXX when should it be validated ?
# Kato: It is already validated because this is Form Dialog script
if node == '':
node = context.REQUEST.get('field_your_node', node)
if mirror_section == '':
......@@ -22,9 +24,9 @@ if grouping == '':
grouping = request.get('your_grouping',
request.get('field_your_grouping',
grouping))
# edit selection for dialog parameters
portal.portal_selections.setSelectionParamsFor(
if list_selection_name:
# edit selection for dialog parameters
portal.portal_selections.setSelectionParamsFor(
'grouping_reference_fast_input_selection',
params=dict(node=node,
grouping=grouping,
......@@ -42,8 +44,8 @@ if uids:
request.set('total_selected_amount', total_selected_amount)
if update:
request.set('portal_status_message', Base_translateString('Updated'))
return context.AccountingTransactionModule_viewGroupingFastInputDialog(request)
return context.Base_renderForm('AccountingTransactionModule_viewGroupingFastInputDialog',
keep_items={'portal_status_message': Base_translateString('Updated')})
# otherwise, try to group...
......@@ -53,7 +55,7 @@ if grouping == 'grouping':
if grouped_line_list:
psm = Base_translateString('${grouped_line_count} lines grouped.',
mapping=dict(grouped_line_count=len(grouped_line_list)))
if list_selection_name:
# make sure nothing will be checked next time
portal.portal_selections.setSelectionCheckedUidsFor(list_selection_name, [])
......@@ -119,8 +121,9 @@ else:
psm = Base_translateString('${ungrouped_line_count} lines ungrouped.',
mapping=dict(ungrouped_line_count=len(ungrouped_line_list)))
if list_selection_name:
# make sure nothing will be checked next time
portal.portal_selections.setSelectionCheckedUidsFor(list_selection_name, [])
request.set('portal_status_message', psm)
return context.AccountingTransactionModule_viewGroupingFastInputDialog(request)
return context.Base_renderForm('AccountingTransactionModule_viewGroupingFastInputDialog',
keep_items={'portal_status_message': psm})
......@@ -223,7 +223,7 @@
</item>
<item>
<key> <string>first_item</string> </key>
<value> <int>0</int> </value>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>hidden</string> </key>
......
......@@ -4,8 +4,9 @@
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><th rowspan="1" colspan="4">
Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5/tests/testAccountingReports.py#L894">testAccountingReports.py:TestAccountingReports.testAccountStatement</a>
Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5/tests/testAccountingReports.py#L894">testAccountingReports.py:TestAccountingReports.testAccountStatement</a> (expected failure)
</th></tr>
<!-- This report is not using template fields and overwrites columns dynamicaly in Selection. This report will not be tested -->
</thead>
<tbody
......@@ -43,17 +44,6 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<tr><td>click</td>
<td>//a[@data-i18n="Account Statement"]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[@data-gadget-scope="field_your_node"]/div/div[1]//select</td><td></td></tr>
<tr><td>select</td>
<td>//div[@data-gadget-scope="field_your_node"]/div/div[1]//select</td>
<td>value=account_module/receivable</td></tr>
<tr><td>waitForElementPresent</td>
<td>//select[@name="field_your_section_category"]</td><td></td></tr>
<tr><td>select</td>
<td>//select[@name="field_your_section_category"]</td>
<td>value=group/demo_group/sub1</td></tr>
<tr><td>waitForElementPresent</td>
<td>//input[@name="field_your_at_date"]</td><td></td></tr>
<tr><td>type</td>
......@@ -77,14 +67,57 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<tr><td>select</td>
<td>//div[@data-gadget-scope="field_your_simulation_state"]/div/div[1]//select</td>
<td>value=delivered</td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[@data-gadget-scope="field_your_node"]/div/div[1]//select</td><td></td></tr>
<tr><td>select</td>
<td>//div[@data-gadget-scope="field_your_node"]/div/div[1]//select</td>
<td>value=account_module/receivable</td></tr>
<tr><td colspan="3">Test update button on a valid form</td></tr>
<!-- group/client does not have any Bank account assigned so we test on empty "Bank Account" field -->
<tr><td>waitForElementPresent</td>
<td>//select[@name="field_your_section_category"]</td><td></td></tr>
<tr><td>select</td>
<td>//select[@name="field_your_section_category"]</td>
<td>value=group/client</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//button[@type="submit" and @name="action_update"]</td><td></td></tr>
<tr><td>waitForElementNotPresent</td>
<td>//select[@name="field_your_payment"]/option[2]</td><td></td></tr>
<tr><td>assertElementNotPresent</td>
<td>//select[@name="field_your_payment"]/option[2]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//select[@name="field_your_section_category"]</td><td></td></tr>
<tr><td>select</td>
<td>//select[@name="field_your_section_category"]</td>
<td>value=group/demo_group/sub1</td></tr>
<tr><td>type</td>
<td>//input[@name="field_your_portal_skin"]</td>
<td>Hal</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//button[@type="submit" and @name="action_update"]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//select[@name="field_your_payment"]/option[2]</td><td></td></tr>
<tr><td>assertElementPresent</td>
<td>//select[@name="field_your_payment"]/option[@data-i18n="Bank1"]</td><td></td></tr>
<tr><td>assertElementPresent</td>
<td>//select[@name="field_your_payment"]/option[@data-i18n="Bank2"]</td><td></td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//input[@type="submit"]</td><td></td></tr>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//input[@type="submit" and @name="action_confirm"]</td><td></td></tr>
<tr><td>store</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_field_listbox.html"]//table</td>
<td>table</td></tr>
<tr><td>waitForElementPresent</td>
<td>${table}//../nav//span[text()="6 Records"]</td><td></td></tr>
<td>${table}/../nav//span[text()="6 Records"]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[@data-gadget-scope="field_your_currency"]//p[text()="EUR"]</td><td></td></tr>
......
<html>
<head><title>Test Aged Balance Report</title></head>
<head><title>Test Aged Balance Report (expected failure)</title></head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><th rowspan="1" colspan="4">
Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5/tests/testAccountingReports.py#L4904">testAccountingReports.py:test_simple_aged_creditor_report_detailed.testOtherPartiesReport</a>
</th></tr>
<tr><td rowspan="1" colspan="3">
Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5/tests/testAccountingReports.py#L4904">test_simple_aged_creditor_report_detailed.testOtherPartiesReport</a> (expected failure)
</td></tr>
</thead>
<tbody
......@@ -32,11 +32,17 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<td>//a[@data-i18n="Export"]</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n="Export"]</td><td></td></tr>
<!-- Remove when the test is fixed -->
<tr><td>setTimeout</td><td>2000</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//a[@data-i18n="Aged Balance"]</td><td></td></tr>
<!-- Remove when the test is fixed -->
<tr><td>assertElementPresent</td>
<td>//a[@data-i18n="Aged Balance"]</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n="Aged Balance"]</td><td></td></tr>
<!-- Remove when the test is fixed -->
<tr><td>setTimeout</td><td>36000</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//select[@name="field_your_section_category"]</td><td></td></tr>
......@@ -74,6 +80,11 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<tr><td>select</td>
<td>//div[@data-gadget-scope="field_your_simulation_state"]/div/div[1]//select</td>
<td>value=delivered</td></tr>
<tr><td>type</td>
<td>//input[@name="field_your_portal_skin"]</td>
<td>Hal</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//input[@type="submit"]</td><td></td></tr>
......@@ -81,6 +92,7 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<tr><td>store</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_field_listbox.html"]//table</td>
<td>table</td></tr>
<tr><td>waitForElementPresent</td>
<td>${table}/../nav//span[text()="1 Records"]</td><td></td></tr>
......
......@@ -47,7 +47,7 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<tr><td>waitForElementPresent</td><td>//input[@name="field_your_from_date"]</td><td></td></tr>
<tr><td>type</td>
<td>//input[@name="field_your_from_date"]</td>
<td>${year}-1-1</td></tr>
<td>${year}-01-01</td></tr>
<tr><td>waitForElementPresent</td><td>//input[@name="field_your_at_date"]</td><td></td></tr>
<tr><td>type</td>
<td>//input[@name="field_your_at_date"]</td>
......@@ -75,6 +75,15 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<td>//div[@data-gadget-scope="field_your_simulation_state"]/div/div[1]//select</td>
<td>value=delivered</td></tr>
<tr><th colspan="3">Override fields set by Action URL parameters to out values</th></tr>
<!-- No Titles are displayed if "export" is checked. Should we remove the check for title (<h3>) instead? -->
<tr><td>click</td>
<td>//input[@name="field_your_export"]</td><td></td></tr>
<!-- Force rendering inplace by replacinf ODS style with Hal -->
<tr><td>type</td>
<td>//input[@name="field_your_portal_skin"]</td>
<td>Hal</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//input[@type="submit"]</td><td></td></tr>
......
......@@ -69,6 +69,11 @@ Copy of <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5/tes
<tr><td>select</td>
<td>//div[@data-gadget-scope="field_your_simulation_state"]/div/div[1]//select</td>
<td>value=delivered</td></tr>
<tr><td>type</td>
<td>//input[@name="field_your_portal_skin"]</td>
<td>Hal</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//input[@type="submit"]</td><td></td></tr>
......
......@@ -71,6 +71,11 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<tr><td>select</td>
<td>//div[@data-gadget-scope="field_your_simulation_state"]/div/div[1]//select</td>
<td>value=delivered</td></tr>
<tr><td>type</td>
<td>//input[@name="field_your_portal_skin"]</td>
<td>Hal</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//input[@type="submit"]</td><td></td></tr>
......
......@@ -71,6 +71,10 @@ Copy of unittest <a href="https://lab.nexedi.com/nexedi/erp5/blob/master/product
<td>${multi_select}/div[3]//select</td>
<td></td></tr>
<tr><td>type</td>
<td>//input[@name="field_your_portal_skin"]</td>
<td>Hal</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-url="${renderjs_url}/gadget_erp5_page_form.html"]//input[@type="submit"]</td><td></td></tr>
......
erp5_full_text_mroonga_catalog
erp5_dummy_movement
erp5_ods_style
\ No newline at end of file
......@@ -142,7 +142,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>context/Event_getPreferredResponseEventSource</string> </value>
<value> <string>python:context.Event_getPreferredResponseEventSource()</string> </value>
</item>
</dictionary>
</pickle>
......@@ -155,7 +155,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>context/Event_getPreferredResponseEventSourceItemList</string> </value>
<value> <string>python:context.Event_getPreferredResponseEventSourceItemList()</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -138,7 +138,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>context/Event_getPreferredResponseEventPortalType</string> </value>
<value> <string>python:context.Event_getPreferredResponseEventPortalType()</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -129,7 +129,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>here/getResource</string> </value>
<value> <string>python:context.getResource()</string> </value>
</item>
</dictionary>
</pickle>
......@@ -142,7 +142,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>here/Event_getResponseResourceItemList</string> </value>
<value> <string>python:context.Event_getResponseResourceItemList()</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -39,7 +39,7 @@
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/update_dialog" />
<tal:block tal:define="notification_configuration python: {'class': 'success',
'text': 'Data received'}">
'text': 'Data received.'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
......
......@@ -39,7 +39,7 @@
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/update_dialog" />
<tal:block tal:define="notification_configuration python: {'class': 'success',
'text': 'Data received'}">
'text': 'Data received.'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
......
......@@ -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>
......@@ -16,6 +16,7 @@ return context.ERP5Document_getHateoas(
select_list=select_list,
limit=limit,
form=form,
form_data=form_data,
relative_url=relative_url,
list_method=list_method,
default_param_json=default_param_json,
......@@ -24,6 +25,8 @@ return context.ERP5Document_getHateoas(
sort_on=sort_on,
local_roles=local_roles,
selection_domain=selection_domain,
restricted=1,
extra_param_json=extra_param_json,
restricted=1
portal_status_message=portal_status_message,
portal_status_level=portal_status_level
)
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None, response=None, view=None, mode=\'root\', query=None, select_list=None, limit=10, local_roles=None, form=None, relative_url=None, list_method=None, default_param_json=None, form_relative_url=None, bulk_list="[]", sort_on=None, selection_domain=None, extra_param_json=None</string> </value>
<value> <string>REQUEST=None, response=None, view=None, mode=\'root\', query=None, select_list=None, limit=10, local_roles=None, form=None, form_data=None, relative_url=None, list_method=None, default_param_json=None, form_relative_url=None, bulk_list="[]", sort_on=None, selection_domain=None, extra_param_json=None, portal_status_message=\'\', portal_status_level=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
"""
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.
There are runtime values hidden in every form (injected by getHateoas Script):
form_id - previous form ID (backward compatibility reasons)
dialog_id - current form dialog ID
dialog_method - method to be called - can be either update_method or dialog_method of the Dialog Form
"""
from Products.ERP5Type.Log import log, DEBUG, INFO, WARNING
from Products.ERP5Type.Log import log, DEBUG, INFO, WARNING, ERROR
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.
......@@ -27,30 +32,44 @@ request = kw.get('REQUEST', None) or container.REQUEST
request_form = request.form
error_message = ''
translate = context.Base_translateString
portal = context.getPortalObject()
# 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(context))['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'],
return context.Base_configureUI(form_id=form_id,
selection_name=kw['selection_name'],
field_columns=kw['field_columns'],
stat_columns=kw['stat_columns'])
# Exceptions for Sort
if dialog_method == 'Base_configureSortOn':
return context.Base_configureSortOn(form_id=kw['form_id'],
return context.Base_configureSortOn(form_id=form_id,
selection_name=kw['selection_name'],
field_sort_on=kw['field_sort_on'],
field_sort_order=kw['field_sort_order'])
# Exceptions for Workflow
if dialog_method == 'Workflow_statusModify':
return context.Workflow_statusModify(form_id=kw['form_id'],
return context.Workflow_statusModify(form_id=form_id,
dialog_id=dialog_id)
# Exception for edit relation
if dialog_method == 'Base_editRelation':
return context.Base_editRelation(form_id=kw['form_id'],
return context.Base_editRelation(form_id=form_id,
field_id=kw['field_id'],
selection_name=kw['list_selection_name'],
selection_index=kw['selection_index'],
......@@ -60,7 +79,7 @@ if dialog_method == 'Base_editRelation':
# Exception for create relation
# Not used in new UI - relation field implemented using JIO calls from JS
if dialog_method == 'Base_createRelation':
return context.Base_createRelation(form_id=kw['form_id'],
return context.Base_createRelation(form_id=form_id,
selection_name=kw['list_selection_name'],
selection_index=kw['selection_index'],
base_category=kw['base_category'],
......@@ -70,13 +89,15 @@ 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=form_id,
# selection_name=kw['selection_name'],
# md5_object_uid_list=kw['md5_object_uid_list'])
form = getattr(context, dialog_id)
form_data = None
extra_param = json.loads(extra_param_json or "{}")
# form can be a python script that returns the form
if not hasattr(form, 'validate_all_to_request'):
......@@ -88,10 +109,9 @@ try:
# data. Otherwise, field appears as non editable.
editable_mode = request.get('editable_mode', 1)
request.set('editable_mode', 1)
form.validate_all_to_request(request)
form_data = form.validate_all_to_request(request)
request.set('editable_mode', editable_mode)
default_skin = context.getPortalObject().portal_skins.getDefaultSkin()
default_skin = portal.portal_skins.getDefaultSkin()
allowed_styles = ("ODT", "ODS", "Hal", "HalRestricted")
if getattr(getattr(context, dialog_method), 'pt', None) == "report_view" and \
request.get('your_portal_skin', default_skin) not in allowed_styles:
......@@ -99,10 +119,11 @@ try:
# Form is OK, it's just this field - style so we return back form-wide error
# for which we don't have support out-of-the-box thus we manually craft it
# XXX TODO: Form-wide validation errors
return context.Base_renderMessage(
translate('Only ODT, ODS, Hal and HalRestricted skins are allowed for reports '\
return context.Base_renderForm(dialog_id,
message=translate('Only ODT, ODS, Hal and HalRestricted skins are allowed for reports '\
'in Preferences - User Interface - Report Style'),
level=WARNING)
level=WARNING,
form_data=form_data)
except FormValidationError as validation_errors:
# Pack errors into the request
......@@ -161,38 +182,47 @@ 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 = extra_param.get("query", None)
select_all = int(extra_param.pop("_select_all", "0"))
if query != "" or (query == "" and select_all > 0):
listbox = getattr(context, form_id).Form_getListbox()
if listbox is not None:
kw['uids'] = [int(getattr(document, "uid"))
for document in context.Base_searchUsingListbox(listbox, query)]
else:
log('Action {} should not specify `uids` as its parameters when it does not take object list from the previous view!'.format(dialog_method), level=ERROR)
elif query == "" and select_all == 0 and dialog_method != update_method: # do not interrupt on UPDATE
return context.Base_renderForm(
dialog_id,
message=translate("All documents are selected! Submit again to proceed or Cancel and narrow down your search."),
level=WARNING,
keep_items={'_select_all': 1},
query=query,
form_data=form_data)
# 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, [], ()))
portal.portal_selections.setSelectionParamsFor(kw['selection_name'], kw)
# Notify the underlying script whether user did modifications
form_hash = form.hash_validated_data(form_data)
kw['has_changed'] = (form_hash != extra_param.pop('form_hash', ''))
# Add rest of extra param into arguments of the target method
kw.update(extra_param)
# Finally we will call the Dialog Method
# Handle deferred style, unless we are executing the update action
if dialog_method != update_method and clean_kw.get('deferred_style', 0):
clean_kw['deferred_portal_skin'] = clean_kw.get('portal_skin', None)
if dialog_method != update_method and kw.get('deferred_style', 0):
kw['deferred_portal_skin'] = kw.get('portal_skin', None)
# XXX Hardcoded Deferred style name
clean_kw['portal_skin'] = 'Deferred'
kw['portal_skin'] = 'Deferred'
page_template = getattr(getattr(context, dialog_method), 'pt', None)
......@@ -200,21 +230,19 @@ if dialog_method != update_method and clean_kw.get('deferred_style', 0):
# Limit Reports in Deferred style to known working styles
if request_form.get('your_portal_skin', None) not in ("ODT", "ODS"):
# RJS own validation - deferred option works here only with ODS/ODT skins
return context.Base_renderMessage(
translate('Deferred reports are possible only with preference '\
return context.Base_renderForm(dialog_id,
message=translate('Deferred reports are possible only with preference '\
'"Report Style" set to "ODT" or "ODS"'),
level=WARNING)
# If the action form has report_view as it's method, it
if page_template != 'report_view':
# use simple wrapper
clean_kw['deferred_style_dialog_method'] = dialog_method
kw['deferred_style_dialog_method'] = dialog_method
kw['deferred_style_dialog_method'] = dialog_method
request.set('deferred_style_dialog_method', dialog_method)
dialog_method = 'Base_activateSimpleView'
url_params_string = make_query(clean_kw)
# Never redirect in JSON style - do as much as possible here.
# At this point the 'dialog_method' should point to a form (if we are in report)
# if we are not in Deferred mode - then it points to `Base_activateSimpleView`
......@@ -223,11 +251,11 @@ if True:
if dialog_method != update_method:
# When we are not executing the update action, we have to change the skin
# manually,
if 'portal_skin' in clean_kw:
new_skin_name = clean_kw['portal_skin']
context.getPortalObject().portal_skins.changeSkin(new_skin_name)
if 'portal_skin' in kw:
new_skin_name = kw['portal_skin']
portal.portal_skins.changeSkin(new_skin_name)
request.set('portal_skin', new_skin_name)
deferred_portal_skin = clean_kw.get('deferred_portal_skin')
deferred_portal_skin = kw.get('deferred_portal_skin')
if deferred_portal_skin:
# has to be either ODS or ODT because only those contain `form_list`
request.set('deferred_portal_skin', deferred_portal_skin)
......@@ -247,16 +275,20 @@ if True:
# with the content of REQUEST.URL
request.set('URL', '%s/%s' % (context.absolute_url(), dialog_method))
# Only in case we are not updating but executing - then proceed with direct
# execution based on Skin selection
if dialog_method != update_method:
# RJS: If we are in deferred mode - call the form directly and return
# dialog method is now `Base_activateSimpleView` - the only script in
# deferred portal_skins folder
if clean_kw.get('deferred_style', 0):
if kw.get('deferred_style', 0):
return dialog_form(**kw) # deferred form should return redirect with a message
# RJS: If skin selection is different than Hal* then ERP5Document_getHateoas
# does not exist and we call form method directly
# If update_method was clicked and the target is the original dialog form then we must not call dialog_form directly because it returns HTML
if clean_kw.get("portal_skin", context.getPortalObject().portal_skins.getDefaultSkin()) not in ("Hal", "HalRestricted", "View"):
# If update_method was clicked and the target is the original dialog for
# then we must not call dialog_form directly because it returns HTML
if kw.get("portal_skin", portal.portal_skins.getDefaultSkin()) not in ("Hal", "HalRestricted", "View"):
return dialog_form(**kw)
# dialog_form can be anything from a pure python function, class method to ERP5 Form or Python Script
......@@ -266,8 +298,7 @@ if True:
meta_type = ""
if meta_type in ("ERP5 Form", "ERP5 Report"):
return context.ERP5Document_getHateoas(REQUEST=request, form=dialog_form, mode="form")
return context.ERP5Document_getHateoas(REQUEST=request, form=dialog_form, mode="form", form_data=form_data)
return dialog_form(**kw)
return getattr(context, dialog_method)(**kw)
......@@ -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, extra_param_json="{}", **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
......@@ -19,7 +19,7 @@ if context.isDeletable(check_relation=True):
else:
parent.manage_delObjects(ids= [context.getId()])
# redirect back to the container since the context was deleted
return container.Base_redirect(
return parent.Base_redirect(
keep_items={
"portal_status_message": translate("Document deleted")
})
......
"""
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
......
"""Render form while keeping its values back to user.
This script differs from Base_redirect that it keeps the form values in place.
:param message: {str} message to be displayed at the user
:param level: {str|int} severity of the message using ERP5Type.Log levels or their names like 'info', 'warn', 'error'
:param keep_items: {dict} items to be available in the next call. They will be either added as hidden fields to the
rendered form or in case of "portal_status_message" just displayed to the user
:param REQUEST: request
:param **kwargs: should contain parameters to ERP5Document_getHateoas such as 'query' to replace Selections
"""
keep_items = keep_items or {}
form = getattr(context, form_id)
return context.ERP5Document_getHateoas(form=form, mode='form')
if not message and "portal_status_message" in keep_items:
message = keep_items.pop("portal_status_message")
if not level and "portal_status_level" in keep_items:
level = keep_items.pop("portal_status_level")
return context.ERP5Document_getHateoas(form=form, mode='form', REQUEST=REQUEST, extra_param_json=keep_items,
portal_status_message=message, portal_status_level=level, **kwargs)
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>form_id</string> </value>
<value> <string>form_id, message=\'\', level=None, keep_items=None, REQUEST=None, **kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
"""Hello. This will be long because this godly script does almost everything.
In general, it always returns a JSON response in HATEOAS format specification.
It **always** return a JSON reponse in HATEOAS format specification.
:param REQUEST: HttpRequest holding GET and/or POST data
:param response:
:param view: either "view" or absolute URL of an ERP5 Action
:param mode: {str} help to decide what user wants from us "form" | "search" ...
:param relative_url: an URL of `traversed_document` to operate on (it must have an object_view)
:param portal_status_message: {str} message to be displayed on the user
:param portal_status_level: {str|int} severity of the message using ERP5Type.Log levels or their names like 'info', 'warn', 'error'
Only in mode == 'search'
Parameters for mode == 'search'
:param query: string-serialized Query
:param 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"
Only in mode == 'form'
:param form:
Only in mode == 'traverse'
:param extra_param_json: {str} BASE64 encoded JSON with parameters for getHateoas script. Content will be put to the REQUEST so
it is accessible to all Scripts and TALES expressions.
:param default_param_json: {str} BASE64 encoded JSON with parameters intended for the list_method
:param .form_id: In case of page_template = "form" it will be similar to form_relative_url with the exception that it contains
only the form name (e.g. FooModule_viewFooList). In case of dialogs it points to the previous form which is
often more important than the dialog form.
Parameters for mode == 'form'
:param form: {instace} of a form - obviously this call can be only internal (Script-to-Script)
:param form_data: {dict} cleaned (validated) form data stored in dict where the key is (prefixed) field.id. We do not use it to
obtain the value of the field because of how the validation itself work. Take a look in
Formulator/Form.validata_all_to_request where REQUEST is modified inplace and in case of first error
an exception is thrown which prevents the return thus form_data are empty in case of partial success.
:param extra_param_json: {dict} extra fields to be added to the rendered form
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 view: {str} mandatory. the view reference as defined on a Portal Type (e.g. "view" or "publish_view")
:param query: {str} optional, is a remaining from the search on a previous view. Query is used to replace selections.
It provides complete information together with listbox configuration so we are able to pass a list of UIDs
to methods which require it. This allows dialogs to show selection from previous view.
:param extra_param_json: {str} BASE64 encoded JSON with parameters for getHateoas script. Content will be put to the REQUEST so
it is accessible to all Scripts and TALES expressions. If view contains embedded **dialog** form then
fields will be added to that form to preserve the values for the next step.
# Form
When handling form, we can expect field values to be stored in REQUEST.form in two forms
......@@ -41,10 +61,10 @@ import time
from email.Utils import formatdate
import re
from zExceptions import Unauthorized
from Products.ERP5Type.Utils import UpperCase
from Products.ERP5Type.Log import log, DEBUG, INFO, WARNING, ERROR
from Products.ERP5Type.Message import Message
from Products.ERP5Type.Utils import UpperCase
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
from Products.ERP5Type.Log import log
from collections import OrderedDict
from urlparse import urlparse
......@@ -56,11 +76,13 @@ if REQUEST is None:
if response is None:
response = REQUEST.RESPONSE
def isFieldType(field, type_name):
if field.meta_type == 'ProxyField':
field = field.getRecursiveTemplateField()
return field.meta_type == type_name
def toBasicTypes(obj):
"""Ensure that obj contains only basic types."""
if obj is None:
......@@ -79,7 +101,8 @@ def toBasicTypes(obj):
log('Cannot convert {!s} to basic types {!s}'.format(type(obj), obj), level=100)
return obj
def addHiddenFieldToForm(form, name, value):
def renderHiddenField(form, name, value):
if form == {}:
form['_embedded'] = {}
form['_embedded']['_view'] = {}
......@@ -90,7 +113,7 @@ def addHiddenFieldToForm(form, name, value):
field_dict = form
field_dict[name] = {
"type": "StringField",
"type": "StringField", # must be string field because only this gets send when non-editable
"key": name,
"default": value,
"editable": 0,
......@@ -101,6 +124,7 @@ def addHiddenFieldToForm(form, name, value):
"required": 1,
}
# http://stackoverflow.com/a/13105359
def byteify(string):
if isinstance(string, dict):
......@@ -223,15 +247,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 +286,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
......@@ -411,21 +464,31 @@ url_template_dict = {
"form_action": "%(traversed_document_url)s/%(action_id)s",
"traverse_generator": "%(root_url)s/%(script_id)s?mode=traverse" + \
"&relative_url=%(relative_url)s&view=%(view)s",
"traverse_generator_non_view": "%(root_url)s/%(script_id)s?mode=traverse" + \
"&relative_url=%(relative_url)s&view=%(view)s&form_id=%(form_id)s",
"traverse_generator_with_parameter": "%(root_url)s/%(script_id)s?mode=traverse" + \
"traverse_generator_action": "%(root_url)s/%(script_id)s?mode=traverse" + \
"&relative_url=%(relative_url)s&view=%(view)s&extra_param_json=%(extra_param_json)s",
"traverse_generator_action_module": "%(root_url)s/%(script_id)s?mode=traverse" + \
"&relative_url=%(relative_url)s&view=%(view)s&extra_param_json=%(extra_param_json)s{&query}",
"traverse_template": "%(root_url)s/%(script_id)s?mode=traverse" + \
"{&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).
# :param extra_param_json: contains mainly previous form id to replicate previous search (it is a replacement for Selections)
"custom_search_template": "%(root_url)s/%(script_id)s?mode=search" + \
"&relative_url=%(relative_url)s" \
"&form_relative_url=%(form_relative_url)s" \
"&list_method=%(list_method)s" \
"&extra_param_json=%(extra_param_json)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" \
......@@ -447,10 +510,45 @@ 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))
def parseActionUrl(url):
"""Parse usual ERP5 Action URL into components: ~root, context~, view_id, param_dict, url.
:param url: {str} is expected to be in form https://<site_root>/context/view_id?optional=params
"""
param_dict = {}
url_and_params = url.split(site_root.absolute_url())[-1].split('?')
_, script = url_and_params[0].strip("/ ").rsplit('/', 1)
if len(url_and_params) > 1:
for param in url_and_params[1].split('&'):
param_name, param_value = param.split('=')
if "+" in param_value:
param_value = param_value.replace("+", " ")
if ":" in param_name:
param_name, param_type = param_name.split(":")
if param_type == "int":
param_value = int(param_value)
elif param_type == "bool":
param_value = True if param_value.lower() in ("true", "1") else False
else:
raise ValueError("Cannot convert param {}={} to type {}. Feel free to add implemetation at the position of this exception.".format(
param_name, param_value, param_type))
param_dict[param_name] = param_value
return {
'view_id': script,
'params': param_dict,
'url': url
}
def getFormRelativeUrl(form):
return portal.portal_catalog(
portal_type=("ERP5 Form", "ERP5 Report"),
......@@ -464,8 +562,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.get(field.id, REQUEST.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:
......@@ -741,7 +841,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# portal_type list can be overriden by selection too
# since it can be intentionally empty we don't override with non-empty field value
portal_type_list = selection_params.get("portal_type", field.get_value('portal_types'))
# requirement: get only sortable/searchable columns which are already displayed in listbox
# see https://lab.nexedi.com/nexedi/erp5/blob/HEAD/product/ERP5Form/ListBox.py#L1004
# implemented in javascript in the end
......@@ -785,17 +884,9 @@ 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,
......@@ -803,7 +894,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id),
"list_method": list_method_name,
"default_param_json": urlsafe_b64encode(
json.dumps(ensureSerializable(list_method_query_dict)))
json.dumps(ensureSerializable(list_method_query_dict))),
# in case of a dialog the form_id points to previous form, otherwise current form
"extra_param_json": urlsafe_b64encode(
json.dumps(ensureSerializable({"form_id": REQUEST.get('form_id', form.id)})))
}
# once we imprint `default_params` into query string of 'list method' we
# don't want them to propagate to the query as well
......@@ -875,8 +969,20 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
return result
if meta_type == "FormBox":
document_type_name = traversed_document.getPortalType()
document_type = getattr(portal.portal_types, document_type_name, None)
# Kato: link to type is mandatory for extracting title of the form...
embedded_document = {
'_links': {},
'_links': {
'type': {
"name": Base_translateString(traversed_document.getPortalType()),
"href": default_document_uri_template % {
"root_url": site_root.absolute_url(),
"relative_url": document_type.getRelativeUrl(),
"script_id": script.id
}
}
},
'_actions': {},
}
......@@ -884,12 +990,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
formbox_context = traversed_document
if field.get_value('context_method_id'):
# harness acquisition and call the method right away
formbox_context = getattr(traversed_document, field.get_value('context_method_id'))()
formbox_context = getattr(traversed_document, field.get_value('context_method_id'))(
field=field, REQUEST=REQUEST)
embedded_document['_debug'] = "Different context"
embeded_form = getattr(formbox_context, field.get_value('formbox_target_id'))
# get embedded form definition
embedded_form = getattr(formbox_context, field.get_value('formbox_target_id'))
# renderForm mutates `embedded_document` therefor no return/assignment
renderForm(formbox_context, embeded_form, embedded_document, key_prefix=key)
renderForm(formbox_context, embedded_form, embedded_document, key_prefix=key)
# fix editability which is hard-coded to 0 in `renderForm` implementation
embedded_document['form_id']['editable'] = field.get_value("editable")
......@@ -923,7 +1030,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
"""
Render a `form` in plain python dict.
This function sets varibles 'here' and 'form_id' resp. 'dialog_id' for forms resp. form dialogs to REQUEST.
This function sets variables 'here' and 'form_id' resp. 'dialog_id' for forms resp. form dialogs to REQUEST.
Any other REQUEST mingling are at the responsability of the callee.
:param selection_params: holds parameters to construct ERP5Form.Selection instance
......@@ -934,9 +1041,12 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
previous_request_other = {}
REQUEST.set('here', traversed_document)
if extra_param_json is None:
extra_param_json = {}
# 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)
......@@ -946,7 +1056,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"
......@@ -968,6 +1077,27 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
"method": form.method,
}
}
if form.pt == "form_dialog":
# If there is a "form_id" in the REQUEST then it means that last view was actually a form
# and we are most likely in a dialog. We save previous form into `last_form_id` ...
last_form_id = extra_param_json.pop("form_id", "") or REQUEST.get("form_id", "")
last_listbox = None
# ... so we can do some magic with it (especially embedded listbox if exists)!
try:
if last_form_id:
last_form = getattr(context, last_form_id)
last_listbox = last_form.Form_getListbox()
# In order not to use Selections we need to pass all search attributes to *a listbox inside the form dialog*
# in case there was a listbox in the previous form. No other case!
if last_listbox:
# If a Lisbox's list_method takes `uid` as input parameter then it will be ready in the request. But the actual
# computation is too expensive so we make it lazy (and evaluate any callable at `selectKwargsForCallable`)
# UID will be used in (Listbox's|RelationField's) list_method as a constraint if defined in parameters
REQUEST.set("uid", lazyUidList(traversed_document, last_listbox, query))
except AttributeError:
pass
# Form traversed_document
response_dict['_links']['traversed_document'] = {
"href": default_document_uri_template % {
......@@ -1009,21 +1139,36 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# Do not crash if field configuration is wrong.
pass
# Form Edit handler uses form_id to recover the submitted form.
# Form Dialog handler uses 'dialog_id' instead and 'form_id'
# - Some dialog actions (e.g. Print) uses form_id to obtain previous view form
if (form.pt == 'form_dialog'):
addHiddenFieldToForm(response_dict, 'dialog_id', form.id)
# Form Edit handler uses form_id to recover the submitted form and to control its
# properties like editability
if form.pt == 'form_dialog':
# overwrite "form_id" field's value because old UI does that by passing
# the form_id in query string and hidden fields
if REQUEST.get('form_id', None):
addHiddenFieldToForm(response_dict, "form_id", REQUEST.get('form_id'))
# some dialog actions (Print Module) use previous selection name
if REQUEST.get('selection_name', None):
addHiddenFieldToForm(response_dict, "selection_name", REQUEST.get('selection_name'))
renderHiddenField(response_dict, "form_id", REQUEST.get('form_id') or form.id)
else:
# In form_view we place only form_id in the request form
addHiddenFieldToForm(response_dict, 'form_id', form.id)
renderHiddenField(response_dict, 'form_id', form.id)
# Form Dialog handler uses 'dialog_id' instead and 'form_id'
# - Some dialog actions (e.g. Print) uses form_id to obtain previous view form
if form.pt == 'form_dialog':
# dialog_id is a mandatory field in any form_dialog
renderHiddenField(response_dict, 'dialog_id', form.id)
# some dialog actions use custom cancel_url
if REQUEST.get('cancel_url', None):
renderHiddenField(response_dict, "cancel_url", REQUEST.get('cancel_url'))
# Let's support Selections!
# There are two things needed
# - `uid` parameter of list_method of listbox (if present) in this dialog
# - `uids` or `brain` to a Dialog Form Action Method
#
# We can serialize `uids` into a hidden form field but we cannot do it with brain so we
# simply put "query" in a hidden field and construct `brain` from it in Base_callDialogMethod
dialog_method_kwargs = selectKwargsForCallable(getattr(traversed_document, form.action), {}, {'brain': None, 'uids': None})
if 'uids' in dialog_method_kwargs:
# If we do not have "query" in the REQUEST but the Dialog Method requires uids
# then we still should inject empty "query" in the dialog call
extra_param_json["query"] = query or REQUEST.get("query", "")
if (form.pt == 'report_view'):
# reports are expected to return list of ReportSection which is a wrapper
......@@ -1114,52 +1259,28 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
response_dict['report_section_list'] = report_result_list
# end-if report_section
if form.pt == "form_dialog":
# Insert hash of current values into the form so scripts can see whether data has
# changed if they provide multi-step process
if form_data:
extra_param_json["form_hash"] = form.hash_validated_data(form_data)
# extra_param_json is a special field in forms (just like form_id). extra_param_json field holds JSON
# metadata about the form (its hash and dynamic fields)
renderHiddenField(response_dict, 'extra_param_json', json.dumps(extra_param_json))
for key, value in previous_request_other.items():
if value is not None:
REQUEST.set(key, value)
# XXX form action update, etc
def renderRawField(field):
meta_type = field.meta_type
return {
"meta_type": field.meta_type
}
if meta_type == "MethodField":
result = {
"meta_type": field.meta_type
}
else:
result = {
"meta_type": field.meta_type,
"_values": field.values,
# XXX TALES expression is not JSON serializable by default
# "_tales": field.tales
"_overrides": field.overrides
}
if meta_type == "ProxyField":
result['_delegated_list'] = field.delegated_list
# try:
# result['_delegated_list'].pop('list_method')
# except KeyError:
# pass
# XXX ListMethod is not JSON serialized by default
try:
result['_values'].pop('list_method')
except KeyError:
pass
try:
result['_overrides'].pop('list_method')
except KeyError:
pass
return result
def renderFormDefinition(form, response_dict):
"""Form "definition" is configurable in Zope admin: Form -> Order."""
"""Form "definition" is configurable in Zope admin: Form -> Order.
It would be nice to actually receive description of the document for which
the form definition is rendered. Mainly because there are some dynamical
attributes (such as `fixit` for Form Dialog Methods) that are optional.
"""
group_list = []
for group in form.Form_getGroupTitleAndId():
......@@ -1167,48 +1288,52 @@ def renderFormDefinition(form, response_dict):
field_list = []
for field in form.get_fields_in_group(group['goid'], include_disabled=1):
field_list.append((field.id, renderRawField(field)))
field_list.append((field.id, {'meta_type': field.meta_type}))
group_list.append((group['gid'], field_list))
# some forms might not have any fields so we put empty bottom group
if not group_list:
group_list = [('bottom', [])]
# each form has hidden attribute `form_id`
group_list[-1][1].append(('form_id', {'meta_type': 'StringField'}))
if form.pt == "form_dialog":
# every form dialog has its dialog_id and meta (control) attributes in extra_param_json
group_list[-1][1].extend([
('dialog_id', {'meta_type': 'StringField'}),
('extra_param_json', {'meta_type': 'TextAreaField'})
])
response_dict["group_list"] = group_list
response_dict["title"] = Base_translateString(form.getTitle())
response_dict["pt"] = form.pt
response_dict["action"] = form.action
response_dict["update_action"] = form.update_action
mime_type = 'application/hal+json'
portal = context.getPortalObject()
sql_catalog = portal.portal_catalog.getSQLCatalog()
# Calculate the site root to prevent unexpected browsing
is_web_mode = (context.REQUEST.get('current_web_section', None) is not None) or (hasattr(context, 'isWebMode') and context.isWebMode())
# is_web_mode = traversed_document.isWebMode()
if is_web_mode:
site_root = context.getWebSectionValue()
view_action_type = site_root.getLayoutProperty("configuration_view_action_category", default='object_view')
else:
site_root = portal
view_action_type = "object_view"
context.Base_prepareCorsResponse(RESPONSE=response)
# Check if traversed_document is the site_root
if relative_url:
temp_traversed_document = site_root.restrictedTraverse(relative_url, None)
if (temp_traversed_document is None):
response.setStatus(404)
return ""
else:
temp_traversed_document = context
def statusLevelToString(level):
"""Transform any level format to lowercase string representation"""
if isinstance(level, (str, unicode)):
if level.lower() == "error":
return "error"
elif level.lower().startswith("warn"):
return "error" # we might want to add another level for warning
else:
return "success"
if level == ERROR:
return "error"
elif level == WARNING:
return "error"
else:
return "success"
temp_is_site_root = (temp_traversed_document.getPath() == site_root.getPath())
temp_is_portal = (temp_traversed_document.getPath() == portal.getPath())
def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, REQUEST=None,
response=None, view=None, mode=None, query=None,
select_list=None, limit=None, form=None,
relative_url=None, restricted=None, list_method=None,
default_param_json=None, form_relative_url=None):
default_param_json=None, form_relative_url=None, extra_param_json=None):
if relative_url:
try:
......@@ -1217,6 +1342,12 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
is_site_root = False
except:
raise NotImplementedError(relative_url)
# extra_param_json holds parameters for search interpreted by getHateoas itself
# not by the list_method neither url_columns - only getHateoas
if extra_param_json is None:
extra_param_json = {}
result_dict = {
'_debug': mode,
'_links': {
......@@ -1246,8 +1377,17 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
"name": portal.getTitle(),
}
}
# possible other attributes
# _notification {dict} form of {'message': "", 'status': ""}
# _embedded {dict} form of {"_view": <erp5_document_properties>}
}
# Inject notification into response no matter the kind of request
if portal_status_message:
result_dict['_notification'] = {
'message': str(portal_status_message),
'status': statusLevelToString(portal_status_level)
}
if (restricted == 1) and (portal.portal_membership.isAnonymousUser()):
login_relative_url = site_root.getLayoutProperty("configuration_login", default="")
......@@ -1266,19 +1406,34 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
response.setStatus(406)
return ""
elif (mode == 'root') or (mode == 'traverse'):
#################################################
# Raw document
#################################################
##
# Render ERP Document with a `view` specified
# `view` contains view's name and we extract view's URL (we suppose form ${object_url}/Form_view)
# which after expansion gives https://<site-root>/context/view_id?optional=params
if (REQUEST is not None) and (REQUEST.other['method'] != "GET"):
response.setStatus(405)
return ""
# Default properties shared by all ERP5 Document and Site
action_dict = {}
# result_dict['_relative_url'] = traversed_document.getRelativeUrl()
current_action = {} # current action parameters (context, script, URL params)
action_dict = {} # actions available on current `traversed_document`
last_form_id = None # will point to the previous form so we can obtain previous selection
result_dict['title'] = traversed_document.getTitle()
# extra_param_json should be base64 encoded JSON at this point
# only for mode == 'form' it is already a dictionary
if not extra_param_json:
extra_param_json = {}
if isinstance(extra_param_json, str):
extra_param_json = ensureDeserialized(json.loads(urlsafe_b64decode(extra_param_json)))
for key, value in extra_param_json.items():
REQUEST.set(key, value)
# Add a link to the portal type if possible
if not is_portal:
# traversed_document should always have its Portal Type in ERP5 Portal Types
......@@ -1309,65 +1464,49 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
"name": Base_translateString(container.getTitle()),
}
# Extract embedded form in the document view
embedded_url = None
# Find current action URL and extract embedded view
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
if (view == view_action['id']):
embedded_url = '%s' % view_action['url']
# `form_id` should be actually called `dialog_id` in case of form dialogs
# 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 ""
form_id = ""
if (embedded_url is not None):
# XXX Try to fetch the form in the traversed_document of the document
# Of course, this code will completely crash in many cases (page template
# instead of form, unexpected action TALES expression). Happy debugging.
# renderer_form_relative_url = view_action['url'][len(portal.absolute_url()):]
form_id = embedded_url.split('?', 1)[0].split("/")[-1]
# renderer_form = traversed_document.restrictedTraverse(form_id, None)
# XXX Proxy field are not correctly handled in traversed_document of web site
renderer_form = getattr(traversed_document, form_id)
if (renderer_form is not None):
current_action = parseActionUrl('%s' % view_action['url']) # current action/view being rendered
# If we have current action definition we are able to render embedded view
# which should be a "ERP5 Form" but in reality can be anything
if current_action.get('view_id', ''):
view_instance = getattr(traversed_document, current_action['view_id'])
if (view_instance is not None):
embedded_dict = {
'_links': {
'self': {
'href': embedded_url
'href': current_action['url']
}
}
}
if view_instance.pt == "form_dialog":
# If there is a "form_id" in the REQUEST then it means that last view was actually a form
# and we are most likely in a dialog. We save previous form into `last_form_id` ...
last_form_id = REQUEST.get('form_id', "") if REQUEST is not None else ""
# Put all query parameters (?reset:int=1&workflow_action=start_action) in request to mimic usual form display
query_param_dict = {}
query_split = embedded_url.split('?', 1)
if len(query_split) == 2:
for query_parameter in query_split[1].split("&"):
query_key, query_value = query_parameter.split('=')
# often + is used instead of %20 so we replace for space here
query_param_dict[query_key] = query_value.replace("+", " ")
# set URL params into REQUEST (just like it was sent by form)
for query_key, query_value in query_param_dict.items():
# Request 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 current_action['params'].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:
if "Script" in view_instance.meta_type:
# we suppose that the script takes only what is given in the URL params
return renderer_form(**query_param_dict)
return view_instance(**current_action['params'])
except AttributeError:
# if renderer form does not have attr meta_type then it is not a document
# but most likely bound instance method. Some form_ids do actually point to methods.
returned_value = renderer_form(**query_param_dict)
returned_value = view_instance(**current_action['params'])
# returned value is usually REQUEST.RESPONSE.redirect()
log('ERP5Document_getHateoas', 'HAL_JSON cannot handle returned value "{!s}" from {}({!s})'.format(
returned_value, form_id, query_param_dict), 100)
returned_value, current_action['view_id'], current_action['params']), 100)
status_message = Base_translateString('Operation executed')
if isinstance(returned_value, (str, unicode)) and returned_value.startswith('http'):
parsed_url = urlparse(returned_value)
......@@ -1377,8 +1516,8 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
return traversed_document.Base_redirect(keep_items={
'portal_status_message': status_message})
renderForm(traversed_document, view_instance, embedded_dict, extra_param_json=extra_param_json)
renderForm(traversed_document, renderer_form, embedded_dict, extra_param_json=extra_param_json)
result_dict['_embedded'] = {
'_view': embedded_dict
}
......@@ -1403,18 +1542,31 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# select correct URL template based on action_type and form page template
url_template_key = "traverse_generator"
# Modify Actions on Module - they need access to current form_id and runtime
# information such as `query` in case they operate on selections!
if erp5_action_key not in ("view", "object_view", "object_jio_view"):
# previous view's form_id required almost everything but other views
url_template_key = "traverse_generator_non_view"
# 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_action"
if traversed_document.getPortalType() in portal.getPortalModuleTypeList():
url_template_key = "traverse_generator_action_module"
# but when we do not have the last form id we do not pass is of course
if not (current_action.get('view_id', '') or last_form_id):
url_template_key = "traverse_generator"
# mark templated links according to HAL specs http://stateless.co/hal_specification.html
if "{&" in url_template_dict[url_template_key]:
erp5_action_list[-1]['templated'] = True
# some dialogs need previous form_id when rendering to pass UID to embedded Listbox
extra_param_json['form_id'] = current_action['view_id'] \
if current_action.get('view_id', '') and view_instance.pt in ("form_view", "form_list") \
else last_form_id
erp5_action_list[-1]['href'] = url_template_dict[url_template_key] % {
"root_url": site_root.absolute_url(),
"script_id": script.id,
"script_id": script.id, # this script (ERP5Document_getHateoas)
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"view": erp5_action_list[-1]['name'],
"form_id": form_id if form_id and renderer_form.pt == "form_view" else last_form_id
"extra_param_json": urlsafe_b64encode(json.dumps(ensureSerializable(extra_param_json)))
}
if erp5_action_key == 'object_jump':
......@@ -1557,6 +1709,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'}
# extra_param_json: <base64 encoded JSON> (paramters for getHateoas itself)
#
# Default Param JSON contains
# portal_type: list of Portal Types to include (singular form matches the
......@@ -1568,6 +1721,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)
......@@ -1575,6 +1732,21 @@ 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)
# Put all items from extra_param_json into the REQUEST. It is the only
# way how we can keep state, which is required by some actions for example
# 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 not extra_param_json:
extra_param_json = {}
if isinstance(extra_param_json, str):
extra_param_json = ensureDeserialized(json.loads(urlsafe_b64decode(extra_param_json)))
for key, value in extra_param_json.items():
REQUEST.set(key, value)
# in case we have custom list method
catalog_kw = {}
......@@ -1629,7 +1801,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)
......@@ -1652,7 +1824,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
#
# for k, v in catalog_kw.items():
# REQUEST.set(k, v)
search_result_iterable = callable_list_method(**catalog_kw)
# Cast to list if only one element is provided
......@@ -1689,7 +1860,8 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
catalog_kw['selection'] = context.getPortalObject().portal_selections.getSelectionFor(selection_name, REQUEST)
# field TALES expression evaluated by Base_getRelatedObjectParameter requires that
REQUEST.other['form_id'] = listbox_form.id
# 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
......@@ -1805,7 +1977,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
}
# By default, we won't be generating views in the URL
generate_view = False
url_parameter_dict = {}
if select in url_column_dict:
# Check if we get URL parameters using listbox field `url_columns`
......@@ -1874,14 +2045,12 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# view_kw. These parameters are required to create url at hateoas side
# using the URL template as necessary
if 'view_kw' in contents_item[select]['url_value']:
generate_view = True
# Get extra parameters either from url_result_dict or from brain
extra_url_param_dict = contents_item[select]['url_value']['view_kw'].get('extra_param_json', {})
if generate_view:
url_template_id = 'traverse_generator'
if extra_url_param_dict:
url_template_id = 'traverse_generator_with_parameter'
url_template_id = 'traverse_generator_action'
contents_item[select]['url_value']['options']['view'] =\
url_template_dict[url_template_id] % {
......@@ -1997,7 +2166,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
response.setStatus(405)
return ""
renderForm(traversed_document, form, result_dict)
renderForm(traversed_document, form, result_dict, extra_param_json=extra_param_json)
elif mode == 'newContent':
#################################################
......@@ -2087,6 +2256,35 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
return result_dict
mime_type = 'application/hal+json'
portal = context.getPortalObject()
sql_catalog = portal.portal_catalog.getSQLCatalog()
# Calculate the site root to prevent unexpected browsing
is_web_mode = (context.REQUEST.get('current_web_section', None) is not None) or (hasattr(context, 'isWebMode') and context.isWebMode())
# is_web_mode = traversed_document.isWebMode()
if is_web_mode:
site_root = context.getWebSectionValue()
view_action_type = site_root.getLayoutProperty("configuration_view_action_category", default='object_view')
else:
site_root = portal
view_action_type = "object_view"
context.Base_prepareCorsResponse(RESPONSE=response)
# Check if traversed_document is the site_root
if relative_url:
temp_traversed_document = site_root.restrictedTraverse(relative_url, None)
if (temp_traversed_document is None):
response.setStatus(404)
return ""
else:
temp_traversed_document = context
temp_is_site_root = (temp_traversed_document.getPath() == site_root.getPath())
temp_is_portal = (temp_traversed_document.getPath() == portal.getPath())
response.setHeader('Content-Type', mime_type)
hateoas = calculateHateoas(is_portal=temp_is_portal, is_site_root=temp_is_site_root,
traversed_document=temp_traversed_document,
......@@ -2095,7 +2293,8 @@ hateoas = calculateHateoas(is_portal=temp_is_portal, is_site_root=temp_is_site_r
query=query, select_list=select_list, limit=limit, form=form,
restricted=restricted, list_method=list_method,
default_param_json=default_param_json,
form_relative_url=form_relative_url)
form_relative_url=form_relative_url,
extra_param_json=extra_param_json)
if hateoas == "":
return hateoas
else:
......
......@@ -56,7 +56,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None, response=None, view=None, mode=\'root\', query=None, select_list=None, limit=10, local_roles=None, form=None, relative_url=None, restricted=0, list_method=None, default_param_json=None, form_relative_url=None, bulk_list="[]", sort_on=None, selection_domain=None, extra_param_json=None</string> </value>
<value> <string>REQUEST=None, response=None, view=None, mode=\'root\', query=None, select_list=None, limit=10, local_roles=None, form=None, form_data=None, relative_url=None, restricted=0, list_method=None, default_param_json=None, form_relative_url=None, bulk_list="[]", sort_on=None, selection_domain=None, extra_param_json=None, portal_status_message=\'\', portal_status_level=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
......@@ -567,7 +567,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_links']['action_object_view'][0]['name'], "view")
self.assertEqual(result_dict['_links']['action_workflow'][0]['href'],
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=custom_action_no_dialog&form_id=Foo_view" % (
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=custom_action_no_dialog&extra_param_json=eyJmb3JtX2lkIjogIkZvb192aWV3In0=" % (
self.portal.absolute_url(),
urllib.quote_plus(document.getRelativeUrl())))
self.assertEqual(result_dict['_links']['action_workflow'][0]['title'], "Custom Action No Dialog")
......@@ -586,7 +586,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_links']['site_root']['name'], self.portal.web_site_module.hateoas.getTitle())
self.assertEqual(result_dict['_links']['action_object_new_content_action']['href'],
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=create_a_document&form_id=Foo_view" % (
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=create_a_document&extra_param_json=eyJmb3JtX2lkIjogIkZvb192aWV3In0=" % (
self.portal.absolute_url(),
urllib.quote_plus(document.getRelativeUrl())))
self.assertEqual(result_dict['_links']['action_object_new_content_action']['title'], "Create a Document")
......@@ -630,7 +630,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_embedded']['_view']['listbox']['editable_column_list'], [['id', 'ID'], ['title', 'Title'], ['quantity', 'quantity'], ['start_date', 'Date']])
self.assertEqual(result_dict['_embedded']['_view']['listbox']['sort_column_list'], [['id', 'ID'], ['title', 'Title'], ['quantity', 'Quantity'], ['start_date', 'Date']])
self.assertEqual(result_dict['_embedded']['_view']['listbox']['list_method_template'],
'%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_ui_test/Foo_view/listbox&list_method=objectValues&default_param_json=eyJwb3J0YWxfdHlwZSI6IFsiRm9vIExpbmUiXSwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId()))
'%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_ui_test/Foo_view/listbox&list_method=objectValues&extra_param_json=eyJmb3JtX2lkIjogIkZvb192aWV3In0=&default_param_json=eyJwb3J0YWxfdHlwZSI6IFsiRm9vIExpbmUiXSwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId()))
self.assertEqual(result_dict['_embedded']['_view']['listbox']['domain_root_list'], [['foo_category', 'FooCat'], ['foo_domain', 'FooDomain'], ['not_existing_domain', 'NotExisting']])
NBSP_prefix = u'\xA0' * 4
self.assertEqual(result_dict['_embedded']['_view']['listbox']['domain_dict'], {'foo_domain': [['a', 'a'], ['%sa1' % NBSP_prefix, 'a/a1'], ['%sa2' % NBSP_prefix, 'a/a2'], ['b', 'b']], 'foo_category': [['a', 'a'], ['a/a1', 'a/a1'], ['a/a2', 'a/a2'], ['b', 'b']]})
......@@ -888,7 +888,7 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['editable_column_list'], [['time', 'Time'], ['comment', 'Comment'], ['error_message', 'Error Message']])
self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['sort_column_list'], [])
self.assertEqual(result_dict['_embedded']['_view']['report_section_list'][1]['listbox']['list_method_template'],
'%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_core/Base_viewWorkflowHistory/listbox&list_method=Base_getWorkflowHistoryItemList&default_param_json=eyJ3b3JrZmxvd19pZCI6ICJmb29fd29ya2Zsb3ciLCAiY2hlY2tlZF9wZXJtaXNzaW9uIjogIlZpZXciLCAid29ya2Zsb3dfdGl0bGUiOiAiRm9vIFdvcmtmbG93IiwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId()))
'%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=search&relative_url=foo_module%%2F%s&form_relative_url=portal_skins/erp5_core/Base_viewWorkflowHistory/listbox&list_method=Base_getWorkflowHistoryItemList&extra_param_json=eyJmb3JtX2lkIjogIkJhc2Vfdmlld1dvcmtmbG93SGlzdG9yeSJ9&default_param_json=eyJ3b3JrZmxvd19pZCI6ICJmb29fd29ya2Zsb3ciLCAiY2hlY2tlZF9wZXJtaXNzaW9uIjogIlZpZXciLCAid29ya2Zsb3dfdGl0bGUiOiAiRm9vIFdvcmtmbG93IiwgImlnbm9yZV91bmtub3duX2NvbHVtbnMiOiB0cnVlfQ=={&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}' % (self.portal.absolute_url(), document.getId()))
@simulate('Base_getRequestUrl', '*args, **kwargs',
......@@ -1643,9 +1643,9 @@ class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin):
urllib.quote_plus(document.getRelativeUrl())))
self.assertEqual(result_dict['result_list'][0]['_links']['action_object_view'][0]['title'], "View")
self.assertEqual(result_dict['result_list'][0]['_links']['action_object_view'][0]['name'], "view")
# extra_param_json contains information necessary for the getHateoas script itself (currently it is {'form_id': "Foo_view"})
self.assertEqual(result_dict['result_list'][0]['_links']['action_workflow'][0]['href'],
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=custom_action_no_dialog&form_id=Foo_view" % (
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=custom_action_no_dialog&extra_param_json=eyJmb3JtX2lkIjogIkZvb192aWV3In0=" % (
self.portal.absolute_url(),
urllib.quote_plus(document.getRelativeUrl())))
self.assertEqual(result_dict['result_list'][0]['_links']['action_workflow'][0]['title'], "Custom Action No Dialog")
......@@ -1658,7 +1658,7 @@ class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['result_list'][0]['_links']['site_root']['name'], self.portal.web_site_module.hateoas.getTitle())
self.assertEqual(result_dict['result_list'][0]['_links']['action_object_new_content_action']['href'],
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=create_a_document&form_id=Foo_view" % (
"%s/web_site_module/hateoas/ERP5Document_getHateoas?mode=traverse&relative_url=%s&view=create_a_document&extra_param_json=eyJmb3JtX2lkIjogIkZvb192aWV3In0=" % (
self.portal.absolute_url(),
urllib.quote_plus(document.getRelativeUrl())))
self.assertEqual(result_dict['result_list'][0]['_links']['action_object_new_content_action']['title'], "Create a Document")
......@@ -1889,6 +1889,7 @@ class TestERP5Action_getHateoas(ERP5HALJSONStyleSkinsMixin):
REQUEST=fake_request,
dialog_method='Foo_doNothing', # 'Workflow_statusModify' would lead us by a different path in the code
dialog_id='Foo_viewCustomWorkflowRequiredActionDialog',
form_id='FooModule_viewFooList'
)
self.assertEqual(fake_request.RESPONSE.status, 400)
......
portal_actions | clone_document
portal_actions | create_a_document
portal_actions | delete_document
portal_actions | delete_document_list
\ No newline at end of file
......@@ -26,7 +26,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
<tal:block tal:define="form_action string:Base_doSelect;
form_id form/id;
list_mode python: True;
listmode_default_listbox python: here.ERP5Site_getListbox(form=form);
listmode_default_listbox python: form.Form_getListbox();
selection_name listmode_default_listbox/selection_name;
selection_index request/selection_index | python:0;
dummy python: selection_name and request.set('selection_name', selection_name);
......
......@@ -110,7 +110,7 @@ It contains the same columns
<td></td>
</tr>
<tr>
<td>verifyTextPresent</td>
<td>assertTextPresent</td>
<td>Deleted.</td>
<td></td>
</tr>
......@@ -186,8 +186,8 @@ It contains the same columns
<td></td>
</tr>
<tr>
<td>verifyTextPresent</td>
<td>Sorry, you can not delete 1 item.</td>
<td>assertTextPresent</td>
<td>Deleted.</td>
<td></td>
</tr>
<tr>
......@@ -217,7 +217,7 @@ It contains the same columns
</tr>
<tr>
<td>open</td>
<td>${base_url}/foo_module/view</td>
<td>${base_url}/foo_module/view?reset=1</td>
<td></td>
</tr>
<tr>
......@@ -289,7 +289,7 @@ It contains the same columns
</tr>
<tr>
<td>open</td>
<td>${base_url}/foo_module/view</td>
<td>${base_url}/foo_module/view?reset=1</td>
<td></td>
</tr>
<tr>
......
/*global window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray */
/*global window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray */
/*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray) {
(function (window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray) {
"use strict";
/////////////////////////////////////////////////////////////////
......@@ -19,15 +19,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": UriTemplate.parse(erp5_link.href).expand({query: query}),
"page": undefined
}
});
......@@ -62,6 +62,7 @@
.declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
/////////////////////////////////////////////////////////////////
// declared methods
......@@ -72,7 +73,10 @@
// Get the whole view as attachment because actions can change based on
// what view we are at. If no view available than fallback to "links".
return gadget.jio_getAttachment(options.jio_key, options.view || "links")
return new RSVP.Queue()
.push(function () {
return gadget.jio_getAttachment(options.jio_key, options.view || "links");
})
.push(function (jio_attachment) {
var transition_list = ensureArray(jio_attachment._links.action_workflow),
action_list = ensureArray(jio_attachment._links.action_object_jio_action)
......@@ -105,4 +109,4 @@
});
});
}(window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray));
\ No newline at end of file
}(window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray));
\ No newline at end of file
......@@ -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.28693.12915.8550</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1519987107.92</float>
<float>1522141241.23</float>
<string>UTC</string>
</tuple>
</state>
......
/*global window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray */
/*global window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray */
/*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray) {
(function (window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray) {
"use strict";
/////////////////////////////////////////////////////////////////
......@@ -19,15 +19,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": UriTemplate.parse(erp5_link.href).expand({query: query}),
"page": undefined
}
});
......@@ -60,6 +60,7 @@
.declareAcquiredMethod("jio_getAttachment", "jio_getAttachment")
.declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
.declareAcquiredMethod("updateHeader", "updateHeader")
/////////////////////////////////////////////////////////////////
......@@ -100,4 +101,4 @@
});
});
}(window, rJS, RSVP, Handlebars, calculatePageTitle, ensureArray));
\ No newline at end of file
}(window, rJS, RSVP, Handlebars, UriTemplate, calculatePageTitle, ensureArray));
\ No newline at end of file
......@@ -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.24987.20289.62754</string> </value>
<value> <string>966.34409.25650.52155</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1518184046.23</float>
<float>1522842712.42</float>
<string>UTC</string>
</tuple>
</state>
......
<!DOCTYPE html>
<!--
data-i18n=Encountered an unknown error. Try to resubmit.
data-i18n=Input data has errors.
data-i18n=You do not have the permissions to edit the object.
data-i18n=You are offline.
data-i18n=Action succeeded.
data-i18n=Data received.
-->
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
......
......@@ -220,7 +220,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>
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>961.19210.8471.60620</string> </value>
<value> <string>966.41656.42235.61815</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,7 +252,7 @@
</tuple>
<state>
<tuple>
<float>1518685706.64</float>
<float>1523287594.73</float>
<string>UTC</string>
</tuple>
</state>
......
/*global window, rJS, URI, RSVP, asBoolean */
/*global window, document, rJS, URI, RSVP, jIO, Blob, URL, asBoolean */
/*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, URI, RSVP, asBoolean) {
/**
Page Form is a top-level gadget (a "Page") taking care of rendering form
and handling data send&receive.
*/
(function (window, document, rJS, URI, RSVP, jIO, Blob, URL, asBoolean) {
"use strict";
/** Return local modifications to editable form fields after leaving the form
for a while - for example selecting a related object.
We use the fact that selecting a related object is still rendered by page_form
thus gadget.state acts as a persistent storage.
@argument result is possible current field value
*/
function loadFormContent(gadget, result) {
......@@ -44,9 +51,14 @@
// Acquired methods
/////////////////////////////////////////////////////////////////
.declareAcquiredMethod("jio_getAttachment", "jio_getAttachment")
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("jio_allDocs", "jio_allDocs")
.declareAcquiredMethod("updatePanel", "updatePanel")
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
/////////////////////////////////////////////////////////////////
// Proxy methods to the child gadget
......@@ -63,8 +75,12 @@
return declared_gadget.checkValidity();
});
}, {mutex: 'changestate'})
.declareMethod('getContent', function () {
return this.getDeclaredGadget('fg')
var gadget = this;
// no need to add runtime information in general for forms ...
// each Form Page Template handles that on their own
return gadget.getDeclaredGadget('fg')
.push(function (declared_gadget) {
return declared_gadget.getContent();
});
......@@ -103,7 +119,8 @@
options: options,
erp5_document: undefined,
erp5_form: undefined,
url: undefined
url: undefined,
embedded: asBoolean(options.embedded)
};
if (options.hasOwnProperty('erp5_document')) {
......@@ -145,7 +162,11 @@
return gadget.jio_getAttachment(uri.segment(2), "view");
})
.push(function (erp5_form) {
var url = "gadget_erp5_pt_" + erp5_form.pt;
var url;
if (new_state.embedded) {
erp5_form.pt = "embedded_form_render"; // hard-coded erp5 naming
}
url = "gadget_erp5_pt_" + erp5_form.pt;
// XXX Hardcoded specific behaviour for form_view
if ((options.editable === 1) && (erp5_form.pt === "form_view")) {
url += "_editable";
......@@ -163,7 +184,7 @@
.onStateChange(function (modification_dict) {
var queue,
gadget = this,
options = this.state.options,
options = gadget.state.options,
page_template_gadget,
erp5_document = JSON.parse(gadget.state.erp5_document),
erp5_form = JSON.parse(gadget.state.erp5_form);
......@@ -216,19 +237,69 @@
}
});
})
.allowPublicAcquisition("displayFormulatorValidationError", function (param_list) {
var erp5_document = JSON.parse(this.state.erp5_document);
erp5_document._embedded._view = param_list[0];
// Force refresh
erp5_document._now = Date.now();
/** SubmitContent should be called by the gadget which renders submit button
thus should handle the submit event.
It calls getContent on the child gadget and submits those data to given
jio_key and URL using JIO putAttachment call.
This function handles parsing the server response, showing error/success
messages and re-rendering the form if obtained (in success and failure case).
Your .thenable will either receive string jio key to redirect to or undefined|null
in case no redirect should be issued.
Returns: on success it returns a Promise with {string} JIO key
on failure it throws an error with the invalid response
*/
.allowPublicAcquisition("submitContent", function (param_list) {
var gadget = this,
jio_key = param_list[0],
target_url = param_list[1];
return gadget.notifySubmitting()
.push(function () {
return gadget.getContent();
})
.push(function (data) {
return gadget.jio_putAttachment(jio_key, target_url, data);
})
.push(function (attachment) {
return this.changeState({erp5_document: JSON.stringify(erp5_document)});
if (attachment.target.response.type === "application/json") {
// successful form save returns simple redirect and an answer as JSON
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
return gadget.notifySubmitted({
"message": response.portal_status_message,
"status": response.portal_status_level || "success"
});
})
/** Re-render whole form page with completely new form. */
.allowPublicAcquisition("updateForm", function (args, subgadget_id) {
var erp5_document = JSON.parse(this.state.erp5_document),
options = this.state.options;
erp5_document._embedded._view = args[0];
.push(function () {
// here we figure out where to go after form submit - indicated
// by X-Location HTTP header placed by Base_redirect script
var redirect_jio_key = new URI(
attachment.target.getResponseHeader("X-Location")
).segment(2);
return redirect_jio_key;
});
}
if (attachment.target.response.type === "application/hal+json") {
// we have received a view definition thus we need to redirect
// this will happen only in report/export when "Format" is unspecified
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
var erp5_document = JSON.parse(gadget.state.erp5_document),
response_view = JSON.parse(response_text.target.result),
options = gadget.state.options;
erp5_document._embedded._view = response_view;
erp5_document._now = Date.now(); // force refresh
// We choose render instead of changeState because the new form can use
// different page_template (reports are setup in form_dialog but rendered
......@@ -239,7 +310,208 @@
// We modify inplace state.options because render method uses and removes
// erp5_document hidden in its options.
options.erp5_document = erp5_document;
return this.render(options);
return new RSVP.Queue()
.push(function () {
if (response_view._notification === undefined) {
return gadget.translate("Data received.");
}
return response_view._notification.message;
})
.push(function (translated_message) {
return gadget.notifySubmitted({
"message": translated_message,
"status": response_view._notification ? response_view._notification.status : "success"
});
})
.push(function () {
/* We do not need to remove _notification because we
* force-reload by putting _now into "hashed" document
if (response_view._notification !== undefined) {
delete response_view._notification;
}
*/
return gadget.render(options);
})
.push(function () {
// Make sure to return nothing (previous render can return
// something) so the successfull handler does not receive
// anything which it could consider as redirect jio key.
return;
});
});
}
// response status > 200 (e.g. 202 "Accepted" or 204 "No Content")
// means a sucessful execution of the action but does not carry any data
// XMLHttpRequest automatically inserts Content-Type="text/xml" thus
// we cannot test based on that
if (attachment.target.response.size === 0 &&
attachment.target.status > 200 &&
attachment.target.status < 400) {
return gadget.translate("Action succeeded.")
.push(function (translated_message) {
return gadget.notifySubmitted({
"message": translated_message,
"status": "success"
});
})
.push(function () {
return jio_key;
});
}
// any other attachment type we force to download because it is most
// likely product of export/report (thus PDF, ODT ...)
return gadget.translate("Data received.")
.push(function (translated_message) {
return gadget.notifySubmitted({
"message": translated_message,
"status": "success"
});
})
.push(function () {
return gadget.forceDownload(attachment);
})
// we could redirect back after download which was not possible
// in the old UI but it will be a change of behaviour
// Nicolas required this feature to be allowed
.push(function () {
return jio_key;
});
})
.push(null, function (error) {
/** Fail branch of the JIO call. */
var error_text = 'Encountered an unknown error. Try to resubmit.';
if (error instanceof RSVP.CancellationError) {
// CancellationError is thrown on "redirect" to cancel any pending
// promises. Since it is not a failure we rethrow.
throw error;
}
if (error === undefined || error.target === undefined) {
return gadget.translate('Encountered an unknown error. Try to resubmit.')
.push(function (translated_message) {
return gadget.notifySubmitted({
'message': translated_message,
'status': 'error'
});
})
.push(function () {
throw error;
});
}
// Let's display notification about the error to the user if possible
if (error.target.status === 400) {
error_text = 'Input data has errors.';
} else if (error.target.status === 403) {
error_text = 'You do not have the permissions to edit the object.';
} else if (error.target.status === 0) {
error_text = 'You are offline.';
}
// If the response is JSON, then look for the translated message sent
// by the portal and display it to the user
if (error.target.response.type === 'application/json' ||
error.target.response.type === 'application/hal+json') {
return gadget.notifySubmitted()
.push(function () {
return jIO.util.readBlobAsText(error.target.response);
})
// Translated error description must be part of the response
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
if (error.target.response.type === 'application/json') {
// pure JSON carries only the message (deprecated)
// so we parse it out and return
return gadget.notifyChange({
"message": response.portal_status_message,
"status": "error"
});
}
if (error.target.response.type === 'application/hal+json') {
// HAL+JSON carries whole form definition with optional message
return new RSVP.Queue()
.push(function () {
if (!response._notification || !response._notification.message) {
// return error text from HTTP Status CODE and translate
return gadget.translate(error_text);
}
return response._notification.message;
})
.push(function (translated_message) {
return gadget.notifyChange({
"message": translated_message,
"status": response._notification ? response._notification.status : "error"
});
})
.push(function () {
var erp5_document = JSON.parse(gadget.state.erp5_document);
erp5_document._embedded._view = response;
erp5_document._now = Date.now();
return gadget.changeState({erp5_document: JSON.stringify(erp5_document)});
});
}
})
.push(function () {
throw error;
});
}
// If the response in empty with only HTTP Status code then we display
// our static translated error_text to the user
return gadget.notifySubmitted()
.push(function () {
return gadget.translate(error_text);
})
.push(function (message) {
return gadget.notifyChange({
"message": message,
"status": "error"
});
})
.push(function () {
throw error;
});
});
})
/** The only way how to force download from javascript (working everywhere)
* is unfortunately constructing <a> and clicking on it
*/
.declareJob("forceDownload", function (attachment) {
var attachment_data = attachment.target.response,
filename = /(?:^|;)\s*filename\s*=\s*"?([^";]+)/i.exec(
attachment.target.getResponseHeader("Content-Disposition") || ""
),
a_tag = document.createElement("a");
if (attachment.target.responseType !== "blob") {
attachment_data = new Blob(
[attachment.target.response],
{type: attachment.target.getResponseHeader("Content-Type")}
);
}
a_tag.style = "display: none";
a_tag.href = URL.createObjectURL(attachment_data);
a_tag.download = filename ? filename[1].trim() : "untitled";
document.body.appendChild(a_tag);
a_tag.click();
return new RSVP.Queue()
.push(function () {
return RSVP.delay(10);
})
.push(function () {
URL.revokeObjectURL(a_tag.href);
document.body.removeChild(a_tag);
});
});
}(window, rJS, URI, RSVP, asBoolean));
\ No newline at end of file
}(window, document, rJS, URI, RSVP, jIO, Blob, URL, asBoolean));
\ No newline at end of file
/*jslint nomen: true, indent: 2, maxerr: 3 */
/*global window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray */
(function (window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray) {
/*global window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray */
(function (window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray) {
"use strict";
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;
function submitDialog(gadget, is_updating) {
return form_gadget.notifySubmitting()
.push(function () {
return form_gadget.getDeclaredGadget("erp5_form");
})
.push(function (erp5_form) {
return erp5_form.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'];
}
data.dialog_method = form_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];
}
}
return form_gadget.jio_putAttachment(
form_gadget.state.jio_key,
action.href,
data
);
})
.push(function (attachment) {
if (attachment.target.response.type === "application/json") {
// successful form save returns simple redirect and answer as JSON
// validation errors are handled in failure branch on bottom
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
// set state for getContent to know what field values to choose for `dialog_method`
gadget.state.is_updating = is_updating;
return form_gadget.notifySubmitted({
"message": response.portal_status_message,
"status": "success"
});
})
.push(function () {
// here we figure out where to go after form submit - indicated
// by X-Location HTTP header placed by Base_redirect script
var jio_key = new URI(
attachment.target.getResponseHeader("X-Location")
).segment(2),
splitted_jio_key_list,
return gadget.submitContent(
gadget.state.jio_key,
gadget.state.erp5_document._embedded._view._actions.put.href // most likely points to Base_callDialogMethod
)
.push(function (jio_key) { // success redirect handler
var splitted_jio_key_list,
splitted_current_jio_key_list,
command,
i;
if (redirect_to_parent) {
return form_gadget.redirect({command: 'history_previous'});
if (gadget.state.redirect_to_parent || jio_key === undefined) {
return gadget.redirect({command: 'history_previous'});
}
if (form_gadget.state.jio_key === jio_key) {
if (gadget.state.jio_key === jio_key) {
// don't update navigation history when not really redirecting
return form_gadget.redirect({
return gadget.redirect({
command: 'change',
options: {
"jio_key": jio_key,
......@@ -92,7 +37,7 @@
// In this case, do not add current document to the history
// example: when cloning, do not keep the original document in history
splitted_jio_key_list = jio_key.split('/');
splitted_current_jio_key_list = form_gadget.state.jio_key.split('/');
splitted_current_jio_key_list = gadget.state.jio_key.split('/');
command = 'display_with_history';
if (splitted_jio_key_list.length === splitted_current_jio_key_list.length) {
for (i = 0; i < splitted_jio_key_list.length - 1; i += 1) {
......@@ -105,139 +50,21 @@
}
// forced document change thus we update history
return form_gadget.redirect({
return gadget.redirect({
command: command,
options: {
"jio_key": jio_key
// do not mingle with editable because it isn't necessary
}
});
});
}
if (attachment.target.response.type === "application/hal+json") {
// we have received a view definition thus we need to redirect
// this will happen only in report/export when "Format" is unspecified
return new RSVP.Queue()
.push(function () {
return form_gadget.notifySubmitted({
"message": "Data received",
"status": "success"
});
})
.push(function () {
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
return form_gadget.updateForm(JSON.parse(response_text.target.result));
});
}
// response status > 200 (e.g. 202 "Accepted" or 204 "No Content")
// mean sucessful execution of an action but does not carry any data
// XMLHttpRequest automatically inserts Content-Type="text/xml" thus
// we cannot test based on that
if (attachment.target.response.size === 0 &&
attachment.target.status > 200 &&
attachment.target.status < 400) {
return new RSVP.Queue()
.push(function () {
return form_gadget.notifySubmitted({
"message": "Action succeeded",
"status": "success"
});
})
.push(function () {
if (redirect_to_parent) {
return form_gadget.redirect({command: 'history_previous'});
}
return form_gadget.redirect({
command: 'change',
options: {
"jio_key": form_gadget.state.jio_key,
"view": "view",
"page": undefined
// do not mingle with editable because it isn't necessary
}
});
});
}
// any other attachment type we force to download because it is most
// likely product of export/report (thus PDF, ODT ...)
return new RSVP.Queue()
.push(function () {
return form_gadget.notifySubmitted({
"message": "Data received",
"status": "success"
});
})
.push(function () {
return form_gadget.forceDownload(attachment);
});
})
.push(undefined, function (error) {
if (error !== undefined && error.target !== undefined) {
var error_text = 'Encountered an unknown error. Try to resubmit',
promise_queue = new RSVP.Queue();
// if we know what the error was, try to precise it for the user
if (error.target.status === 400) {
error_text = 'Input data has errors';
} else if (error.target.status === 403) {
error_text = 'You do not have the permissions to edit the object';
} else if (error.target.status === 0) {
error_text = 'Document was not saved! Resubmit when you are online or the document accessible';
}
// if the response type is json, then look for the status message
// sent from the portal. We prefer to have portal_status_message in
// all cases when we have error
if (error.target.response.type === 'application/json') {
promise_queue
.push(function () {
return jIO.util.readBlobAsText(error.target.response);
})
// Get the error_text from portal_status_message, if there is no
// portal_status_message, then use the default error_text
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
// If there is no portal_status_message, use the default
// error_text
error_text = response.portal_status_message || error_text;
});
}
// display translated error_text to user
promise_queue
.push(function () {
return form_gadget.notifySubmitted();
})
.push(function () {
return form_gadget.translate(error_text);
})
.push(function (message) {
return form_gadget.notifyChange({
"message": message + '.',
"status": "error"
});
});
// if server validation of form data failed (indicated by response code 400)
// we parse out field errors and display them to the user
if (error.target.status === 400 &&
error.target.response.type === 'application/hal+json') {
promise_queue
.push(function () {
// when the server-side validation returns the error description
if (error.target.responseType === "blob") {
return jIO.util.readBlobAsText(error.target.response);
}
// otherwise return (most-likely) textual response of the server
return {target: {result: error.target.response}};
})
.push(function (event) {
return form_gadget.displayFormulatorValidationError(JSON.parse(event.target.result));
});
}
return promise_queue;
}
.push(null, function (error) {
if (error instanceof RSVP.CancellationError) {
// CancellationError is thrown on "redirect" to cancel any pending
// promises. Since it is not a failure we rethrow.
throw error;
}
return; // page form handles failures good enough
});
}
......@@ -249,20 +76,22 @@
dialog_button_template = Handlebars.compile(dialog_button_source);
gadget_klass
.setState({
'redirect_to_parent': false, // set by a presence of special field
'has_update_action': undefined, // default "submit" issue update in case of its presence
'is_updating': false // state variable set based on the type of clicked submit button
})
/////////////////////////////////////////////////////////////////
// acquisition
/////////////////////////////////////////////////////////////////
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
.declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("updateForm", "updateForm")
.declareAcquiredMethod("displayFormulatorValidationError", "displayFormulatorValidationError")
.declareAcquiredMethod("submitContent", "submitContent")
/////////////////////////////////////////////////////////////////
// Proxy methods to the child gadget
......@@ -273,12 +102,37 @@
return declared_gadget.checkValidity();
});
}, {mutex: 'changestate'})
.declareMethod('getContent', function () {
var gadget = this;
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;
// For Update Action - override the default value from "action"
if (gadget.state.is_updating) {
data.dialog_method = gadget.state.form_definition.update_action;
data.update_method = gadget.state.form_definition.update_action;
}
return data;
});
}, {mutex: 'changestate'})
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
......@@ -287,23 +141,31 @@
}, {mutex: 'changestate'})
.declareMethod('render', function (options) {
var gadget = this;
// copy out wanted items from options and pass it to `changeState`
return this.changeState({
return gadget.getUrlParameter('extended_search')
.push(function (extended_search) {
return gadget.changeState({
jio_key: options.jio_key,
view: options.view,
// ignore options.editable because dialog is always editable
erp5_document: options.erp5_document,
form_definition: options.form_definition,
erp5_form: options.erp5_form || {},
// ignore global editable state (be always editable)
show_update_button: Boolean(options.form_definition.update_action)
// editable: true, // ignore global editable state (be always editable)
has_update_action: Boolean(options.form_definition.update_action),
// pass extended_search from previous view in case any gadget is curious
extended_search: extended_search,
// XXX Hack of ERP5 how to express redirect to parent after success
redirect_to_parent: options.erp5_document._embedded._view.field_your_redirect_to_parent !== undefined
});
});
})
.onStateChange(function (modification_dict) {
var form_gadget = this,
selector = form_gadget.element.querySelector("h3"),
view_list = ensureArray(this.state.erp5_document._links.action_workflow),
view_list,
icon,
title,
i;
......@@ -329,19 +191,22 @@
break;
}
// if possible get current action from list of actions to get the correct title
if (this.state.erp5_document._links !== undefined && this.state.erp5_document._links.action_workflow !== undefined) {
view_list = ensureArray(this.state.erp5_document._links.action_workflow);
// By default we display dialog form title
for (i = 0; i < view_list.length; i += 1) {
if (this.state.view === view_list[i].href) {
title = view_list[i].title;
}
}
}
return new RSVP.Queue()
.push(function () {
// Set the dialog button
if (modification_dict.hasOwnProperty('show_update_button')) {
if (modification_dict.hasOwnProperty('has_update_action')) {
return form_gadget.translateHtml(dialog_button_template({
show_update_button: form_gadget.state.show_update_button
show_update_button: form_gadget.state.has_update_action
}))
.push(function (html) {
form_gadget.element.querySelector('.dialog_button_container')
......@@ -374,11 +239,17 @@
form_options.view = form_gadget.state.view;
form_options.jio_key = form_gadget.state.jio_key;
form_options.editable = true; // dialog is always editable
// this might cause problems if the listbox in the dialog is not curious
// about the previous search
if (form_gadget.state.extended_search) {
form_options.form_definition.extended_search = form_gadget.state.extended_search;
}
return erp5_form.render(form_options);
})
.push(function () {
// Render the headers
return new RSVP.Queue()
.push(function () {
return RSVP.all([
form_gadget.getUrlFor({command: 'change', options: {page: undefined, view: undefined}}),
calculatePageTitle(form_gadget, form_gadget.state.erp5_document)
......@@ -390,57 +261,30 @@
cancel_url: all_result[0],
page_title: all_result[1]
});
});
})
/** The only way how to force download from javascript (working everywhere)
* is unfortunately constructing <a> and clicking on it
*/
.declareJob("forceDownload", function (attachment) {
var attachment_data = attachment.target.response,
filename = /(?:^|;)\s*filename\s*=\s*"?([^";]+)/i.exec(
attachment.target.getResponseHeader("Content-Disposition") || ""
),
a_tag = document.createElement("a");
if (attachment.target.responseType !== "blob") {
attachment_data = new Blob(
[attachment.target.response],
{type: attachment.target.getResponseHeader("Content-Type")}
);
}
a_tag.style = "display: none";
a_tag.href = URL.createObjectURL(attachment_data);
a_tag.download = filename ? filename[1].trim() : "untitled";
document.body.appendChild(a_tag);
a_tag.click();
return new RSVP.Queue()
.push(function () {
return RSVP.delay(10);
})
.push(function () {
URL.revokeObjectURL(a_tag.href);
document.body.removeChild(a_tag);
.push(null, function (error) { // error while updating the header
window.console.log(error); // should not bring down the app
});
});
})
.onEvent('submit', function () {
if (this.state.has_update_action === true) {
return submitDialog(this, "update_action", true);
// default action on submit is update in case of its existence
return submitDialog(this, true);
}
return submitDialog(this, "action");
return submitDialog(this, false);
}, false, true)
.onEvent('click', function (evt) {
if (evt.target.name === "action_confirm") {
evt.preventDefault();
return submitDialog(this, "action");
return submitDialog(this, false);
}
if (evt.target.name === "action_update") {
evt.preventDefault();
return submitDialog(this, "update_action", true);
return submitDialog(this, true);
}
}, false, false);
}(window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray));
\ No newline at end of file
}(window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray));
\ No newline at end of file
......@@ -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.53063.4535.13141</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1519729953.99</float>
<float>1523541554.47</float>
<string>UTC</string>
</tuple>
</state>
......
/*global window, rJS, RSVP, calculatePageTitle, jIO */
/*global window, rJS, RSVP, calculatePageTitle */
/*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, calculatePageTitle, jIO) {
(function (window, rJS, RSVP, calculatePageTitle) {
"use strict";
rJS(window)
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("submitContent", "submitContent")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("displayFormulatorValidationError",
"displayFormulatorValidationError")
.declareAcquiredMethod('isDesktopMedia', 'isDesktopMedia')
.declareAcquiredMethod('getUrlParameter', 'getUrlParameter')
.allowPublicAcquisition("notifyChange", function () {
......@@ -29,12 +25,14 @@
return declared_gadget.checkValidity();
});
}, {mutex: 'changestate'})
.declareMethod('getContent', function () {
return this.getDeclaredGadget("erp5_form")
.push(function (declared_gadget) {
return declared_gadget.getContent();
});
}, {mutex: 'changestate'})
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
......@@ -64,44 +62,44 @@
})
.onStateChange(function () {
var form_gadget = this;
var gadget = this;
// render the erp5 form
return form_gadget.getDeclaredGadget("erp5_form")
.push(function (erp5_form) {
var form_options = form_gadget.state.erp5_form;
form_options.erp5_document = form_gadget.state.erp5_document;
form_options.form_definition = form_gadget.state.form_definition;
form_options.view = form_gadget.state.view;
form_options.jio_key = form_gadget.state.jio_key;
return gadget.getDeclaredGadget("erp5_form")
.push(function (sub_gadget) {
var form_options = gadget.state.erp5_form;
form_options.erp5_document = gadget.state.erp5_document;
form_options.form_definition = gadget.state.form_definition;
form_options.view = gadget.state.view;
form_options.jio_key = gadget.state.jio_key;
form_options.editable = 1;
return erp5_form.render(form_options);
return sub_gadget.render(form_options);
})
// render the header
.push(function () {
return RSVP.all([
form_gadget.getUrlFor({command: 'change', options: {page: "tab"}}),
form_gadget.getUrlFor({command: 'change', options: {page: "action"}}),
form_gadget.state.erp5_document._links.action_object_new_content_action ?
form_gadget.getUrlFor({command: 'change', options: {
view: form_gadget.state.erp5_document._links.action_object_new_content_action.href,
gadget.getUrlFor({command: 'change', options: {page: "tab"}}),
gadget.getUrlFor({command: 'change', options: {page: "action"}}),
gadget.state.erp5_document._links.action_object_new_content_action ?
gadget.getUrlFor({command: 'change', options: {
view: gadget.state.erp5_document._links.action_object_new_content_action.href,
editable: true
}}) :
"",
form_gadget.getUrlFor({command: 'history_previous'}),
form_gadget.getUrlFor({command: 'selection_previous'}),
form_gadget.getUrlFor({command: 'selection_next'}),
calculatePageTitle(form_gadget, form_gadget.state.erp5_document),
form_gadget.isDesktopMedia(),
(form_gadget.state.erp5_document._links.action_object_jio_report ||
form_gadget.state.erp5_document._links.action_object_jio_exchange ||
form_gadget.state.erp5_document._links.action_object_jio_print) ?
form_gadget.getUrlFor({command: 'change', options: {page: "export"}}) :
gadget.getUrlFor({command: 'history_previous'}),
gadget.getUrlFor({command: 'selection_previous'}),
gadget.getUrlFor({command: 'selection_next'}),
calculatePageTitle(gadget, gadget.state.erp5_document),
gadget.isDesktopMedia(),
(gadget.state.erp5_document._links.action_object_jio_report ||
gadget.state.erp5_document._links.action_object_jio_exchange ||
gadget.state.erp5_document._links.action_object_jio_print) ?
gadget.getUrlFor({command: 'change', options: {page: "export"}}) :
"",
form_gadget.getUrlParameter('selection_index')
gadget.getUrlParameter('selection_index')
]);
})
.push(function (all_result) {
......@@ -117,13 +115,13 @@
page_title: all_result[6]
},
is_desktop = all_result[7];
if (form_gadget.state.save_action === true) {
if (gadget.state.save_action === true) {
header_dict.save_action = true;
}
if (is_desktop) {
header_dict.export_url = all_result[8];
}
return form_gadget.updateHeader(header_dict);
return gadget.updateHeader(header_dict);
});
})
......@@ -134,103 +132,37 @@
return;
}
var form_gadget = this,
erp5_form,
form_id = this.state.erp5_document._embedded._view.form_id,
action = form_gadget.state.erp5_document._embedded._view._actions.put;
return form_gadget.getDeclaredGadget("erp5_form")
.push(function (gadget) {
erp5_form = gadget;
return erp5_form.checkValidity();
})
.push(function (validity) {
if (validity) {
return form_gadget.notifySubmitting()
.push(function () {
// try to send the form data over the network to jIO storage
return erp5_form.getContent();
})
.push(function (data) {
data[form_id.key] = form_id['default'];
var gadget = this,
action = gadget.state.erp5_document._embedded._view._actions.put;
return form_gadget.jio_putAttachment(
form_gadget.state.jio_key,
action.href,
data
);
})
// handle response from the server
.push(function (result) {
if (result.target.responseType === "blob") {
return jIO.util.readBlobAsText(result.target.response);
}
return {target: {result: result.target.response}};
})
.push(function (event) {
var message;
try {
message = JSON.parse(event.target.result).portal_status_message;
} catch (ignore) {
}
return form_gadget.notifySubmitted({
"message": message,
"status": "success"
});
return gadget.getDeclaredGadget("erp5_form")
.push(function (sub_gadget) {
return sub_gadget.checkValidity();
})
.push(function () {
return form_gadget.redirect({command: 'reload'});
})
.push(undefined, function (error) {
if (error.target !== undefined) {
var error_text = 'Encountered an unknown error. Try to resubmit',
promise;
// improve error message if we can
if (error.target.status === 400) {
error_text = 'Input data has errors';
} else if (error.target.status === 403) {
error_text = 'You do not have the permissions to edit the object';
} else if (error.target.status === 0) {
// no/default=0 status means a network connection problem
error_text = 'Document was not saved! Resubmit when you are online or the document accessible';
.push(function (is_valid) {
if (!is_valid) {
return;
}
// display translated error_text to user
promise = form_gadget.notifySubmitted()
.push(function () {
return form_gadget.translate(error_text);
return gadget.submitContent(
gadget.state.jio_key,
action.href
);
})
.push(function (message) {
return form_gadget.notifyChange({
'message': message + '.',
'status': 'error'
});
});
// if server validation of form data failed (indicated by response code 400)
// we parse out field errors and display them to the user
if (error.target.status === 400) {
promise
.push(function () {
// when the server-side validation returns the error description
if (error.target.responseType === "blob") {
return jIO.util.readBlobAsText(error.target.response);
.push(function (jio_key) {
if (jio_key) {
// success redirect callback receives jio_key
return gadget.redirect({command: 'reload'});
}
// otherwise return (most-likely) textual response of the server
return {target: {result: error.target.response}};
})
.push(function (event) {
return form_gadget.displayFormulatorValidationError(JSON.parse(event.target.result));
});
}
return promise;
}
// throwing an error is the last desperate option
.push(null, function (error) {
if (error instanceof RSVP.CancellationError) {
// CancellationError is thrown on "redirect" to cancel any pending
// promises. Since it is not a failure we rethrow.
throw error;
});
}
return; // page form handles failures well enough
});
}, false, true);
}(window, rJS, RSVP, calculatePageTitle, jIO));
\ No newline at end of file
}(window, rJS, RSVP, calculatePageTitle));
\ No newline at end of file
......@@ -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>966.32967.51097.665</string> </value>
<value> <string>966.48607.41615.32324</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1522335846.96</float>
<float>1523351312.53</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -65,7 +65,7 @@
var form_options = gadget.state.erp5_form,
embedded_form = gadget.state.erp5_document._embedded._view,
rendered_form = {},
key, field;
key;
/* Remove empty non-editable fields to prevent them from displaying (business requirement).
Deleting objects inplace was not a good idea.
......
......@@ -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>966.32979.9603.63197</string> </value>
<value> <string>966.39861.30579.11912</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1522336529.93</float>
<float>1522749475.57</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -67,7 +67,7 @@
return this.changeState({
erp5_document: options.erp5_document,
form_definition: options.form_definition,
form_gadget_url: form_gadget_url,
form_gadget_url: form_gadget_url
});
})
.onStateChange(function (modification_dict) {
......
......@@ -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>962.56167.53905.31470</string> </value>
<value> <string>964.45882.29366.36147</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1508400391.84</float>
<float>1522842618.99</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -57,7 +57,7 @@
<tal:block tal:define="notification_configuration python: {'class': 'error',
'text': 'Workflow script raised.'}">
'text': 'Workflow script raised'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
......
......@@ -46,24 +46,18 @@
<tr>
<td>open</td>
<td>${base_url}/web_site_module/renderjs_runner/#/bar_module/1</td>
<td>${base_url}/web_site_module/renderjs_runner/#/bar_module</td>
<td></td>
</tr>
<!-- Wait for gadget to be loaded -->
<tr>
<td>waitForElementPresent</td>
<td>//div[@data-gadget-url='${base_url}/web_site_module/renderjs_runner/gadget_erp5_pt_form_view.html']</td>
<td></td>
</tr>
<tr>
<td>waitForTextPresent</td>
<td>Title 1</td>
<td>//div[@data-gadget-url='${base_url}/web_site_module/renderjs_runner/gadget_erp5_field_listbox.html']</td>
<td></td>
</tr>
<!-- Header has a link to the export page
Here, we assume that portal_types/Bar has no Object JIO Report Action -->
<!-- There is a Print action for every Document except Modules so we test on modules -->
<tr>
<td>waitForElementPresent</td>
<td>//div[@data-gadget-scope='header']//a[text()='Export' and contains(@class, 'ui-disabled')]</td>
......
<?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.'}">
<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
......@@ -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>
# Return first listbox in a form that is enabled and not hidden
# Christophe Dumez <christophe@nexedi.com>
# This script should be used to detect a listbox without having to name it "listbox"
if form is None:
form=context
if form.meta_type != 'ERP5 Form':
return None
# XXX We should not use meta_type properly,
# XXX We need to discuss this problem.(yusei)
def isListBox(field):
if field.meta_type=='ListBox':
return True
elif field.meta_type=='ProxyField':
template_field = field.getRecursiveTemplateField()
if template_field.meta_type=='ListBox':
return True
return False
# we start with 'bottom' because most of the time
# the listbox is there.
for group in ('bottom', 'center', 'left', 'right'):
for field in form.get_fields_in_group(group):
if isListBox(field) and not(field['hidden']) and field['enabled']:
return field
"""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 fixit: {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:
fixit = int(fixit)
except ValueError:
fixit = 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_redirect(keep_items={'portal_status_message': translate("Sorry, your selection has changed.")})
if object_used > 0:
if object_used == 1:
message = Base_translateString("Sorry, 1 item is in use.")
else:
message = Base_translateString("Sorry, ${count} items are in use.",
mapping={'count': repr(object_used)})
if not uids:
return context.Base_redirect(keep_items={
'portal_status_message': translate("Please select one or more items first."),
'portal_status_level': "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 fixit 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_renderForm(dialog_id,
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:
# 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 fixit=1 flag
return context.Base_renderForm(dialog_id,
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',
keep_items={'fixit': 1}
)
if fixit 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 +100,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})
......@@ -70,7 +116,8 @@ elif uids:
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
for object in object_to_delete_list:
# Hidden transition (without a message displayed)
......@@ -82,18 +129,8 @@ 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
if 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 @@
</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=\'\',dialog_id=\'\',selection_index=None,object_uid=None,selection_name=None,field_id=None,cancel_url=\'\',md5_object_uid_list=\'\', uids=(), fixit=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
This script should be used to detect a listbox without having to name it "listbox".
:param form: {Form} optional Form instance instead of calling this script directly on a Form
:param form_id: {str} if specified the Script must be called on currently traversed document
Christophe Dumez <christophe@nexedi.com>
"""
def isListBox(field):
if field.meta_type == "ListBox":
return True
elif field.meta_type == "ProxyField":
template_field = field.getRecursiveTemplateField()
if template_field.meta_type == "ListBox":
return True
return False
if form_id is not None:
form = getattr(context, form_id)
if form is None:
form = context
if form.meta_type not in ('ERP5 Form', 'Folder', 'ERP5 Folder'):
raise RuntimeError("Cannot get Listbox field from \"{!s}\"! Supported is only ERP5 Form and (ERP5) Folder".format(form.meta_type))
if form.has_field('listbox'):
return form.get_field('listbox')
# we start with 'bottom' because most of the time
# the listbox is there.
for group in ('bottom', 'center', 'left', 'right'):
for field in form.get_fields_in_group(group):
if (isListBox(field) and
not field.get_value('hidden') and
field.get_value('enabled')):
return field
......@@ -50,11 +50,11 @@
</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>
<value> <string>ERP5Site_getListbox</string> </value>
<value> <string>Form_getListbox</string> </value>
</item>
</dictionary>
</pickle>
......
"""Render form while keeping its values back to user.
This script differs from Base_redirect that it keeps the form values in place.
:param message: {str} message to be displayed at the user
:param level: {str|int} is ignored in XHTML style - no support for message level distinction
:param keep_items: {dict} items to be available in the next call. They will be either added as hidden fields to the
rendered form or in case of "portal_status_message" just displayed to the user
:param REQUEST: request
:param **kwargs: is used to pass necessary parameters to overcome backend-held state (aka Selections)
"""
keep_items = keep_items or {}
if message and "portal_status_message" not in keep_items:
keep_items["portal_status_message"] = message
keep_items.pop("portal_status_level", None)
if REQUEST is None:
REQUEST = context.REQUEST
for key, value in keep_items.items():
REQUEST.set(key, value)
return getattr(context, form_id)()
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>form_id</string> </value>
<value> <string>form_id, message=\'\', level=None, keep_items=None, REQUEST=None, **kwargs</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>
......@@ -491,7 +491,13 @@ class TestERP5Core(ERP5TypeTestCase, ZopeTestCase.Functional):
portal_selections.setSelectionCheckedUidsFor(selection_name,
[x.getUid() for x in object_list])
md5_string = portal_selections.getSelectionChecksum(selection_name)
return object_list[0].getParentValue().Folder_delete(
object_parent = object_list[0].getParentValue()
# get default form from default view for given context
default_view_url = str(self.portal.portal_actions.listFilteredActionsFor(object_parent)['object_view'][0]['url'])
form_id = default_view_url.split('?', 1)[0].split("/")[-1]
return object_parent.Folder_delete(form_id=form_id, dialog_id="Folder_viewDeleteDialog",
selection_name=selection_name, md5_object_uid_list=md5_string)
def test_Folder_delete(self):
......@@ -499,8 +505,7 @@ class TestERP5Core(ERP5TypeTestCase, ZopeTestCase.Functional):
document_1 = module.newContent(portal_type='Folder', id='1')
document_2 = module.newContent(portal_type='Folder', id='2')
self.tic()
redirect = self._Folder_delete(document_1, document_2)
self.assert_('Deleted.' in redirect, redirect)
self._Folder_delete(document_1, document_2)
self.assertEqual(module.objectCount(), 0)
def test_Folder_delete_related_object(self):
......@@ -508,6 +513,7 @@ class TestERP5Core(ERP5TypeTestCase, ZopeTestCase.Functional):
organisation_module_len = len(self.portal.organisation_module)
person_module_len = len(self.portal.person_module)
organisation = self.portal.organisation_module.newContent()
initial_organisation_state = organisation.getValidationState()
person = self.portal.person_module.newContent(
default_career_subordination_value=organisation)
for obj in person, organisation:
......@@ -517,15 +523,20 @@ class TestERP5Core(ERP5TypeTestCase, ZopeTestCase.Functional):
self.tic()
self.assertEqual(2, organisation.getRelationCountForDeletion())
self.assertEqual(0, person.getRelationCountForDeletion())
def delete(assert_deleted, obj):
redirect = self._Folder_delete(obj)
self.assertTrue((urllib.quote('Sorry, 1 item is in use.'), 'Deleted.')[assert_deleted]
in redirect, redirect)
self._Folder_delete(organisation)
self.tic()
# here we check that nothing was done because the organisation has relations
refreshed_organisation = self.portal.organisation_module[organisation.id]
assert initial_organisation_state == refreshed_organisation.getValidationState()
self._Folder_delete(person)
self.tic()
delete(0, organisation)
delete(1, person)
self.assertEqual(0, organisation.getRelationCountForDeletion())
delete(1, organisation)
self._Folder_delete(organisation)
self.tic()
self.assertEqual(organisation_module_len + 1,
len(self.portal.organisation_module))
self.assertEqual(person_module_len + 1,
......@@ -547,8 +558,7 @@ class TestERP5Core(ERP5TypeTestCase, ZopeTestCase.Functional):
document_1.manage_permission('View', [], acquire=0)
document_1.manage_permission('Access contents information', [], acquire=0)
redirect = self._Folder_delete(document_2)
self.assert_(urllib.quote('Sorry, 1 item is in use.') in redirect, redirect)
self._Folder_delete(document_2)
self.assertEqual(module.objectCount(), 2)
def test_getPropertyForUid(self):
......
......@@ -26,6 +26,7 @@
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import hashlib
from copy import deepcopy
......@@ -766,6 +767,15 @@ class ERP5Form(Base, ZMIForm, ZopePageTemplate):
raise FormValidationError(errors, result)
return result
security.declareProtected('View', 'hash_validated_data')
def hash_validated_data(self, validated_data):
return hashlib.sha256(
"".join(
str(validated_data[key])
for key in sorted(validated_data.keys())
if isinstance(validated_data[key], (str, unicode, int, long, float, DateTime)))
).hexdigest()
# FTP/DAV Access
manage_FTPget = ZMIForm.get_xml
......
......@@ -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