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

[renderjs] Add support for Reporting

-  hal_json works minimally (but still) with Selections
-  ERP5 Report gets rendered
-  display Title of Forms
parent ff55ef9e
......@@ -78,6 +78,43 @@ def byteify(string):
else:
return string
def ensureSerializable(obj):
"""Ensure obj and all sub-objects are JSON serializable."""
if isinstance(obj, dict):
for key in obj:
obj[key] = ensureSerializable(obj[key])
# throw away date's type information and later reconstruct as Zope's DateTime
if isinstance(obj, DateTime):
return obj.ISO()
if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
return obj.isoformat()
# let us believe that iterables don't contain other unserializable objects
return obj
datetime_iso_re = re.compile(r'^\d{4}-\d{2}-\d{2} |T\d{2}:\d{2}:\d{2}.*$')
time_iso_re = re.compile(r'^(\d{2}):(\d{2}):(\d{2}).*$')
def ensureDeserialized(obj):
"""Deserialize classes serialized by our own `ensureSerializable`.
Method `biteify` must not be called on the result because it would revert out
deserialization by calling __str__ on constructed classes.
"""
if isinstance(obj, dict):
for key in obj:
obj[key] = ensureDeserialized(obj[key])
# seems that default __str__ method is good enough
if isinstance(obj, str):
# Zope's DateTime must be good enough for everyone
if datetime_iso_re.match(obj):
return DateTime(obj)
if time_iso_re.match(obj):
match_obj = time_iso_re.match(obj)
return datetime.time(*tuple(map(int, match_obj.groups())))
return obj
def getProtectedProperty(document, select):
"""getProtectedProperty is a security-aware substitution for builtin `getattr`
......@@ -324,7 +361,7 @@ def getRealRelativeUrl(document):
def getFormRelativeUrl(form):
return portal.portal_catalog(
portal_type="ERP5 Form",
portal_type=("ERP5 Form", "ERP5 Report"),
uid=form.getUid(),
id=form.getId(),
limit=1,
......@@ -359,6 +396,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if key is None:
key = field.generate_field_key(key_prefix=key_prefix)
if meta_type == "ProxyField":
# resolve the base meta_type
meta_type = field.getRecursiveTemplateField().meta_type
result = {
"type": meta_type,
"title": Base_translateString(field.get_value("title")),
......@@ -376,12 +417,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"default": getFieldDefault(form, field, key, value),
})
if meta_type == "ProxyField":
return renderField(traversed_document, field, form, value,
meta_type=field.getRecursiveTemplateField().meta_type,
key=key, key_prefix=key_prefix,
selection_params=selection_params)
# start the actual "switch" on field's meta_type here
if meta_type in ("ListField", "RadioField", "ParallelListField", "MultiListField"):
result.update({
# XXX Message can not be converted to json as is
......@@ -427,6 +463,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if v))
if parameters:
result["default"] = '%s?%s' % (result["default"], parameters)
return result
if meta_type == "DateTimeField":
......@@ -528,7 +565,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if rel_cache[key] is not MARKER:
REQUEST.set(key, rel_cache[key])
result.update({
"url": relative_url,
"translated_portal_types": translated_portal_type,
......@@ -569,12 +605,21 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
return result
if meta_type == "ListBox":
"""Display list of objects with optional search/sort capabilities on columns from catalog."""
"""Display list of objects with optional search/sort capabilities on columns from catalog.
We might be inside a ReportBox which is inside a parent form BUT we still have access to
the original REQUEST with sent POST values from the parent form. We can save those
values into our query method and reconstruct them meanwhile calling asynchronous jio.allDocs.
"""
_translate = Base_translateString
column_list = [(name, _translate(title)) for name, title in field.get_value("columns")]
# column definition in ListBox own value 'columns' is superseded by dynamic
# column definition from Selection for specific Report ListBoxes; the same for editable_columns
column_list = [(name, _translate(title)) for name, title in (selection_params.get('selection_columns', [])
or field.get_value("columns"))]
editable_column_list = [(name, _translate(title)) for name, title in (selection_params.get('editable_columns', [])
or field.get_value("editable_columns"))]
all_column_list = [(name, _translate(title)) for name, title in field.get_value("all_columns")]
editable_column_list = [(name, _translate(title)) for name, title in field.get_value("editable_columns")]
catalog_column_list = [(name, title)
for name, title in OrderedDict(column_list + all_column_list).items()
if sql_catalog.isValidColumn(name)]
......@@ -586,25 +631,28 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# try to get specified sortable columns and fail back to searchable fields
sort_column_list = [(name, _translate(title))
for name, title in field.get_value("sort_columns")
for name, title in (selection_params.get('selection_sort_order', [])
or field.get_value("sort_columns"))
if sql_catalog.isValidColumn(name)] or search_column_list
# 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
# see https://lab.nexedi.com/nexedi/erp5/blob/master/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.js#L163
portal_types = field.get_value('portal_types')
default_params = dict(field.get_value('default_params'))
default_params = dict(field.get_value('default_params')) # default_params is a list of tuples
default_params['ignore_unknown_columns'] = True
if selection_params is not None:
# we abandoned Selections in RJS thus we mix selection query parameters into
# listbox's default parameters
default_params.update(selection_params)
# How to implement pagination?
# default_params.update(REQUEST.form)
lines = field.get_value('lines')
list_method_query_dict = dict(
portal_type=[x[1] for x in portal_types], **default_params
)
# ListBoxes in report view has portal_type defined already in default_params
# in that case we prefer non_empty version
list_method_query_dict = default_params.copy()
if not list_method_query_dict.get("portal_type", []):
list_method_query_dict["portal_type"] = [x for x, _ in portal_type_list]
list_method_custom = None
# Search for non-editable documents - all reports goes here
......@@ -646,8 +694,11 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id),
"list_method": list_method_name,
"default_param_json": urlsafe_b64encode(json.dumps(list_method_query_dict))
"default_param_json": urlsafe_b64encode(
json.dumps(ensureSerializable(list_method_query_dict)))
}
# once we imprint `default_params` into query string of 'list method' we
# don't want them to propagate to the query as well
list_method_query_dict = {}
"""
# We commented out this part because of backward compatibility
......@@ -663,7 +714,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"script_id": script.id,
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"list_method": list_method_name,
"default_param_json": urlsafe_b64encode(json.dumps(list_method_query_dict))
"default_param_json": urlsafe_b64encode(json.dumps(ensureSerializable(list_method_query_dict)))
}
list_method_query_dict = {}
"""
......@@ -696,9 +747,9 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"sort_column_list": sort_column_list,
"editable_column_list": editable_column_list,
"show_anchor": field.get_value("anchor"),
"portal_type": portal_types,
"lines": lines,
"default_params": default_params,
"portal_type": portal_type_list,
"lines": field.get_value('lines'),
"default_params": ensureSerializable(default_params),
"list_method": list_method_name,
"show_stat": field.get_value('stat_method') != "" or len(field.get_value('stat_columns')) > 0,
"show_count": field.get_value('count_method') != "",
......@@ -848,26 +899,84 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
}
if (form.pt == 'report_view'):
# reports are expected to return list of ReportSection which is a wrapper
# around a form - thus we will need to render those forms
report_item_list = []
report_result_list = []
for field in form.get_fields():
if field.getRecursiveTemplateField().meta_type == 'ReportBox':
# ReportBox.render returns a list of ReportSection classes which are
# just containers for FormId(s) usually containing one ListBox
# and its search/query parameters hidden in `selection_params`
# `path` contains relative_url of intended CONTEXT for underlaying ListBox
report_item_list.extend(field.render())
j = 0
for report_item in report_item_list:
report_context = report_item.getObject(portal)
report_prefix = 'x%s' % j
j += 1
# ERP5 Report document differs from a ERP5 Form in only one thing: it has
# `report_method` attached to it - thus we call it right here
if hasattr(form, 'report_method') and getattr(form, 'report_method', ""):
report_method_name = getattr(form, 'report_method')
report_method = getattr(traversed_document, report_method_name)
report_item_list.extend(report_method())
for report_index, report_item in enumerate(report_item_list):
report_context = report_item.getObject(traversed_document)
report_prefix = 'x%s' % report_index
report_title = report_item.getTitle()
# report_class = "report_title_level_%s" % report_item.getLevel()
report_form = report_item.getFormId()
report_result = {'_links': {}}
renderForm(traversed_document, getattr(report_context, report_item.getFormId()),
report_result, key_prefix=report_prefix,
selection_params=report_item.selection_params)
# some reports save a lot of unserializable data (datetime.datetime) and
# key "portal_type" (don't confuse with "portal_types" in ListBox) into
# report_item.selection_params thus we need to take that into account in
# ListBox field
#
# Selection Params are parameters for embedded ListBox's List Method
# and it must be passed in `default_json_param` field (might contain
# unserializable data types thus we need to take care of that
# In order not to lose information we put all ReportSection attributes
# inside the report selection params
report_form_params = report_item.selection_params.copy() \
if report_item.selection_params is not None \
else {}
if report_item.selection_name:
selection_name = report_prefix + "_" + report_item.selection_name
report_form_params.update(selection_name=selection_name)
# this should load selections with correct values - since it is modifying
# global state in the backend we have nothing more to do here
# I could not find where the code stores params in selection with render
# prefix - maybe it in some `render` method where it should not be
# Of course it is ugly, terrible and should be removed!
selection_tool = context.getPortalObject().portal_selections
selection_tool.getSelectionFor(selection_name, REQUEST)
selection_tool.setSelectionParamsFor(selection_name, report_form_params)
selection_tool.setSelectionColumns(selection_name, report_item.selection_columns)
if report_item.selection_columns:
report_form_params.update(selection_columns=report_item.selection_columns)
if report_item.selection_sort_order:
report_form_params.update(selection_sort_order=report_item.selection_sort_order)
# Report section is just a wrapper around form thus we render it right
# we keep traversed_document because its Portal Type Class should be
# addressable by the user = have actions (object_view) attached to it
# BUT! when Report Section defines `path` that is the new context for
# form rendering and subsequent searches...
renderForm(traversed_document if not report_item.path else report_context,
getattr(report_context, report_item.getFormId()),
report_result,
key_prefix=report_prefix,
selection_params=report_form_params) # used to be only report_item.selection_params
# Report Title is important since there are more section on report page
# but often they render the same form with different data so we need to
# distinguish by the title at least.
report_result['title'] = report_title
report_result_list.append(report_result)
response_dict['report_section_list'] = report_result_list
# end-if report_section
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):
......@@ -910,6 +1019,7 @@ def renderRawField(field):
def renderFormDefinition(form, response_dict):
"""Form "definition" is configurable in Zope admin: Form -> Order."""
group_list = []
for group in form.Form_getGroupTitleAndId():
......@@ -1031,11 +1141,15 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# 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
# thus attached actions to it so it is viewable
document_type_name = traversed_document.getPortalType()
document_type = getattr(portal.portal_types, document_type_name, None)
if document_type is not None:
result_dict['_links']['type'] = {
"href": default_document_uri_template % {
"root_url": site_root.absolute_url(),
"relative_url": portal.portal_types[traversed_document.getPortalType()]\
.getRelativeUrl(),
"relative_url": document_type.getRelativeUrl(),
"script_id": script.id
},
"name": Base_translateString(traversed_document.getPortalType())
......@@ -1060,6 +1174,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
portal.portal_actions.listFilteredActionsFor(traversed_document))
embedded_url = None
# XXX See ERP5Type.getDefaultViewFor
for erp5_action_key in erp5_action_dict.keys():
erp5_action_list = []
......@@ -1151,7 +1266,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# 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)
# traversed_document.log(form_id)
if (renderer_form is not None):
embedded_dict = {
'_links': {
......@@ -1161,12 +1275,24 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
}
}
# 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("=")
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.set(query_key, query_value)
# unfortunatelly some people use Scripts as targets for Workflow
# transactions - thus we need to check and mitigate
if "Script" in renderer_form.meta_type:
# we suppose that the script takes only what is given in the URL params
return renderer_form(**query_param_dict)
renderForm(traversed_document, renderer_form, embedded_dict)
result_dict['_embedded'] = {
'_view': embedded_dict
......@@ -1255,7 +1381,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
else:
traversed_document_portal_type = traversed_document.getPortalType()
if traversed_document_portal_type == "ERP5 Form":
if traversed_document_portal_type in ("ERP5 Form", "ERP5 Report"):
renderFormDefinition(traversed_document, result_dict)
response.setHeader("Cache-Control", "private, max-age=1800")
response.setHeader("Vary", "Cookie,Authorization,Accept-Encoding")
......@@ -1283,14 +1409,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if action_dict:
result_dict['_actions'] = action_dict
elif mode == 'search':
#################################################
# Portal catalog search
#
# Possible call arguments example:
# form_relative_url: portal_skins/erp5_web/WebSite_view/listbox
# list_method: objectValues (Script providing listing)
# list_method: "objectValues" (Script providing items)
# default_param_json: <base64 encoded JSON> (Additional search params)
# query: <str> (term for fulltext search)
# select_list: ['int_index', 'id', 'title', ...] (column names to select)
......@@ -1337,9 +1462,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
"sort_on": () # default is an empty tuple
}
if default_param_json is not None:
catalog_kw.update(byteify(json.loads(urlsafe_b64decode(default_param_json))))
catalog_kw.update(
ensureDeserialized(
byteify(
json.loads(urlsafe_b64decode(default_param_json)))))
if query:
catalog_kw["full_text"] = query
if sort_on is not None:
def parseSortOn(raw_string):
"""Turn JSON serialized array into a tuple (col_name, order)."""
......@@ -1452,6 +1581,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
contents_uid, contents_relative_url, property_getter = \
getUidAndAccessorForAnything(search_result, result_index, traversed_document)
# Check if this object provides a specific URL method.
# if getattr(search_result, 'getListItemUrl', None) is not None:
# search_result.getListItemUrl(contents_uid, result_index, selection_name)
# _links.self.href is mandatory for JIO so it can create reference to the
# (listbox) item alone
contents_item['_links'] = {
......@@ -1503,7 +1636,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# endfor select
REQUEST.other.pop('cell', None)
contents_list.append(contents_item)
result_dict['_embedded']['contents'] = contents_list
result_dict['_embedded']['contents'] = ensureSerializable(contents_list)
# Compute statistics if the search issuer was ListBox
# or in future if the stats (SUM) are required by JIO call
......@@ -1562,7 +1695,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
traversed_document, editable_field_dict[key], listbox_form, value, key=editable_field_dict[key].id + '__sum')
if len(contents_stat_list) > 0:
result_dict['_embedded']['sum'] = contents_stat_list
result_dict['_embedded']['sum'] = ensureSerializable(contents_stat_list)
# We should cleanup the selection if it exists in catalog params BUT
# we cannot because it requires escalated Permission.'modifyPortal' so
......
......@@ -9,8 +9,11 @@ from functools import wraps
from ZPublisher.HTTPRequest import HTTPRequest
from ZPublisher.HTTPResponse import HTTPResponse
import base64
import DateTime
import StringIO
import json
import re
import urllib
def changeSkin(skin_name):
......@@ -639,6 +642,42 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_embedded']['_view']['_actions']['put']['method'], 'POST')
@simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs',
'return "application/hal+json"')
@changeSkin('Hal')
def test_getHateoasDocument_listbox_vs_relation_inconsistency(self):
"""Purpose of this test is to point to inconsistencies in search-enabled field rendering.
ListBox gets its Portal Types in `portal_type` as list of tuples whether
Relation Input receives `portal_types` and `translated_portal_types`
"""
document = self._makeDocument()
# Drop editable permission
document.manage_permission('Modify portal content', [], 0)
document.Foo_view.listbox.ListBox_setPropertyList(
field_title = 'Foo Lines',
field_list_method = 'objectValues',
field_portal_types = 'Foo Line | Foo Line',
)
fake_request = do_fake_request("GET")
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request,
mode="traverse",
relative_url=document.getRelativeUrl(),
view="view")
self.assertEquals(fake_request.RESPONSE.status, 200)
self.assertEquals(fake_request.RESPONSE.getHeader('Content-Type'),
"application/hal+json"
)
result_dict = json.loads(result)
# ListBox rendering of allowed Portal Types
self.assertEqual(result_dict['_embedded']['_view']['listbox']['portal_type'], [['Foo Line', 'Foo Line']])
# Relation Input rendering of allowed Portal Types
self.assertEqual(result_dict['_embedded']['_view']['my_foo_category_title']['portal_types'], ['Category'])
self.assertEqual(result_dict['_embedded']['_view']['my_foo_category_title']['translated_portal_types'], ['Category'])
@simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs',
......@@ -712,6 +751,71 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertFalse(result_dict['_embedded']['_view'].has_key('_actions'))
@simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs',
'return "application/hal+json"')
@changeSkin('Hal')
def test_getHateoasDocument_listbox_list_method_params(self):
"""Ensure that `list_method` of ListBox receives specified parameters."""
document = self._makeDocument()
document.manage_permission('Modify portal content', [], 0)
# pass custom list method which expect input arguments
document.Foo_view.listbox.ListBox_setPropertyList(
field_title = 'Foo Lines',
field_list_method = 'Foo_listWithInputParams',
field_portal_types = 'Foo Line | Foo Line',
field_columns = 'id|ID\ntitle|Title\nquantity|Quantity\nstart_date|Date\ncatalog.uid|Uid')
now = DateTime.DateTime()
tomorrow = now + 1
fake_request = do_fake_request("GET", data=(
('start_date', now.ISO()),
('stop_date', tomorrow.ISO()))
)
# I tried to implement the standard way (see `data` param in do_fake_request)
# but for some reason it does not work...so we hack our way around
fake_request.set('start_date', now.ISO())
fake_request.set('stop_date', tomorrow.ISO())
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request,
mode="traverse",
relative_url=document.getRelativeUrl(),
form=document.restrictedTraverse('portal_skins/erp5_ui_test/Foo_view'),
view="view"
)
self.assertEquals(fake_request.RESPONSE.status, 200)
self.assertEquals(fake_request.RESPONSE.getHeader('Content-Type'),
"application/hal+json"
)
result_dict = json.loads(result)
list_method_template = \
result_dict['_embedded']['_view']['listbox']['list_method_template']
# default_param_json must not be empty because our custom list method
# specifies input parameters - they need to be filled from REQUEST
self.assertIn('default_param_json', list_method_template)
default_param_json = json.loads(
base64.b64decode(
re.search(r'default_param_json=([^\{&]+)',
list_method_template).group(1)))
self.assertIn("start_date", default_param_json)
self.assertEqual(default_param_json["start_date"], now.ISO())
self.assertIn("stop_date", default_param_json)
self.assertEqual(default_param_json["stop_date"], tomorrow.ISO())
# reset listbox properties to defaults
document.Foo_view.listbox.ListBox_setPropertyList(
field_title = 'Foo Lines',
field_list_method = 'objectValues',
field_portal_types = 'Foo Line | Foo Line',
field_stat_method = 'portal_catalog',
field_stat_columns = 'quantity | Foo_statQuantity',
field_editable = 1,
field_columns = 'id|ID\ntitle|Title\nquantity|Quantity\nstart_date|Date\ncatalog.uid|Uid',
field_editable_columns = 'id|ID\ntitle|Title\nquantity|quantity\nstart_date|Date',
field_search_columns = 'id|ID\ntitle|Title\nquantity|Quantity\nstart_date|Date',)
@simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs',
......@@ -1208,7 +1312,6 @@ class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin):
self.assertEquals(fake_request.RESPONSE.status, 405)
self.assertEquals(result, "")
@simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs',
......@@ -1347,6 +1450,7 @@ class TestERP5Document_getHateoas_mode_worklist(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_debug'], "worklist")
class TestERP5Document_getHateoas_translation(ERP5HALJSONStyleSkinsMixin):
code_string = "\
from Products.CMFCore.utils import getToolByName\n\
......@@ -1428,8 +1532,7 @@ return msg"
code_string)
@createIndexedDocument()
@changeSkin('Hal')
def test_getHateoasWorklist_default_view_translation(self, **kw):
# self._makeDocument()
def test_getHateoasWorklist_default_view_translation(self, document):
fake_request = do_fake_request("GET")
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request,
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_view</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_view</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string>Form with hidden quantity field with external validator asserting positiveness of the value. Used to test behaviour of errors on hidden fields.</string> </value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view_hidden_positive_only_quantity</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>10.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View Hidden Positive-Only Quantity</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}/Foo_viewHiddenErrorneousField</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
"""Foo_listWithInputParams is here only to test passing parameters from REQUEST via introspection in RenderJS UI.
We expect DateTime parameters thus they have to undergo a serialization/deserialization process.
"""
from DateTime import DateTime
assert isinstance(start_date, DateTime), "start_date is instance of {!s} instead of DateTime!".format(type(start_date))
assert isinstance(stop_date, DateTime), "stop_date is instance of {!s} instead of DateTime!".format(type(stop_date))
return context.listFolder(portal_type='Foo Line')
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Python Script" module="erp5.portal_type"/>
</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>start_date, stop_date=None, **kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Foo_listWithInputParams</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Python Script</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_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/>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Base_edit</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string>Display some integers field for selenium tests</string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list>
<string>my_quantity</string>
<string>read_only_quantity</string>
</list>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Foo_viewHiddenErrorneousField</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>Foo_view</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_view</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Foo</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="IntegerField" module="Products.Formulator.StandardFields"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>my_quantity</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
<item>
<key> <string>integer_out_of_range</string> </key>
<value> <string>The integer you entered was out of range.</string> </value>
</item>
<item>
<key> <string>not_integer</string> </key>
<value> <string>You did not enter an integer.</string> </value>
</item>
<item>
<key> <string>required_not_found</string> </key>
<value> <string>Input is required but no input given.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>end</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>start</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>end</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>start</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <int>20</int> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>end</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>start</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Quantity</string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <int>0</int> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Method" module="Products.Formulator.MethodField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>method_name</string> </key>
<value> <string>Validator_positiveNumber</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="IntegerField" module="Products.Formulator.StandardFields"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>read_only_quantity</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
<item>
<key> <string>integer_out_of_range</string> </key>
<value> <string>The integer you entered was out of range.</string> </value>
</item>
<item>
<key> <string>not_integer</string> </key>
<value> <string>You did not enter an integer.</string> </value>
</item>
<item>
<key> <string>required_not_found</string> </key>
<value> <string>Input is required but no input given.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>end</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>start</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>end</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>start</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <int>20</int> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>end</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>start</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Read-Only Quantity</string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <int>0</int> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<tuple>
<tuple>
<string>Products.Formulator.TALESField</string>
<string>TALESMethod</string>
</tuple>
<none/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: here.getQuantity()</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -30,6 +30,7 @@ Foo | view_duration_field
Foo | view_formbox
Foo | view_formbox_dialog
Foo | view_formbox_fooline
Foo | view_hidden_positive_only_quantity
Foo | view_listbox
Foo | view_multiple_listbox
Foo | view_planning
......
......@@ -171,12 +171,29 @@
form_definition = this.state.form_definition,
rendered_document = erp5_document._embedded._view,
group_list = form_definition.group_list,
form_gadget = this;
form_gadget = this,
tmp;
if (modification_dict.hasOwnProperty('hash')) {
form_gadget.props.gadget_list = [];
}
/* Update or remove h3 element based on value of `title` */
if (modification_dict.hasOwnProperty('title')) {
tmp = this.element.querySelector("h3");
if (modification_dict.title) {
if (tmp === null) {
// create new title element for existing title
tmp = document.createElement("h3");
this.element.insertBefore(tmp, this.element.firstChild);
}
tmp.textContent = modification_dict.title;
}
if (modification_dict.title === null || modification_dict.title === "") {
// user tends to remove the title
if (tmp !== null) {tmp.remove(); }
}
tmp = undefined;
}
return new RSVP.Queue()
.push(function () {
return RSVP.all(group_list.map(function (group) {
......
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>963.11788.48702.26146</string> </value>
<value> <string>964.25533.41108.47530</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1514393621.04</float>
<float>1515496577.67</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -5,6 +5,7 @@
/** Return true if `field` resembles non-empty and non-editable field. */
function isGoodNonEditableField(field) {
if (field === undefined || field === null) {return false; }
// ListBox and FormBox should always get a chance to render because they
// can contain editable fields
if (field.type === "ListBox") {return true; }
......@@ -47,6 +48,7 @@
.declareMethod('render', function (options) {
var state_dict = {
jio_key: options.jio_key,
title: options.title,
view: options.view,
editable: options.editable,
erp5_document: options.erp5_document,
......@@ -80,6 +82,7 @@
form_options.erp5_document = gadget.state.erp5_document;
form_options.form_definition = gadget.state.form_definition;
form_options.view = gadget.state.view;
form_options.title = gadget.state.title;
form_options.jio_key = gadget.state.jio_key;
form_options.editable = 0; // because for editable=1 there is a special
// page template 'pt_form_editable'. Once it is
......@@ -96,7 +99,7 @@
gadget.getUrlFor({command: 'selection_previous'}),
gadget.getUrlFor({command: 'selection_next'}),
gadget.getUrlFor({command: 'change', options: {page: "tab"}}),
gadget.state.erp5_document._links.action_object_jio_report ?
gadget.state.erp5_document._links.action_object_jio_report || gadget.state.erp5_document._links.action_object_print ?
gadget.getUrlFor({command: 'change', options: {page: "export"}}) :
"",
calculatePageTitle(gadget, gadget.state.erp5_document)
......
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>964.44232.19748.18107</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>1515406785.95</float>
<float>1515593717.52</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -33,7 +33,7 @@
};
return form_gadget.render({erp5_document: erp5_document,
form_definition: form_definition,
editable: 0});
editable: 0, title: report_section.title});
});
}
......
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>961.16421.12334.2201</string> </value>
<value> <string>962.56167.53905.31470</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1502116518.17</float>
<float>1508400391.84</float>
<string>UTC</string>
</tuple>
</state>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testHiddenFieldError</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>
<head><title>Test Invoices Report Skin Allowance</title></head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><th rowspan="1" colspan="4">
Check that user gets notified if there is an error on a hidden field.
</th></tr>
</thead>
<tbody>
<tal:block metal:use-macro="here/PTZuite_CommonTemplate/macros/init" />
<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/1/?editable=1</td><td></td></tr>
<!-- Originaly the field was required and we tested here an empty value. Problem is that Firefox
evaluates numerical rule before required value wheras Chrome does it in the opposite direction -->
<!-- Put negative quantity so the external validator will not pass external test in the next view -->
<tr><td>waitForElementPresent</td>
<td>//input[@name="field_my_quantity"]</td><td></td></tr>
<tr><td>type</td>
<td>//input[@name="field_my_quantity"]</td>
<td>-20</td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/save" />
<!-- Let the external validator throw an error - this time we test explicitely
for a notification with the error -->
<tr><td>waitForElementPresent</td>
<td>//a[@data-i18n="Views"]</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n="Views"]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//a[@data-i18n="View Hidden Positive-Only Quantity"]</td><td></td></tr>
<tr><td>click</td>
<td>//a[@data-i18n="View Hidden Positive-Only Quantity"]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//button[@data-i18n='Save']</td><td></td></tr>
<tr><td>click</td>
<td>//button[@data-i18n='Save']</td><td></td></tr>
<tr><td>waitForTextPresent</td>
<td>The input failed the external validator.</td><td></td></tr>
</tbody>
</table>
</body>
</html>
\ No newline at end of file
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