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): ...@@ -78,6 +78,43 @@ def byteify(string):
else: else:
return string 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): def getProtectedProperty(document, select):
"""getProtectedProperty is a security-aware substitution for builtin `getattr` """getProtectedProperty is a security-aware substitution for builtin `getattr`
...@@ -324,7 +361,7 @@ def getRealRelativeUrl(document): ...@@ -324,7 +361,7 @@ def getRealRelativeUrl(document):
def getFormRelativeUrl(form): def getFormRelativeUrl(form):
return portal.portal_catalog( return portal.portal_catalog(
portal_type="ERP5 Form", portal_type=("ERP5 Form", "ERP5 Report"),
uid=form.getUid(), uid=form.getUid(),
id=form.getId(), id=form.getId(),
limit=1, limit=1,
...@@ -359,6 +396,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -359,6 +396,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if key is None: if key is None:
key = field.generate_field_key(key_prefix=key_prefix) 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 = { result = {
"type": meta_type, "type": meta_type,
"title": Base_translateString(field.get_value("title")), "title": Base_translateString(field.get_value("title")),
...@@ -376,12 +417,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -376,12 +417,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"default": getFieldDefault(form, field, key, value), "default": getFieldDefault(form, field, key, value),
}) })
if meta_type == "ProxyField": # start the actual "switch" on field's meta_type here
return renderField(traversed_document, field, form, value,
meta_type=field.getRecursiveTemplateField().meta_type,
key=key, key_prefix=key_prefix,
selection_params=selection_params)
if meta_type in ("ListField", "RadioField", "ParallelListField", "MultiListField"): if meta_type in ("ListField", "RadioField", "ParallelListField", "MultiListField"):
result.update({ result.update({
# XXX Message can not be converted to json as is # 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 ...@@ -427,6 +463,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if v)) if v))
if parameters: if parameters:
result["default"] = '%s?%s' % (result["default"], parameters) result["default"] = '%s?%s' % (result["default"], parameters)
return result return result
if meta_type == "DateTimeField": if meta_type == "DateTimeField":
...@@ -528,7 +565,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -528,7 +565,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if rel_cache[key] is not MARKER: if rel_cache[key] is not MARKER:
REQUEST.set(key, rel_cache[key]) REQUEST.set(key, rel_cache[key])
result.update({ result.update({
"url": relative_url, "url": relative_url,
"translated_portal_types": translated_portal_type, "translated_portal_types": translated_portal_type,
...@@ -569,12 +605,21 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -569,12 +605,21 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
return result return result
if meta_type == "ListBox": 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 _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")] 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) catalog_column_list = [(name, title)
for name, title in OrderedDict(column_list + all_column_list).items() for name, title in OrderedDict(column_list + all_column_list).items()
if sql_catalog.isValidColumn(name)] if sql_catalog.isValidColumn(name)]
...@@ -586,25 +631,28 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -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 # try to get specified sortable columns and fail back to searchable fields
sort_column_list = [(name, _translate(title)) 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 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 # 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 # see https://lab.nexedi.com/nexedi/erp5/blob/HEAD/product/ERP5Form/ListBox.py#L1004
# implemented in javascript in the end # 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 # 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
default_params = dict(field.get_value('default_params')) # default_params is a list of tuples
portal_types = field.get_value('portal_types')
default_params = dict(field.get_value('default_params'))
default_params['ignore_unknown_columns'] = True 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) default_params.update(selection_params)
# How to implement pagination?
# default_params.update(REQUEST.form) # ListBoxes in report view has portal_type defined already in default_params
lines = field.get_value('lines') # in that case we prefer non_empty version
list_method_query_dict = dict( list_method_query_dict = default_params.copy()
portal_type=[x[1] for x in portal_types], **default_params 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 list_method_custom = None
# Search for non-editable documents - all reports goes here # 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 ...@@ -646,8 +694,11 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"), "relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id), "form_relative_url": "%s/%s" % (getFormRelativeUrl(form), field.id),
"list_method": list_method_name, "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 = {} list_method_query_dict = {}
""" """
# We commented out this part because of backward compatibility # 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 ...@@ -663,7 +714,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"script_id": script.id, "script_id": script.id,
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"), "relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"list_method": list_method_name, "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 = {} list_method_query_dict = {}
""" """
...@@ -696,9 +747,9 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -696,9 +747,9 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"sort_column_list": sort_column_list, "sort_column_list": sort_column_list,
"editable_column_list": editable_column_list, "editable_column_list": editable_column_list,
"show_anchor": field.get_value("anchor"), "show_anchor": field.get_value("anchor"),
"portal_type": portal_types, "portal_type": portal_type_list,
"lines": lines, "lines": field.get_value('lines'),
"default_params": default_params, "default_params": ensureSerializable(default_params),
"list_method": list_method_name, "list_method": list_method_name,
"show_stat": field.get_value('stat_method') != "" or len(field.get_value('stat_columns')) > 0, "show_stat": field.get_value('stat_method') != "" or len(field.get_value('stat_columns')) > 0,
"show_count": field.get_value('count_method') != "", "show_count": field.get_value('count_method') != "",
...@@ -848,26 +899,84 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -848,26 +899,84 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
} }
if (form.pt == 'report_view'): 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_item_list = []
report_result_list = [] report_result_list = []
for field in form.get_fields(): for field in form.get_fields():
if field.getRecursiveTemplateField().meta_type == 'ReportBox': 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()) report_item_list.extend(field.render())
j = 0 # ERP5 Report document differs from a ERP5 Form in only one thing: it has
for report_item in report_item_list: # `report_method` attached to it - thus we call it right here
report_context = report_item.getObject(portal) if hasattr(form, 'report_method') and getattr(form, 'report_method', ""):
report_prefix = 'x%s' % j report_method_name = getattr(form, 'report_method')
j += 1 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_title = report_item.getTitle()
# report_class = "report_title_level_%s" % report_item.getLevel() # report_class = "report_title_level_%s" % report_item.getLevel()
report_form = report_item.getFormId() report_form = report_item.getFormId()
report_result = {'_links': {}} report_result = {'_links': {}}
renderForm(traversed_document, getattr(report_context, report_item.getFormId()), # some reports save a lot of unserializable data (datetime.datetime) and
report_result, key_prefix=report_prefix, # key "portal_type" (don't confuse with "portal_types" in ListBox) into
selection_params=report_item.selection_params) # 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) report_result_list.append(report_result)
response_dict['report_section_list'] = report_result_list 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 # XXX form action update, etc
def renderRawField(field): def renderRawField(field):
...@@ -910,6 +1019,7 @@ def renderRawField(field): ...@@ -910,6 +1019,7 @@ def renderRawField(field):
def renderFormDefinition(form, response_dict): def renderFormDefinition(form, response_dict):
"""Form "definition" is configurable in Zope admin: Form -> Order."""
group_list = [] group_list = []
for group in form.Form_getGroupTitleAndId(): for group in form.Form_getGroupTitleAndId():
...@@ -1031,11 +1141,15 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -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 # Add a link to the portal type if possible
if not is_portal: 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'] = { result_dict['_links']['type'] = {
"href": default_document_uri_template % { "href": default_document_uri_template % {
"root_url": site_root.absolute_url(), "root_url": site_root.absolute_url(),
"relative_url": portal.portal_types[traversed_document.getPortalType()]\ "relative_url": document_type.getRelativeUrl(),
.getRelativeUrl(),
"script_id": script.id "script_id": script.id
}, },
"name": Base_translateString(traversed_document.getPortalType()) "name": Base_translateString(traversed_document.getPortalType())
...@@ -1060,6 +1174,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1060,6 +1174,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
portal.portal_actions.listFilteredActionsFor(traversed_document)) portal.portal_actions.listFilteredActionsFor(traversed_document))
embedded_url = None embedded_url = None
# XXX See ERP5Type.getDefaultViewFor # XXX See ERP5Type.getDefaultViewFor
for erp5_action_key in erp5_action_dict.keys(): for erp5_action_key in erp5_action_dict.keys():
erp5_action_list = [] erp5_action_list = []
...@@ -1151,7 +1266,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1151,7 +1266,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# renderer_form = traversed_document.restrictedTraverse(form_id, None) # renderer_form = traversed_document.restrictedTraverse(form_id, None)
# XXX Proxy field are not correctly handled in traversed_document of web site # XXX Proxy field are not correctly handled in traversed_document of web site
renderer_form = getattr(traversed_document, form_id) renderer_form = getattr(traversed_document, form_id)
# traversed_document.log(form_id)
if (renderer_form is not None): if (renderer_form is not None):
embedded_dict = { embedded_dict = {
'_links': { '_links': {
...@@ -1161,12 +1275,24 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -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 # 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) query_split = embedded_url.split('?', 1)
if len(query_split) == 2: if len(query_split) == 2:
for query_parameter in query_split[1].split("&"): 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) 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) renderForm(traversed_document, renderer_form, embedded_dict)
result_dict['_embedded'] = { result_dict['_embedded'] = {
'_view': embedded_dict '_view': embedded_dict
...@@ -1255,7 +1381,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1255,7 +1381,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
else: else:
traversed_document_portal_type = traversed_document.getPortalType() 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) renderFormDefinition(traversed_document, result_dict)
response.setHeader("Cache-Control", "private, max-age=1800") response.setHeader("Cache-Control", "private, max-age=1800")
response.setHeader("Vary", "Cookie,Authorization,Accept-Encoding") response.setHeader("Vary", "Cookie,Authorization,Accept-Encoding")
...@@ -1283,14 +1409,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1283,14 +1409,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if action_dict: if action_dict:
result_dict['_actions'] = action_dict result_dict['_actions'] = action_dict
elif mode == 'search': elif mode == 'search':
################################################# #################################################
# Portal catalog search # Portal catalog search
# #
# Possible call arguments example: # Possible call arguments example:
# form_relative_url: portal_skins/erp5_web/WebSite_view/listbox # 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) # default_param_json: <base64 encoded JSON> (Additional search params)
# query: <str> (term for fulltext search) # query: <str> (term for fulltext search)
# select_list: ['int_index', 'id', 'title', ...] (column names to select) # 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, ...@@ -1337,9 +1462,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
"sort_on": () # default is an empty tuple "sort_on": () # default is an empty tuple
} }
if default_param_json is not None: 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: if query:
catalog_kw["full_text"] = query catalog_kw["full_text"] = query
if sort_on is not None: if sort_on is not None:
def parseSortOn(raw_string): def parseSortOn(raw_string):
"""Turn JSON serialized array into a tuple (col_name, order).""" """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, ...@@ -1452,6 +1581,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
contents_uid, contents_relative_url, property_getter = \ contents_uid, contents_relative_url, property_getter = \
getUidAndAccessorForAnything(search_result, result_index, traversed_document) 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 # _links.self.href is mandatory for JIO so it can create reference to the
# (listbox) item alone # (listbox) item alone
contents_item['_links'] = { contents_item['_links'] = {
...@@ -1503,7 +1636,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1503,7 +1636,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# endfor select # endfor select
REQUEST.other.pop('cell', None) REQUEST.other.pop('cell', None)
contents_list.append(contents_item) 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 # Compute statistics if the search issuer was ListBox
# or in future if the stats (SUM) are required by JIO call # 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, ...@@ -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') traversed_document, editable_field_dict[key], listbox_form, value, key=editable_field_dict[key].id + '__sum')
if len(contents_stat_list) > 0: 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 should cleanup the selection if it exists in catalog params BUT
# we cannot because it requires escalated Permission.'modifyPortal' so # we cannot because it requires escalated Permission.'modifyPortal' so
......
...@@ -9,8 +9,11 @@ from functools import wraps ...@@ -9,8 +9,11 @@ from functools import wraps
from ZPublisher.HTTPRequest import HTTPRequest from ZPublisher.HTTPRequest import HTTPRequest
from ZPublisher.HTTPResponse import HTTPResponse from ZPublisher.HTTPResponse import HTTPResponse
import base64
import DateTime
import StringIO import StringIO
import json import json
import re
import urllib import urllib
def changeSkin(skin_name): def changeSkin(skin_name):
...@@ -639,6 +642,42 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): ...@@ -639,6 +642,42 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_embedded']['_view']['_actions']['put']['method'], 'POST') 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', @simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"') 'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs', @simulate('Base_getRequestHeader', '*args, **kwargs',
...@@ -712,6 +751,71 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): ...@@ -712,6 +751,71 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertFalse(result_dict['_embedded']['_view'].has_key('_actions')) 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', @simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"') 'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs', @simulate('Base_getRequestHeader', '*args, **kwargs',
...@@ -1208,7 +1312,6 @@ class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin): ...@@ -1208,7 +1312,6 @@ class TestERP5Document_getHateoas_mode_bulk(ERP5HALJSONStyleSkinsMixin):
self.assertEquals(fake_request.RESPONSE.status, 405) self.assertEquals(fake_request.RESPONSE.status, 405)
self.assertEquals(result, "") self.assertEquals(result, "")
@simulate('Base_getRequestUrl', '*args, **kwargs', @simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"') 'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs', @simulate('Base_getRequestHeader', '*args, **kwargs',
...@@ -1347,6 +1450,7 @@ class TestERP5Document_getHateoas_mode_worklist(ERP5HALJSONStyleSkinsMixin): ...@@ -1347,6 +1450,7 @@ class TestERP5Document_getHateoas_mode_worklist(ERP5HALJSONStyleSkinsMixin):
self.assertEqual(result_dict['_debug'], "worklist") self.assertEqual(result_dict['_debug'], "worklist")
class TestERP5Document_getHateoas_translation(ERP5HALJSONStyleSkinsMixin): class TestERP5Document_getHateoas_translation(ERP5HALJSONStyleSkinsMixin):
code_string = "\ code_string = "\
from Products.CMFCore.utils import getToolByName\n\ from Products.CMFCore.utils import getToolByName\n\
...@@ -1428,8 +1532,7 @@ return msg" ...@@ -1428,8 +1532,7 @@ return msg"
code_string) code_string)
@createIndexedDocument() @createIndexedDocument()
@changeSkin('Hal') @changeSkin('Hal')
def test_getHateoasWorklist_default_view_translation(self, **kw): def test_getHateoasWorklist_default_view_translation(self, document):
# self._makeDocument()
fake_request = do_fake_request("GET") fake_request = do_fake_request("GET")
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas( result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request, 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 ...@@ -30,6 +30,7 @@ Foo | view_duration_field
Foo | view_formbox Foo | view_formbox
Foo | view_formbox_dialog Foo | view_formbox_dialog
Foo | view_formbox_fooline Foo | view_formbox_fooline
Foo | view_hidden_positive_only_quantity
Foo | view_listbox Foo | view_listbox
Foo | view_multiple_listbox Foo | view_multiple_listbox
Foo | view_planning Foo | view_planning
......
...@@ -171,12 +171,29 @@ ...@@ -171,12 +171,29 @@
form_definition = this.state.form_definition, form_definition = this.state.form_definition,
rendered_document = erp5_document._embedded._view, rendered_document = erp5_document._embedded._view,
group_list = form_definition.group_list, group_list = form_definition.group_list,
form_gadget = this; form_gadget = this,
tmp;
if (modification_dict.hasOwnProperty('hash')) { if (modification_dict.hasOwnProperty('hash')) {
form_gadget.props.gadget_list = []; 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() return new RSVP.Queue()
.push(function () { .push(function () {
return RSVP.all(group_list.map(function (group) { return RSVP.all(group_list.map(function (group) {
......
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>963.11788.48702.26146</string> </value> <value> <string>964.25533.41108.47530</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1514393621.04</float> <float>1515496577.67</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
/** Return true if `field` resembles non-empty and non-editable field. */ /** Return true if `field` resembles non-empty and non-editable field. */
function isGoodNonEditableField(field) { function isGoodNonEditableField(field) {
if (field === undefined || field === null) {return false; }
// ListBox and FormBox should always get a chance to render because they // ListBox and FormBox should always get a chance to render because they
// can contain editable fields // can contain editable fields
if (field.type === "ListBox") {return true; } if (field.type === "ListBox") {return true; }
...@@ -47,6 +48,7 @@ ...@@ -47,6 +48,7 @@
.declareMethod('render', function (options) { .declareMethod('render', function (options) {
var state_dict = { var state_dict = {
jio_key: options.jio_key, jio_key: options.jio_key,
title: options.title,
view: options.view, view: options.view,
editable: options.editable, editable: options.editable,
erp5_document: options.erp5_document, erp5_document: options.erp5_document,
...@@ -80,6 +82,7 @@ ...@@ -80,6 +82,7 @@
form_options.erp5_document = gadget.state.erp5_document; form_options.erp5_document = gadget.state.erp5_document;
form_options.form_definition = gadget.state.form_definition; form_options.form_definition = gadget.state.form_definition;
form_options.view = gadget.state.view; form_options.view = gadget.state.view;
form_options.title = gadget.state.title;
form_options.jio_key = gadget.state.jio_key; form_options.jio_key = gadget.state.jio_key;
form_options.editable = 0; // because for editable=1 there is a special form_options.editable = 0; // because for editable=1 there is a special
// page template 'pt_form_editable'. Once it is // page template 'pt_form_editable'. Once it is
...@@ -96,7 +99,7 @@ ...@@ -96,7 +99,7 @@
gadget.getUrlFor({command: 'selection_previous'}), gadget.getUrlFor({command: 'selection_previous'}),
gadget.getUrlFor({command: 'selection_next'}), gadget.getUrlFor({command: 'selection_next'}),
gadget.getUrlFor({command: 'change', options: {page: "tab"}}), 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"}}) : gadget.getUrlFor({command: 'change', options: {page: "export"}}) :
"", "",
calculatePageTitle(gadget, gadget.state.erp5_document) calculatePageTitle(gadget, gadget.state.erp5_document)
......
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>964.44232.19748.18107</string> </value> <value> <string>964.45882.29366.36147</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1515406785.95</float> <float>1515593717.52</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
}; };
return form_gadget.render({erp5_document: erp5_document, return form_gadget.render({erp5_document: erp5_document,
form_definition: form_definition, form_definition: form_definition,
editable: 0}); editable: 0, title: report_section.title});
}); });
} }
......
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>961.16421.12334.2201</string> </value> <value> <string>962.56167.53905.31470</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1502116518.17</float> <float>1508400391.84</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>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