Commit d2f8c300 authored by Tomáš Peterka's avatar Tomáš Peterka

WWIP

parent 45641950
...@@ -21,8 +21,13 @@ Only in mode == 'form' ...@@ -21,8 +21,13 @@ Only in mode == 'form'
Only in mode == 'traverse' Only in mode == 'traverse'
TBD.
# Form
When handling form, we can expect field values to be stored in REQUEST.form in two forms
- raw string value under key "field_" + <field.id>
- python-object parsed from raw values under <field.id>
""" """
from ZTUtils import make_query from ZTUtils import make_query
import json import json
from base64 import urlsafe_b64encode, urlsafe_b64decode from base64 import urlsafe_b64encode, urlsafe_b64decode
...@@ -35,13 +40,32 @@ import re ...@@ -35,13 +40,32 @@ import re
from zExceptions import Unauthorized from zExceptions import Unauthorized
from Products.ERP5Type.Utils import UpperCase from Products.ERP5Type.Utils import UpperCase
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
from Products.ERP5Type.Log import log
MARKER = []
if REQUEST is None: if REQUEST is None:
REQUEST = context.REQUEST REQUEST = context.REQUEST
# raise Unauthorized
if response is None: if response is None:
response = REQUEST.RESPONSE response = REQUEST.RESPONSE
def toBasicTypes(obj):
"""Ensure that obj contains only basic types."""
if obj is None:
return obj
if isinstance(obj, (bool, int, float, long, str, unicode)):
return obj
if isinstance(obj, (tuple, list)):
return [toBasicTypes(x) for x in obj]
try:
return {toBasicTypes(key): toBasicTypes(obj[key]) for key in obj}
except:
log('Cannot convert {!s} to basic types {!s}'.format(type(obj), obj), level=100)
return obj
# http://stackoverflow.com/a/13105359 # http://stackoverflow.com/a/13105359
def byteify(string): def byteify(string):
if isinstance(string, dict): if isinstance(string, dict):
...@@ -53,11 +77,12 @@ def byteify(string): ...@@ -53,11 +77,12 @@ def byteify(string):
else: else:
return string return string
def ensure_serializable(obj):
def ensureSerializable(obj):
"""Ensure obj and all sub-objects are JSON serializable.""" """Ensure obj and all sub-objects are JSON serializable."""
if isinstance(obj, dict): if isinstance(obj, dict):
for key in obj: for key in obj:
obj[key] = ensure_serializable(obj[key]) obj[key] = ensureSerializable(obj[key])
# throw away date's type information and later reconstruct as Zope's DateTime # throw away date's type information and later reconstruct as Zope's DateTime
if isinstance(obj, DateTime): if isinstance(obj, DateTime):
return obj.ISO() return obj.ISO()
...@@ -66,17 +91,18 @@ def ensure_serializable(obj): ...@@ -66,17 +91,18 @@ def ensure_serializable(obj):
# we don't check other isinstances - we believe that iterables don't contain unserializable objects # we don't check other isinstances - we believe that iterables don't contain unserializable objects
return obj return obj
datetime_iso_re = re.compile(r'^\d{4}-\d{2}-\d{2} |T\d{2}:\d{2}:\d{2}.*$') 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}).*$') time_iso_re = re.compile(r'^(\d{2}):(\d{2}):(\d{2}).*$')
def ensure_deserialized(obj): def ensureDeserialized(obj):
"""Deserialize classes serialized by our own `ensure_serializable`. """Deserialize classes serialized by our own `ensureSerializable`.
Method `biteify` must not be called on the result because it would revert out Method `biteify` must not be called on the result because it would revert out
deserialization by calling __str__ on constructed classes. deserialization by calling __str__ on constructed classes.
""" """
if isinstance(obj, dict): if isinstance(obj, dict):
for key in obj: for key in obj:
obj[key] = ensure_deserialized(obj[key]) obj[key] = ensureDeserialized(obj[key])
# seems that default __str__ method is good enough # seems that default __str__ method is good enough
if isinstance(obj, str): if isinstance(obj, str):
# Zope's DateTime must be good enough for everyone # Zope's DateTime must be good enough for everyone
...@@ -97,16 +123,49 @@ def getProtectedProperty(document, select): ...@@ -97,16 +123,49 @@ def getProtectedProperty(document, select):
See https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5Form/ListBox.py#L2293 See https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5Form/ListBox.py#L2293
""" """
try: try:
if "." in select: #see https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5Form/ListBox.py#L2293
try:
select = select[select.rindex('.') + 1:] select = select[select.rindex('.') + 1:]
except ValueError:
pass
return document.getProperty(select, d=None) return document.getProperty(select, d=None)
except (ConflictError, RuntimeError): except (ConflictError, RuntimeError):
raise raise
except: except:
return None return None
def kwargsForCallable(func, initial_kwargs, kwargs_dict):
"""Create a copy of `kwargs_dict` with only items suitable for `func`.
In case the function cannot state required arguments it throws an AttributeError.
"""
func_param_list = [func_param.strip() for func_param in func.params().split(",")]
func_param_name_list = [func_param if '=' not in func_param else func_param.split('=')[0]
for func_param in func_param_list if '*' not in func_param]
for func_param_name in func_param_name_list:
if func_param_name in kwargs_dict and func_param_name not in initial_kwargs:
initial_kwargs[func_param_name] = kwargs_dict.get(func_param_name)
# MIDDLE-DANGEROUS!
# In case of reports (later even exports) substitute None for unknown
# parameters. We suppose Python syntax for parameters!
# What we do here is literally putting every form field from `kwargs_dict`
# into search method parameters - this is later put back into `kwargs_dict`
# this way we can mimic synchronous rendering when all form field values
# were available in `kwargs_dict`. It is obviously wrong behaviour.
for func_param in func_param_list:
if "*" in func_param:
continue
if "=" in func_param:
continue
# now we have only mandatory parameters
func_param = func_param.strip()
if func_param not in initial_kwargs:
initial_kwargs[func_param] = None
return initial_kwargs
def object_uids_and_accessors(search_result, result_index, traversed_document): def anythingUidAndAccessor(search_result, result_index, traversed_document):
"""Return unique ID, unique URL, getter and hasser for any combination of `search_result` and `index`. """Return unique ID, unique URL, getter and hasser for any combination of `search_result` and `index`.
You want to use this method when you need a unique reference to random object in iterable (for example You want to use this method when you need a unique reference to random object in iterable (for example
...@@ -121,8 +180,9 @@ def object_uids_and_accessors(search_result, result_index, traversed_document): ...@@ -121,8 +180,9 @@ def object_uids_and_accessors(search_result, result_index, traversed_document):
result[uid] = {'url': portal.abolute_url() + url} result[uid] = {'url': portal.abolute_url() + url}
value = getter(random_object, "value") value = getter(random_object, "value")
""" """
context.log("anythingUidAndAccessor({!s}#type:{!s}, {:d}, {!s}".format(search_result, type(search_result), result_index, traversed_document))
if hasattr(search_result, "getObject"): if hasattr(search_result, "getObject"):
# search_result = search_result.getObject() # "Brain" object - which simulates DB Cursor thus result must have UID
contents_uid = search_result.uid contents_uid = search_result.uid
# every document indexed in catalog has to have relativeUrl # every document indexed in catalog has to have relativeUrl
contents_relative_url = getRealRelativeUrl(search_result) contents_relative_url = getRealRelativeUrl(search_result)
...@@ -133,7 +193,7 @@ def object_uids_and_accessors(search_result, result_index, traversed_document): ...@@ -133,7 +193,7 @@ def object_uids_and_accessors(search_result, result_index, traversed_document):
try: try:
return doc.hasProperty(attr) return doc.hasProperty(attr)
except (AttributeError, Unauthorized) as e: except (AttributeError, Unauthorized) as e:
context.log('Cannot state ownership of property "{}" on {!s} because of "{!s}"'.format( log('Cannot state ownership of property "{}" on {!s} because of "{!s}"'.format(
attr, doc, e)) attr, doc, e))
return False return False
elif hasattr(search_result, "aq_self"): elif hasattr(search_result, "aq_self"):
...@@ -162,11 +222,12 @@ def object_uids_and_accessors(search_result, result_index, traversed_document): ...@@ -162,11 +222,12 @@ def object_uids_and_accessors(search_result, result_index, traversed_document):
return contents_uid, contents_relative_url, search_property_getter, search_property_hasser return contents_uid, contents_relative_url, search_property_getter, search_property_hasser
def resolve_field(search_result, select, search_property_getter, search_property_hasser): def getAttrFromAnything(search_result, select, search_property_getter, search_property_hasser, kwargs):
"""Given `data_source` extract fields defined in `field_list` and render them using `field_template_dict`. """Given `data_source` extract value named `select` using helper getter/hasser.
:param data_source: any dict-like object (usually dict or Brain or Document) :param data_source: any dict-like object (usually dict or Brain or Document)
:select: field name (can represent actual Properties or Scripts) :select: field name (can represent actual attributes, Properties or even Scripts)
:kwargs: available arguments for possible callables hidden under `select`
""" """
# if the variable does not have a field template we need to find its # if the variable does not have a field template we need to find its
...@@ -175,7 +236,7 @@ def resolve_field(search_result, select, search_property_getter, search_property ...@@ -175,7 +236,7 @@ def resolve_field(search_result, select, search_property_getter, search_property
contents_value = None contents_value = None
if not isinstance(select, (str, unicode)) or len(select) == 0: if not isinstance(select, (str, unicode)) or len(select) == 0:
context.log('There is an invalid column name "{!s}"!'.format(select), level=200) log('There is an invalid column name "{!s}"!'.format(select), level=200)
return None return None
if "." in select: if "." in select:
...@@ -211,7 +272,7 @@ def resolve_field(search_result, select, search_property_getter, search_property ...@@ -211,7 +272,7 @@ def resolve_field(search_result, select, search_property_getter, search_property
# do not call it here - it will be done later in generic call part # do not call it here - it will be done later in generic call part
contents_value = getattr(search_result, accessor_name) contents_value = getattr(search_result, accessor_name)
except (AttributeError, KeyError, Unauthorized) as error: except (AttributeError, KeyError, Unauthorized) as error:
context.log("Could not evaluate {} nor {} on {} with error {!s}".format( log("Could not evaluate {} nor {} on {} with error {!s}".format(
select, accessor_name, search_result, error), level=100) # WARNING select, accessor_name, search_result, error), level=100) # WARNING
if contents_value is None and search_property_hasser(search_result, select): if contents_value is None and search_property_hasser(search_result, select):
...@@ -222,7 +283,7 @@ def resolve_field(search_result, select, search_property_getter, search_property ...@@ -222,7 +283,7 @@ def resolve_field(search_result, select, search_property_getter, search_property
try: try:
contents_value = getattr(search_result, select, None) contents_value = getattr(search_result, select, None)
except (Unauthorized, AttributeError, KeyError) as error: except (Unauthorized, AttributeError, KeyError) as error:
context.log("Cannot resolve {} on {!s} because {!s}".format( log("Cannot resolve {} on {!s} because {!s}".format(
select, raw_search_result, error), level=100) select, raw_search_result, error), level=100)
if callable(contents_value): if callable(contents_value):
...@@ -242,7 +303,7 @@ def resolve_field(search_result, select, search_property_getter, search_property ...@@ -242,7 +303,7 @@ def resolve_field(search_result, select, search_property_getter, search_property
else: else:
contents_value = contents_value() contents_value = contents_value()
except (AttributeError, KeyError, Unauthorized) as error: except (AttributeError, KeyError, Unauthorized) as error:
context.log("Could not evaluate {} on {} with error {!s}".format( log("Could not evaluate {} on {} with error {!s}".format(
contents_value, search_result, error), level=100) # WARNING contents_value, search_result, error), level=100) # WARNING
# make resulting value JSON serializable # make resulting value JSON serializable
...@@ -309,12 +370,15 @@ def getFormRelativeUrl(form): ...@@ -309,12 +370,15 @@ def getFormRelativeUrl(form):
)[0].relative_url )[0].relative_url
def getFieldDefault(traversed_document, field, key, value=None): def getFieldDefault(form, field, key, value=None):
# REQUEST.get(field.id, field.get_value("default")) """Get available value for `field` preferably in python-object from REQUEST or from field's default."""
result = traversed_document.Field_getDefaultValue(field, key, value, REQUEST) if value is None:
if getattr(result, 'translate', None) is not None: value = REQUEST.form.get(field.id, REQUEST.form.get(key, None)) or field.get_value('default')
result = "%s" % result if field.has_value("unicode") and field.get_value("unicode") and isinstance(value, 'unicode'):
return result value = unicode(value, self.get_form_encoding())
if getattr(value, 'translate', None) is not None:
return "%s" % value
return value
def renderField(traversed_document, field, form, value=None, meta_type=None, key=None, key_prefix=None, selection_params=None): def renderField(traversed_document, field, form, value=None, meta_type=None, key=None, key_prefix=None, selection_params=None):
...@@ -328,6 +392,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -328,6 +392,13 @@ 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
# some TALES expressions are using Base_getRelatedObjectParameter which requires that
previous_request_field = REQUEST.other.pop('field_id', None)
REQUEST.other['field_id'] = field.id
result = { result = {
"type": meta_type, "type": meta_type,
"title": Base_translateString(field.get_value("title")), "title": Base_translateString(field.get_value("title")),
...@@ -342,15 +413,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -342,15 +413,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# fields have default value and can be required (unlike boxes) # fields have default value and can be required (unlike boxes)
result.update({ result.update({
"required": field.get_value("required") if field.has_value("required") else None, "required": field.get_value("required") if field.has_value("required") else None,
"default": getFieldDefault(traversed_document, field, result["key"], value), "default": getFieldDefault(form, field, key, value),
}) })
if meta_type == "ProxyField": # start the actuall "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
...@@ -371,9 +437,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -371,9 +437,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"sub_select_key": traversed_document.Field_getSubFieldKeyDict(field, 'default:list', key=result["key"]), "sub_select_key": traversed_document.Field_getSubFieldKeyDict(field, 'default:list', key=result["key"]),
"sub_input_key": "default_" + traversed_document.Field_getSubFieldKeyDict(field, 'default:list:int', key=result["key"]) "sub_input_key": "default_" + traversed_document.Field_getSubFieldKeyDict(field, 'default:list:int', key=result["key"])
}) })
return result
if meta_type in ("StringField", "FloatField", "EmailField", "TextAreaField", elif meta_type in ("StringField", "FloatField", "EmailField", "TextAreaField",
"LinesField", "ImageField", "FileField", "IntegerField", "LinesField", "ImageField", "FileField", "IntegerField",
"PasswordField", "EditorField"): "PasswordField", "EditorField"):
if meta_type == "FloatField": if meta_type == "FloatField":
...@@ -396,9 +461,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -396,9 +461,8 @@ 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
if meta_type == "DateTimeField": elif meta_type == "DateTimeField":
result.update({ result.update({
"date_only": field.get_value("date_only"), "date_only": field.get_value("date_only"),
"ampm_time_style": field.get_value("ampm_time_style"), "ampm_time_style": field.get_value("ampm_time_style"),
...@@ -407,7 +471,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -407,7 +471,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"hide_day": field.get_value('hide_day'), "hide_day": field.get_value('hide_day'),
"hidden_day_is_last_day": field.get_value('hidden_day_is_last_day'), "hidden_day_is_last_day": field.get_value('hidden_day_is_last_day'),
}) })
date_value = getFieldDefault(traversed_document, field, result["key"], value) date_value = getFieldDefault(form, field, key, value)
if not date_value and field.get_value('default_now'): if not date_value and field.get_value('default_now'):
date_value = DateTime() date_value = DateTime()
if same_type(date_value, DateTime()): if same_type(date_value, DateTime()):
...@@ -418,9 +482,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -418,9 +482,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
result["default"] = date_value result["default"] = date_value
for subkey in ("year", "month", "day", "hour", "minute", "ampm", "timezone"): for subkey in ("year", "month", "day", "hour", "minute", "ampm", "timezone"):
result["subfield_%s_key" % subkey] = traversed_document.Field_getSubFieldKeyDict(field, subkey, key=result["key"]) result["subfield_%s_key" % subkey] = traversed_document.Field_getSubFieldKeyDict(field, subkey, key=result["key"])
return result
if meta_type in ("RelationStringField", "MultiRelationStringField"): elif meta_type in ("RelationStringField", "MultiRelationStringField"):
portal_type_list = field.get_value('portal_type') portal_type_list = field.get_value('portal_type')
translated_portal_type = [] translated_portal_type = []
jump_reference_list = [] jump_reference_list = []
...@@ -462,11 +525,16 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -462,11 +525,16 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
for (listbox_path, listbox_name) in listbox_ids: for (listbox_path, listbox_name) in listbox_ids:
(listbox_form_name, listbox_field_name) = listbox_path.split('/', 2) (listbox_form_name, listbox_field_name) = listbox_path.split('/', 2)
form = getattr(context, listbox_form_name) # do not override "global" `form`
rel_form = getattr(context, listbox_form_name)
# find listbox field # find listbox field
listbox_form_field = filter(lambda f: f.getId() == listbox_field_name, form.get_fields())[0] listbox_form_field = filter(lambda f: f.getId() == listbox_field_name, rel_form.get_fields())[0]
rel_cache = {'form_id': REQUEST.get('form_id', MARKER), 'field_id': REQUEST.get('field_id', MARKER)}
REQUEST.set('form_id', rel_form.id)
REQUEST.set('field_id', listbox_form_field.id)
# get original definition # get original definition
subfield = renderField(context, listbox_form_field, form) subfield = renderField(context, listbox_form_field, rel_form)
# overwrite, like Base_getRelatedObjectParameter does # overwrite, like Base_getRelatedObjectParameter does
if subfield["portal_type"] == []: if subfield["portal_type"] == []:
subfield["portal_type"] = field.get_value('portal_type') subfield["portal_type"] = field.get_value('portal_type')
...@@ -488,6 +556,11 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -488,6 +556,11 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
subfield["column_list"].append((tmp_column[0], Base_translateString(tmp_column[1]))) subfield["column_list"].append((tmp_column[0], Base_translateString(tmp_column[1])))
listbox[Base_translateString(listbox_name)] = subfield listbox[Base_translateString(listbox_name)] = subfield
for key in rel_cache:
if rel_cache[key] is not MARKER:
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,
...@@ -508,21 +581,18 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -508,21 +581,18 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"relation_item_key": traversed_document.Field_getSubFieldKeyDict(field, "item", key=result["key"]), "relation_item_key": traversed_document.Field_getSubFieldKeyDict(field, "item", key=result["key"]),
"relation_item_relative_url": [jump_reference.getRelativeUrl() for jump_reference in jump_reference_list] "relation_item_relative_url": [jump_reference.getRelativeUrl() for jump_reference in jump_reference_list]
}) })
return result
if meta_type in ("CheckBoxField", "MultiCheckBoxField"): elif meta_type in ("CheckBoxField", "MultiCheckBoxField"):
if meta_type == "MultiCheckBoxField": if meta_type == "MultiCheckBoxField":
result["items"] = field.get_value("items"), result["items"] = field.get_value("items"),
return result
if meta_type == "GadgetField": elif meta_type == "GadgetField":
result.update({ result.update({
"url": field.get_value("gadget_url"), "url": field.get_value("gadget_url"),
"sandbox": field.get_value("js_sandbox") "sandbox": field.get_value("js_sandbox")
}) })
return result
if meta_type == "ListBox": elif 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 We might be inside a ReportBox which is inside a parent form BUT we still have access to
...@@ -576,8 +646,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -576,8 +646,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# Search for non-editable documents - all reports goes here # Search for non-editable documents - all reports goes here
# Reports have custom search scripts which wants parameters from the form # Reports have custom search scripts which wants parameters from the form
# thus we introspect such parameters and try to find them in REQUEST # thus we introspect such parameters and try to find them in REQUEST
list_method = None list_method = field.get_value('list_method') or None
list_method_name = traversed_document.Listbox_getListMethodName(field) list_method_name = list_method.getMethodName() if list_method is not None else ""
if list_method_name not in ("", "portal_catalog", "searchFolder", "objectValues"): if list_method_name not in ("", "portal_catalog", "searchFolder", "objectValues"):
# we avoid accessing known protected objects and builtin functions above # we avoid accessing known protected objects and builtin functions above
try: try:
...@@ -585,32 +655,15 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -585,32 +655,15 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
except (Unauthorized, AttributeError, ValueError) as error: except (Unauthorized, AttributeError, ValueError) as error:
# we are touching some specially protected (usually builtin) methods # we are touching some specially protected (usually builtin) methods
# which we will not introspect # which we will not introspect
context.log('ListBox {!s} list_method {} is unavailable because of "{!s}"'.format( log('ListBox {!s} list_method {} is unavailable because of "{!s}"'.format(
field, list_method_name, error), level=100) field, list_method_name, error), level=100)
else:
list_method = None
# Put all ListBox's search method params from REQUEST to `default_param_json` # Put all ListBox's search method params from REQUEST to `default_param_json`
# because old code expects synchronous render thus having all form's values # because old code expects synchronous render thus having all form's values
# still in the request which is not our case because we do asynchronous rendering # still in the request which is not our case because we do asynchronous rendering
if list_method is not None and hasattr(list_method, "ZScriptHTML_tryParams"): if list_method is not None:
for list_method_param in list_method.ZScriptHTML_tryParams(): kwargsForCallable(list_method, list_method_query_dict, REQUEST)
if list_method_param in REQUEST and list_method_param not in list_method_query_dict:
list_method_query_dict[list_method_param] = REQUEST.get(list_method_param)
# MIDDLE-DANGEROUS!
# In case of reports (later even exports) substitute None for unknown
# parameters. We suppose Python syntax for parameters!
# What we do here is literally putting every form field from REQUEST
# into search method parameters - this is later put back into REQUEST
# this way we can mimic synchronous rendering when all form field values
# were available in REQUEST. It is obviously wrong behaviour.
for list_method_param in list_method.params().split(","):
if "*" in list_method_param:
continue
if "=" in list_method_param:
continue
# now we have only mandatory parameters
list_method_param = list_method_param.strip()
if list_method_param not in list_method_query_dict:
list_method_query_dict[list_method_param] = None
# Now if the list_method does not specify **kwargs we need to remove # Now if the list_method does not specify **kwargs we need to remove
# unwanted parameters like "portal_type" which is everywhere # unwanted parameters like "portal_type" which is everywhere
if "**" not in list_method.params(): if "**" not in list_method.params():
...@@ -627,7 +680,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -627,7 +680,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"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( "default_param_json": urlsafe_b64encode(
json.dumps(ensure_serializable(list_method_query_dict))) json.dumps(ensureSerializable(list_method_query_dict)))
} }
# once we imprint `default_params` into query string of 'list method' we # once we imprint `default_params` into query string of 'list method' we
# don't want them to propagate to the query as well # don't want them to propagate to the query as well
...@@ -642,29 +695,10 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -642,29 +695,10 @@ 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(ensure_serializable(list_method_query_dict))) "default_param_json": urlsafe_b64encode(json.dumps(ensureSerializable(list_method_query_dict)))
} }
list_method_query_dict = {} list_method_query_dict = {}
# row_list = list_method(limit=lines, portal_type=portal_types,
# **default_params)
# line_list = []
# for row in row_list:
# document = row.getObject()
# line = {
# "url": url_template_dict["document_hal"] % {
# "root_url": site_root.absolute_url(),
# "relative_url": document.getRelativeUrl(),
# "script_id": script.id
# }
# }
# for property, title in columns:
# prop = document.getProperty(property)
# if same_type(prop, DateTime()):
# prop = "XXX Serialize DateTime"
# line[title] = prop
# line["_relative_url"] = document.getRelativeUrl()
# line_list.append(line)
result.update({ result.update({
"column_list": column_list, "column_list": column_list,
"search_column_list": search_column_list, "search_column_list": search_column_list,
...@@ -674,7 +708,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -674,7 +708,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"show_anchor": field.get_value("anchor"), "show_anchor": field.get_value("anchor"),
"portal_type": portal_type_list, "portal_type": portal_type_list,
"lines": field.get_value('lines'), "lines": field.get_value('lines'),
"default_params": ensure_serializable(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') != "",
...@@ -686,9 +720,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -686,9 +720,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
}) })
if (list_method_custom is not None): if (list_method_custom is not None):
result["list_method_template"] = list_method_custom result["list_method_template"] = list_method_custom
return result
if meta_type == "FormBox": elif meta_type == "FormBox":
embedded_document = { embedded_document = {
'_links': {}, '_links': {},
'_actions': {}, '_actions': {},
...@@ -711,9 +744,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -711,9 +744,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
result['_embedded'] = { result['_embedded'] = {
'_view': embedded_document '_view': embedded_document
} }
return result
if meta_type == "MatrixBox": elif meta_type == "MatrixBox":
# data are generated by python code for MatrixBox.py # data are generated by python code for MatrixBox.py
# template_fields are better rendered here because they can be part of "hidden" # template_fields are better rendered here because they can be part of "hidden"
# group which is not rendered in form by default. Including # group which is not rendered in form by default. Including
...@@ -726,10 +758,14 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -726,10 +758,14 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
for template_field in template_field_names for template_field in template_field_names
if template_field in form}, if template_field in form},
}) })
return result
else:
# All other fields are not implemented and we'll return only basic info about them # All other fields are not implemented and we'll return only basic info about them
result["_debug"] = "Unknown field type " + meta_type result["_debug"] = "Unknown field type " + meta_type
if previous_request_field is not None:
REQUEST.other['field_id'] = previous_request_field
return result return result
...@@ -740,7 +776,12 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -740,7 +776,12 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
we mitigate the functionality here by overriding ListBox's own values we mitigate the functionality here by overriding ListBox's own values
for columns, editable columns, and sort with those found in `selection_params` for columns, editable columns, and sort with those found in `selection_params`
""" """
previous_request_other = {
'form_id': REQUEST.other.pop('form_id', None),
'here': REQUEST.other.pop('here', None)
}
REQUEST.set('here', traversed_document) REQUEST.set('here', traversed_document)
REQUEST.set('form_id', form.id)
field_errors = REQUEST.get('field_errors', {}) field_errors = REQUEST.get('field_errors', {})
#hardcoded #hardcoded
...@@ -860,6 +901,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -860,6 +901,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
if report_item.selection_name: if report_item.selection_name:
selection_name = report_prefix + "_" + report_item.selection_name selection_name = report_prefix + "_" + report_item.selection_name
context.log('Report {} defines selection_name {}'.format(report_title, selection_name))
report_form_params.update(selection_name=selection_name) report_form_params.update(selection_name=selection_name)
# this should load selections with correct values - since it is modifying # this should load selections with correct values - since it is modifying
# global state in the backend we have nothing more to do here # global state in the backend we have nothing more to do here
...@@ -892,6 +934,11 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti ...@@ -892,6 +934,11 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
report_result['title'] = report_title 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):
...@@ -1329,7 +1376,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1329,7 +1376,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# #
# 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 items) # 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)
...@@ -1351,6 +1398,9 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1351,6 +1398,9 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
response.setStatus(405) response.setStatus(405)
return "" return ""
# set 'here' for field rendering which contain TALES expressions
REQUEST.set('here', traversed_document)
# in case we have custom list method # in case we have custom list method
catalog_kw = {} catalog_kw = {}
...@@ -1374,7 +1424,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1374,7 +1424,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
} }
if default_param_json is not None: if default_param_json is not None:
catalog_kw.update( catalog_kw.update(
ensure_deserialized( ensureDeserialized(
byteify( byteify(
json.loads(urlsafe_b64decode(default_param_json))))) json.loads(urlsafe_b64decode(default_param_json)))))
if query: if query:
...@@ -1392,7 +1442,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1392,7 +1442,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
sort_order = "DESC" sort_order = "DESC"
else: else:
# should raise an ValueError instead # should raise an ValueError instead
context.log('Wrong sort order "{}" in {}! It must start with "asc" or "desc"'.format(sort_order, form_relative_url), log('Wrong sort order "{}" in {}! It must start with "asc" or "desc"'.format(sort_order, form_relative_url),
level=200) # error level=200) # error
return (sort_col, sort_order) return (sort_col, sort_order)
...@@ -1414,7 +1464,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1414,7 +1464,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# #
# for k, v in catalog_kw.items(): # for k, v in catalog_kw.items():
# REQUEST.set(k, v) # REQUEST.set(k, v)
context.log('list_method >>> {}({!s})'.format(list_method, catalog_kw))
search_result_iterable = callable_list_method(**catalog_kw) search_result_iterable = callable_list_method(**catalog_kw)
# Cast to list if only one element is provided # Cast to list if only one element is provided
...@@ -1438,6 +1488,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1438,6 +1488,10 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
listbox_field_id = source_field.id listbox_field_id = source_field.id
# 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
listbox_form = getattr(traversed_document, source_field.aq_parent.id) listbox_form = getattr(traversed_document, source_field.aq_parent.id)
# field TALES expression evaluated by Base_getRelatedObjectParameter requires that
REQUEST.other['form_id'] = listbox_form.id
for select in select_list: for select in select_list:
# See Listbox.py getValueList --> getEditableField & getColumnAliasList method # See Listbox.py getValueList --> getEditableField & getColumnAliasList method
# In short: there are Form Field definitions which names start with # In short: there are Form Field definitions which names start with
...@@ -1473,13 +1527,15 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1473,13 +1527,15 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if result_index >= start + num_items: if result_index >= start + num_items:
break break
# we can render fields which need 'here' to be set to currently rendered document
#REQUEST.set('here', search_result)
contents_item = {}
contents_uid, contents_relative_url, property_getter, property_hasser = \ contents_uid, contents_relative_url, property_getter, property_hasser = \
object_uids_and_accessors(search_result, result_index, traversed_document) anythingUidAndAccessor(search_result, result_index, traversed_document)
# this dict will hold all resolved values # _links.self.href is mandatory for JIO so it can create reference to the
contents_item = { # (listbox) item alone
'_links': { contents_item['_links'] = {
# _links.self.href is mandatory for JIO so it can create reference to items alone
'self': { 'self': {
"href": default_document_uri_template % { "href": default_document_uri_template % {
"root_url": site_root.absolute_url(), "root_url": site_root.absolute_url(),
...@@ -1488,7 +1544,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1488,7 +1544,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
}, },
}, },
} }
}
# ERP5 stores&send the list of editable elements in a hidden field called # ERP5 stores&send the list of editable elements in a hidden field called
# only database results can be editable so it belongs here # only database results can be editable so it belongs here
if editable_field_dict and listbox_field_id: if editable_field_dict and listbox_field_id:
...@@ -1505,10 +1561,17 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1505,10 +1561,17 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
REQUEST.set('cell', search_result) REQUEST.set('cell', search_result)
# if default value is given by evaluating Tales expression then we only # if default value is given by evaluating Tales expression then we only
# put "cell" to request (expected by tales) and let the field evaluate # put "cell" to request (expected by tales) and let the field evaluate
default_field_value = None
if getattr(editable_field_dict[select].tales, "default", "") == "": if getattr(editable_field_dict[select].tales, "default", "") == "":
# if there is no tales expr (or is empty) we extract the value from search result # if there is no tales expr (or is empty) we extract the value from search result
default_field_value = getProtectedProperty(search_result, select) default_field_value = getAttrFromAnything(search_result, select, property_getter, property_hasser, {})
context.log('renderField!for"{}"({!s}, field={!s}, form={!s}, value={!s}, key={}'.format(
select,
traversed_document,
editable_field_dict[select],
listbox_form,
default_field_value,
'field_%s_%s' % (editable_field_dict[select].id, contents_uid)))
contents_item[select] = renderField( contents_item[select] = renderField(
traversed_document, traversed_document,
...@@ -1522,13 +1585,11 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1522,13 +1585,11 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# most of the complicated magic happens here - we need to resolve field names # most of the complicated magic happens here - we need to resolve field names
# given search_result. This name can unfortunately mean almost anything from # given search_result. This name can unfortunately mean almost anything from
# a key name to Python Script with variable number of input parameters. # a key name to Python Script with variable number of input parameters.
contents_item[select] = resolve_field(search_result, select, property_getter, property_hasser) contents_item[select] = getAttrFromAnything(search_result, select, property_getter, property_hasser, {'brain': search_result})
# endfor select # endfor select
contents_list.append(contents_item) contents_list.append(contents_item)
result_dict['_embedded'].update({ result_dict['_embedded']['contents'] = ensureSerializable(contents_list)
'contents': 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
...@@ -1536,8 +1597,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1536,8 +1597,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if source_field_meta_type == "ProxyField": if source_field_meta_type == "ProxyField":
source_field_meta_type = source_field.getRecursiveTemplateField().meta_type source_field_meta_type = source_field.getRecursiveTemplateField().meta_type
context.log('source_field "{!s}", source_field_meta_type {!s}'.format(source_field, source_field_meta_type))
if source_field is not None and source_field_meta_type == "ListBox": if source_field is not None and source_field_meta_type == "ListBox":
contents_stat_list = [] contents_stat_list = []
# in case the search was issued by listbox we can provide results of # in case the search was issued by listbox we can provide results of
...@@ -1546,37 +1605,34 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1546,37 +1605,34 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
stat_method = source_field.get_value('stat_method') stat_method = source_field.get_value('stat_method')
stat_columns = source_field.get_value('stat_columns') stat_columns = source_field.get_value('stat_columns')
context.log('stat_method "{!s}", stat_columns {!s}'.format(stat_method, stat_columns)) context.log('stat_method "{!s}", stat_columns {!s}'.format(stat_method, stat_columns))
# Selection is unfortunatelly required fot stat methods # support only selection_name for stat methods because any `selection` is deprecated
# and should be removed
selection_name = source_field.get_value('selection_name') selection_name = source_field.get_value('selection_name')
selection = None if selection_name and 'selection_name' not in catalog_kw:
if selection_name: catalog_kw['selection_name'] = selection_name
selection_tool = context.getPortalObject().portal_selections context.log('stat_method will receive selection_name "{}"'.format(catalog_kw['selection_name']))
selection = selection_tool.getSelectionFor(selection_name, REQUEST)
contents_stat = {} contents_stat = {}
if len(stat_columns) > 0: if len(stat_columns) > 0:
# prefer stat per columns as it is in ListBox # prefer stat per column (follow original ListBox.py implementation)
# always called on current context
for stat_name, stat_script in stat_columns: for stat_name, stat_script in stat_columns:
contents_stat[stat_name] = getattr(traversed_document, stat_script)( contents_stat[stat_name] = getattr(traversed_document, stat_script)(**catalog_kw)
selection=selection,
selection_name=selection_name,
**catalog_kw)
contents_stat_list.append(contents_stat) contents_stat_list.append(contents_stat)
elif stat_method != "" and stat_method.getMethodName() != list_method: elif stat_method != "" and stat_method.getMethodName() != list_method:
# general stat_method is second - should return dictionary or list of dictionaries # general stat_method is second in priority list - should return dictionary or list of dictionaries
# where all "fields" should be accessible by their "select" name # where all "fields" should be accessible by their "select" name (no "listbox_" prefix)
contents_stat_list = getattr(traversed_document, stat_method.getMethodName())(**catalog_kw) stat_method_result = getattr(traversed_document, stat_method.getMethodName())(**catalog_kw)
contents_stat_list = toBasicTypes(stat_method_result) or []
for contents_stat in contents_stat_list: for contents_stat in contents_stat_list:
for key, value in contents_stat.items(): for key, value in contents_stat.items():
if key in editable_field_dict: if key in editable_field_dict:
contents_stat[key] = renderField( contents_stat[key] = renderField(
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')
context.log('contents_stat_list {!s}'.format(contents_stat_list)) for contents_stat in contents_stat_list:
for key, value in contents_stat.items():
context.log('contents_stat["{}"] = type {!s}, value {!s}'.format(key, type(value), value))
if len(contents_stat_list) > 0: if len(contents_stat_list) > 0:
result_dict['_embedded'].update({ result_dict['_embedded']['sum'] = ensureSerializable(contents_stat_list)
'sum': 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
...@@ -1683,6 +1739,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1683,6 +1739,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
return result_dict return result_dict
response.setHeader('Content-Type', mime_type) response.setHeader('Content-Type', mime_type)
context.log('calculateHateoas(traversed_document={!s}, mode="{}"'.format(temp_traversed_document, mode))
hateoas = calculateHateoas(is_portal=temp_is_portal, is_site_root=temp_is_site_root, hateoas = calculateHateoas(is_portal=temp_is_portal, is_site_root=temp_is_site_root,
traversed_document=temp_traversed_document, traversed_document=temp_traversed_document,
relative_url=relative_url, relative_url=relative_url,
...@@ -1692,7 +1749,22 @@ hateoas = calculateHateoas(is_portal=temp_is_portal, is_site_root=temp_is_site_r ...@@ -1692,7 +1749,22 @@ hateoas = calculateHateoas(is_portal=temp_is_portal, is_site_root=temp_is_site_r
default_param_json=default_param_json, default_param_json=default_param_json,
form_relative_url=form_relative_url) form_relative_url=form_relative_url)
def deepInspection(obj, prefix):
if isinstance(obj, dict):
for key, value in obj.items():
if type(key) != str:
log('{} key "{!s}": type {!s}'.format(prefix, key, type(key)))
deepInspection(value, prefix + '.' + str(key))
elif isinstance(obj, (tuple, list)):
for value in obj:
deepInspection(value, prefix)
elif obj is None or isinstance(obj, (str, int)):
return
else:
log('{} value "{!s}", type {!s}'.format(prefix, obj, type(obj)))
if hateoas == "": if hateoas == "":
return hateoas return hateoas
else: else:
# deepInspection(hateoas, '')
return json.dumps(hateoas, indent=2) return json.dumps(hateoas, indent=2)
...@@ -178,31 +178,33 @@ ...@@ -178,31 +178,33 @@
] ]
) )
.push(function (catalog_json) { .push(function (catalog_json) {
var data = catalog_json._embedded.contents, var data = catalog_json._embedded.contents || [],
summary = catalog_json._embedded.sum, summary = catalog_json._embedded.sum || [],
count = catalog_json._embedded.count, count = catalog_json._embedded.count;
length = data.length, return {
k, "data": {
uri, "rows": data.map(function (item) {
item, var uri = new URI(item._links.self.href);
result = [];
for (k = 0; k < length; k += 1) {
item = data[k];
uri = new URI(item._links.self.href);
delete item._links; delete item._links;
result.push({
id: uri.segment(2),
doc: {},
value: item
});
}
return { return {
data: { "id": uri.segment(2),
rows: result, "doc": {},
total_rows: result.length "value": item
};
}),
"total_rows": data.length
},
"sum": {
"rows": summary.map(function (item, index) {
return {
"id": '/#summary' + index, // this is obviously wrong. @Romain help please!
"doc": {},
"value": item
};
}),
"total_rows": summary.length
}, },
sum: summary, "count": count
count: count
}; };
}); });
}) })
......
...@@ -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.50499.50100.12458</string> </value> <value> <string>963.59331.40212.55432</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>1511939345.58</float> <float>1512454358.33</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
</a> </a>
{{/if}} {{/if}}
{{else}} {{else}}
<a href="{{href}}" class="ui-link">{{text}}</a> <a href="{{href}}" class="ui-link">{{default}}</a>
{{/if}} {{/if}}
</td> </td>
{{/each}} {{/each}}
...@@ -124,7 +124,7 @@ ...@@ -124,7 +124,7 @@
{{#each row_list}} {{#each row_list}}
<tr> <tr>
{{#if ../show_anchor}} {{#if ../show_anchor}}
<td></td> <td>Total</td>
{{/if}} {{/if}}
{{#each cell_list}} {{#each cell_list}}
<td> <td>
...@@ -142,13 +142,9 @@ ...@@ -142,13 +142,9 @@
</script> </script>
<script id="listbox-nav-template" type="text/x-handlebars-template"> <script id="listbox-nav-template" type="text/x-handlebars-template">
<nav class="ui-bar-inherit ui-controlgroup ui-controlgroup-horizontal ui-corner-all ui-paging-menu">
<div class="ui-controlgroup-controls">
<a class="{{previous_classname}}" data-i18n="Previous" href="{{previous_url}}">Previous</a> <a class="{{previous_classname}}" data-i18n="Previous" href="{{previous_url}}">Previous</a>
<a class="{{next_classname}}" data-i18n="Next" href="{{next_url}}">Next</a> <a class="{{next_classname}}" data-i18n="Next" href="{{next_url}}">Next</a>
<span class="ui-btn ui-disabled" data-i18n="{{record}}">{{record}}</span> <span class="ui-disabled ui-right" data-i18n="{{record}}">{{record}}</span>
</div>
</nav>
</script> </script>
<script id="listbox-template" type="text/x-handlebars-template"> <script id="listbox-template" type="text/x-handlebars-template">
......
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>963.50750.47688.32426</string> </value> <value> <string>963.52015.15055.51592</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1511952124.83</float> <float>1512039069.26</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -42,20 +42,15 @@ ...@@ -42,20 +42,15 @@
loading_class_list = ['ui-icon-spinner', 'ui-btn-icon-left'], loading_class_list = ['ui-icon-spinner', 'ui-btn-icon-left'],
disabled_class = 'ui-disabled'; disabled_class = 'ui-disabled';
function renderSubField(gadget, element, sub_field_json) {
function renderEditableField(gadget, element, column_list, field_table) { sub_field_json.editable = sub_field_json.editable && gadget.state.editable;
var i, return gadget.declareGadget(
promise_list = [], 'gadget_erp5_label_field.html',
uid_value_dict = {}, {
uid_value, element: element,
column, scope: sub_field_json.key
line, }
element_list = element.querySelectorAll(".editable_div"); )
gadget.props.listbox_uid_dict = {};
gadget.props.cell_gadget_list = [];
function renderSubCell(element, sub_field_json) {
sub_field_json.editable = sub_field_json.editable && gadget.state.editable; // XXX
return gadget.declareGadget('gadget_erp5_label_field.html', {element: element, scope: sub_field_json.key})
.push(function (cell_gadget) { .push(function (cell_gadget) {
gadget.props.cell_gadget_list.push(cell_gadget); gadget.props.cell_gadget_list.push(cell_gadget);
return cell_gadget.render({ return cell_gadget.render({
...@@ -65,23 +60,23 @@ ...@@ -65,23 +60,23 @@
}); });
}); });
} }
function renderEditableField(gadget, element, column_list, field_table) {
var i,
promise_list = [],
column,
line,
element_list = element.querySelectorAll(".editable_div");
for (i = 0; i < element_list.length; i += 1) { for (i = 0; i < element_list.length; i += 1) {
column = element_list[i].getAttribute("data-column"); column = element_list[i].getAttribute("data-column");
line = element_list[i].getAttribute("data-line"); line = element_list[i].getAttribute("data-line");
if (gadget.props.listbox_uid_dict.key === undefined) {
gadget.props.listbox_uid_dict.key = gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].key; promise_list.push(renderSubField(
gadget.props.listbox_uid_dict.value = [gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].value]; gadget,
uid_value_dict[gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].value] = null;
} else {
uid_value = gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].value;
if (!uid_value_dict.hasOwnProperty(uid_value)) {
uid_value_dict[uid_value] = null;
gadget.props.listbox_uid_dict.value.push(uid_value);
}
}
promise_list.push(renderSubCell(
element_list[i], element_list[i],
field_table[line].cell_list[column] || "")); field_table[line].cell_list[column] || ""
));
} }
return RSVP.all(promise_list); return RSVP.all(promise_list);
} }
...@@ -104,9 +99,9 @@ ...@@ -104,9 +99,9 @@
"column_list": column_list "column_list": column_list
} }
)) ))
.push(function (my_html) { .push(function (table_part_html) {
container = document.createElement(container_name); container = document.createElement(container_name);
container.innerHTML = my_html; container.innerHTML = table_part_html;
return renderEditableField(gadget, container, column_list, row_list); return renderEditableField(gadget, container, column_list, row_list);
}) })
.push(function () { .push(function () {
...@@ -122,8 +117,8 @@ ...@@ -122,8 +117,8 @@
} }
function renderListboxTfoot(gadget, nav, foot_sum) { function renderListboxTfoot(gadget, nav, foot) {
return renderTablePart(gadget, listbox_tfoot_template, foot_sum, "tfoot") return renderTablePart(gadget, listbox_tfoot_template, foot, "tfoot")
.push(function () { .push(function () {
return gadget.translateHtml(listbox_nav_template( return gadget.translateHtml(listbox_nav_template(
{ {
...@@ -153,7 +148,10 @@ ...@@ -153,7 +148,10 @@
// Init local properties // Init local properties
.ready(function () { .ready(function () {
this.props = { this.props = {
// holds references to all editable sub-fields
cell_gadget_list: [], cell_gadget_list: [],
// ERP5 needs listbox_uid:list with UIDs of editable sub-documents
// so it can search for them in REQUEST.form under <field.id>_<sub-document.uid>
listbox_uid_dict: {} listbox_uid_dict: {}
}; };
}) })
...@@ -194,7 +192,7 @@ ...@@ -194,7 +192,7 @@
} }
/** Check whether item is in outer-scoped field_json.column_list */ /** Check whether item is in outer-scoped field_json.column_list */
function is_in_column_list(item) { function isInColumnList(item) {
for (i = 0; i < field_json.column_list.length; i += 1) { for (i = 0; i < field_json.column_list.length; i += 1) {
if (field_json.column_list[i][0] === item[0] && field_json.column_list[i][1] === item[1]) { if (field_json.column_list[i][0] === item[0] && field_json.column_list[i][1] === item[1]) {
return true; return true;
...@@ -205,12 +203,12 @@ ...@@ -205,12 +203,12 @@
// use only visible columns for sort // use only visible columns for sort
if (field_json.sort_column_list.length) { if (field_json.sort_column_list.length) {
sort_column_list = field_json.sort_column_list.filter(is_in_column_list); sort_column_list = field_json.sort_column_list.filter(isInColumnList);
} }
// use only visible columns for search // use only visible columns for search
if (field_json.search_column_list.length) { if (field_json.search_column_list.length) {
search_column_list = field_json.search_column_list.filter(is_in_column_list); search_column_list = field_json.search_column_list.filter(isInColumnList);
} }
search_column_list.push(["searchable_text", "Searchable Text"]); search_column_list.push(["searchable_text", "Searchable Text"]);
...@@ -458,7 +456,7 @@ ...@@ -458,7 +456,7 @@
counter = Math.min(allDocs_result.data.total_rows, lines); counter = Math.min(allDocs_result.data.total_rows, lines);
} }
sort_list = JSON.parse(gadget.state.sort_list_json); sort_list = JSON.parse(gadget.state.sort_list_json);
// Every line points to a sub-document so we need those links
for (i = 0; i < counter; i += 1) { for (i = 0; i < counter; i += 1) {
promise_list.push( promise_list.push(
gadget.getUrlFor({ gadget.getUrlFor({
...@@ -479,33 +477,51 @@ ...@@ -479,33 +477,51 @@
return RSVP.all(promise_list); return RSVP.all(promise_list);
}) })
.push(function (result_list) { .push(function (line_link_list) {
var row_list = [], var row_list = [],
value, value,
cell_list, cell_list,
tmp_url,
listbox_tbody_template; listbox_tbody_template;
// reset list of UIDs of editable sub-documents
gadget.props.listbox_uid_dict = {
key: undefined,
value: []
};
// clear list of previous sub-gadgets
gadget.props.cell_gadget_list = [];
for (i = 0; i < counter; i += 1) { for (i = 0; i < counter; i += 1) {
tmp_url = result_list[i];
cell_list = []; cell_list = [];
for (j = 0; j < column_list.length; j += 1) { for (j = 0; j < column_list.length; j += 1) {
value = allDocs_result.data.rows[i].value[column_list[j][0]] || ""; value = allDocs_result.data.rows[i].value[column_list[j][0]] || "";
// value can be simple string with value in case of non-editable field
// thus we construct basic "field_json" manually and insert the value in "default"
if (typeof value === "string") { if (typeof value === "string") {
value = { value = {
'editable': 0, 'editable': 0,
'default': value 'default': value
}; };
} }
value.href = tmp_url; value.href = line_link_list[i];
value.editable = value.editable && gadget.state.editable; value.editable = value.editable && gadget.state.editable;
value.line = i; value.line = i;
value.column = j; value.column = j;
cell_list.push(value); cell_list.push(value);
} }
// note row's editable UID into gadget.props.listbox_uid_dict if exists to send it back to ERP5
// together with ListBox data. The listbox_uid_dict has quite surprising structure {key: <key>, value: <uid-array>}
if (allDocs_result.data.rows[i].value['listbox_uid:list'] !== undefined) {
gadget.props.listbox_uid_dict.key = allDocs_result.data.rows[i].value['listbox_uid:list'].key;
gadget.props.listbox_uid_dict.value.push(allDocs_result.data.rows[i].value['listbox_uid:list'].value);
// we could come up with better name than "value" for almost everything ^^
} else {
// if the document does not have listbox_uid:list then no gadget should be editable
cell_list.forEach(function (cell) {cell.editable = false; });
}
row_list.push({ row_list.push({
"value": allDocs_result.data.rows[i].value.uid, "uid": allDocs_result.data.rows[i].value.uid,
"jump": tmp_url, "jump": line_link_list[i],
"cell_list": cell_list, "cell_list": cell_list,
"line_icon": gadget.state.line_icon "line_icon": gadget.state.line_icon
}); });
...@@ -517,7 +533,7 @@ ...@@ -517,7 +533,7 @@
listbox_tbody_template = listbox_hidden_tbody_template; listbox_tbody_template = listbox_hidden_tbody_template;
} }
return renderTablePart(gadget, listbox_tbody_template, row_list, "tbody", "tbody"); return renderTablePart(gadget, listbox_tbody_template, row_list, "tbody");
}) })
.push(function () { .push(function () {
var prev_param = {}, var prev_param = {},
...@@ -541,48 +557,50 @@ ...@@ -541,48 +557,50 @@
}) })
.push(function (url_list) { .push(function (url_list) {
var summary = gadget.state.allDocs_result.sum || [], // render summary footer if available var result_sum = (gadget.state.allDocs_result.sum || {}).rows || [], // render summary footer if available
tfoot_sum = summary.map(function (row, row_index) { summary = result_sum.map(function (row, row_index) {
var row_editability = row['listbox_uid:list'] !== undefined;
return { return {
"value": 'summary' + row_index, "uid": 'summary' + row_index,
"cell_list": column_list.map(function (col_name, col_index) { "cell_list": column_list.map(function (col_name, col_index) {
var field_json = row[col_name[0]] || ""; var field_json = row.value[col_name[0]] || "";
if (typeof field_json == "string") { if (typeof field_json === "string") {
field_json = {'default': 'value', 'editable': 0}; field_json = {'default': 'value', 'editable': 0};
} }
field_json.editable = field_json.editable && row_editability;
field_json.column = col_index; field_json.column = col_index;
field_json.line = row_index; field_json.line = row_index;
return field_json; return field_json;
}) })
}; };
}), }),
tfoot_count = { navigation = {
"previous_url": url_list[0], "previous_url": url_list[0],
"next_url": url_list[1], "next_url": url_list[1],
"previous_classname": "ui-btn ui-icon-carat-l ui-btn-icon-left responsive ui-first-child", "previous_classname": "ui-btn ui-icon-carat-l ui-btn-icon-left responsive ui-first-child",
"next_classname": "ui-btn ui-icon-carat-r ui-btn-icon-right responsive ui-last-child" "next_classname": "ui-btn ui-icon-carat-r ui-btn-icon-right responsive ui-last-child"
}; };
tfoot_count.colspan = column_list.length + gadget.state.show_anchor + navigation.colspan = column_list.length + gadget.state.show_anchor +
(gadget.state.line_icon ? 1 : 0); (gadget.state.line_icon ? 1 : 0);
if ((gadget.state.begin_from === 0) && (counter === 0)) { if ((gadget.state.begin_from === 0) && (counter === 0)) {
tfoot_count.record = variable.translated_no_record; navigation.record = variable.translated_no_record;
} else if ((allDocs_result.data.rows.length <= lines) && (gadget.state.begin_from === 0)) { } else if ((allDocs_result.data.rows.length <= lines) && (gadget.state.begin_from === 0)) {
tfoot_count.record = counter + " " + variable.translated_records; navigation.record = counter + " " + variable.translated_records;
} else { } else {
tfoot_count.record = variable.translated_records + " " + (((gadget.state.begin_from + lines) / lines - 1) * lines + 1) + " - " + (((gadget.state.begin_from + lines) / lines - 1) * lines + counter); navigation.record = variable.translated_records + " " + (((gadget.state.begin_from + lines) / lines - 1) * lines + 1) + " - " + (((gadget.state.begin_from + lines) / lines - 1) * lines + counter);
} }
if (gadget.state.begin_from === 0) { if (gadget.state.begin_from === 0) {
tfoot_count.previous_classname += " ui-disabled"; navigation.previous_classname += " ui-disabled";
} }
if (allDocs_result.data.rows.length <= lines) { if (allDocs_result.data.rows.length <= lines) {
tfoot_count.next_classname += " ui-disabled"; navigation.next_classname += " ui-disabled";
} }
return renderListboxTfoot(gadget, tfoot_count, tfoot_sum); return renderListboxTfoot(gadget, navigation, summary);
}) })
.push(function (my_html) { .push(function () {
var loading_element_classList = gadget.element.querySelector(".listboxloader").classList; var loading_element_classList = gadget.element.querySelector(".listboxloader").classList;
loading_element_classList.remove.apply(loading_element_classList, loading_class_list); loading_element_classList.remove.apply(loading_element_classList, loading_class_list);
}); });
...@@ -662,7 +680,7 @@ ...@@ -662,7 +680,7 @@
}, function (error) { }, function (error) {
// do not crash interface if allDocs fails // do not crash interface if allDocs fails
//this will catch all error, not only search criteria invalid error // this will catch all error, not only search criteria invalid error
if (error instanceof RSVP.CancellationError) { if (error instanceof RSVP.CancellationError) {
throw error; throw error;
} }
...@@ -674,10 +692,10 @@ ...@@ -674,10 +692,10 @@
}) })
.declareMethod("getContent", function (options) { .declareMethod("getContent", function (options) {
var form_gadget = this, var gadget = this,
k, k,
field_gadget, sub_gadget,
count = form_gadget.props.cell_gadget_list.length, count = gadget.props.cell_gadget_list.length,
data = {}, data = {},
queue = new RSVP.Queue(); queue = new RSVP.Queue();
...@@ -691,17 +709,18 @@ ...@@ -691,17 +709,18 @@
} }
for (k = 0; k < count; k += 1) { for (k = 0; k < count; k += 1) {
field_gadget = form_gadget.props.cell_gadget_list[k]; sub_gadget = gadget.props.cell_gadget_list[k];
// XXX Hack until better defined // XXX Hack until better defined
if (field_gadget.getContent !== undefined) { if (sub_gadget.getContent !== undefined) {
queue queue
.push(field_gadget.getContent.bind(field_gadget, options)) .push(sub_gadget.getContent.bind(sub_gadget, options))
.push(extendData); .push(extendData);
} }
} }
return queue return queue
.push(function () { .push(function () {
data[form_gadget.props.listbox_uid_dict.key] = form_gadget.props.listbox_uid_dict.value; // gadget.props.listbox_uid_dict.value is an array of UIDs of all editable documents
data[gadget.props.listbox_uid_dict.key] = gadget.props.listbox_uid_dict.value;
return data; return data;
}); });
}) })
......
...@@ -236,7 +236,7 @@ ...@@ -236,7 +236,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>963.50757.35572.58794</string> </value> <value> <string>963.60288.35957.62805</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -254,7 +254,7 @@ ...@@ -254,7 +254,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1511952430.52</float> <float>1512439237.91</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -1221,6 +1221,9 @@ div[data-gadget-scope='erp5_searchfield'] .ui-input-text div[data-gadget-scope=' ...@@ -1221,6 +1221,9 @@ div[data-gadget-scope='erp5_searchfield'] .ui-input-text div[data-gadget-scope='
div[data-gadget-scope='erp5_searchfield'] button { div[data-gadget-scope='erp5_searchfield'] button {
padding: 3pt; padding: 3pt;
} }
.document_table {
/* end-of table */
}
.document_table .ui-table-header { .document_table .ui-table-header {
display: flex; display: flex;
padding-bottom: 6pt; padding-bottom: 6pt;
...@@ -1264,24 +1267,30 @@ div[data-gadget-scope='erp5_searchfield'] button { ...@@ -1264,24 +1267,30 @@ div[data-gadget-scope='erp5_searchfield'] button {
.document_table table { .document_table table {
width: 100%; width: 100%;
text-align: left; text-align: left;
/* end-of tbody, tfoot*/
} }
.document_table table th, .document_table table th,
.document_table table td { .document_table table td {
vertical-align: middle; vertical-align: middle;
padding: 3pt;
} }
.document_table table thead { .document_table table thead,
.document_table table tfoot {
background-color: #0E81C2; background-color: #0E81C2;
color: #FFFFFF; color: #FFFFFF;
} }
.document_table table thead a { .document_table table thead a,
.document_table table tfoot a {
color: #FFFFFF; color: #FFFFFF;
text-decoration: underline; text-decoration: underline;
} }
.document_table table thead tr th { .document_table table thead tr th,
.document_table table tfoot tr th {
padding: 6pt 3pt; padding: 6pt 3pt;
} }
@media not screen and (min-width: 45em) { @media not screen and (min-width: 45em) {
.document_table table thead { .document_table table thead,
.document_table table tfoot {
display: none; display: none;
} }
} }
...@@ -1301,7 +1310,6 @@ div[data-gadget-scope='erp5_searchfield'] button { ...@@ -1301,7 +1310,6 @@ div[data-gadget-scope='erp5_searchfield'] button {
@media not screen and (max-width: 85em), only screen and (min-width: 45em) and (max-width: 85em) { @media not screen and (max-width: 85em), only screen and (min-width: 45em) and (max-width: 85em) {
.document_table table tbody a { .document_table table tbody a {
display: block; display: block;
padding: 3pt;
} }
} }
@media not screen and (min-width: 45em) { @media not screen and (min-width: 45em) {
...@@ -1370,41 +1378,42 @@ div[data-gadget-scope='erp5_searchfield'] button { ...@@ -1370,41 +1378,42 @@ div[data-gadget-scope='erp5_searchfield'] button {
content: " ~ "; content: " ~ ";
} }
} }
.document_table table tfoot .ui-controlgroup-controls { .document_table nav {
display: flex; display: flex;
padding-top: 6pt; padding-top: 6pt;
border-top: 2px solid rgba(0, 0, 0, 0.14902); border-top: 2px solid rgba(0, 0, 0, 0.14902);
} }
.document_table table tfoot .ui-controlgroup-controls span { .document_table nav span {
opacity: .3; opacity: .3;
flex: 2; flex: 2;
text-align: right; text-align: right;
float: right;
} }
.document_table table tfoot .ui-controlgroup-controls a { .document_table nav a {
padding: 6pt; padding: 6pt;
border: 1px solid rgba(0, 0, 0, 0.14); border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 0.325em; border-radius: 0.325em;
margin-right: 6pt; margin-right: 6pt;
} }
.document_table table tfoot .ui-controlgroup-controls a:last-of-type { .document_table nav a:last-of-type {
margin-right: 0; margin-right: 0;
} }
.document_table table tfoot .ui-controlgroup-controls a:hover, .document_table nav a:hover,
.document_table table tfoot .ui-controlgroup-controls a:active { .document_table nav a:active {
background-color: #e0e0e0; background-color: #e0e0e0;
} }
@media not screen and (min-width: 45em) { @media not screen and (min-width: 45em) {
.document_table table tfoot .ui-controlgroup-controls a { .document_table nav a {
overflow: hidden; overflow: hidden;
text-indent: -9999px; text-indent: -9999px;
white-space: nowrap; white-space: nowrap;
} }
} }
.document_table table tfoot .ui-controlgroup-controls a::before { .document_table nav a::before {
margin-right: 6pt; margin-right: 6pt;
} }
@media not screen and (min-width: 45em) { @media not screen and (min-width: 45em) {
.document_table table tfoot .ui-controlgroup-controls a::before { .document_table nav a::before {
float: left; float: left;
text-indent: 6pt; text-indent: 6pt;
} }
......
...@@ -242,7 +242,7 @@ ...@@ -242,7 +242,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>962.1204.63259.57958</string> </value> <value> <string>963.54869.55137.61115</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -260,7 +260,7 @@ ...@@ -260,7 +260,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1505133981.01</float> <float>1512455490.33</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -1458,9 +1458,10 @@ div[data-gadget-scope='erp5_searchfield'] { ...@@ -1458,9 +1458,10 @@ div[data-gadget-scope='erp5_searchfield'] {
th, td { th, td {
// line-height: 1.5em; // line-height: 1.5em;
vertical-align: middle; vertical-align: middle;
padding: @half-margin-size;
} }
thead { thead, tfoot {
background-color: @colorsubheaderbackground; background-color: @colorsubheaderbackground;
color: @white; color: @white;
...@@ -1500,7 +1501,6 @@ div[data-gadget-scope='erp5_searchfield'] { ...@@ -1500,7 +1501,6 @@ div[data-gadget-scope='erp5_searchfield'] {
@media @desktop, @tablet { @media @desktop, @tablet {
a { a {
display: block; display: block;
padding: @half-margin-size;
} }
} }
...@@ -1580,10 +1580,10 @@ div[data-gadget-scope='erp5_searchfield'] { ...@@ -1580,10 +1580,10 @@ div[data-gadget-scope='erp5_searchfield'] {
} }
} }
} }
} } /* end-of tbody, tfoot*/
} /* end-of table */
tfoot .ui-controlgroup-controls { nav {
display: flex; display: flex;
padding-top: @margin-size; padding-top: @margin-size;
border-top: 2px solid rgba(0, 0, 0, 0.14902); border-top: 2px solid rgba(0, 0, 0, 0.14902);
...@@ -1592,6 +1592,7 @@ div[data-gadget-scope='erp5_searchfield'] { ...@@ -1592,6 +1592,7 @@ div[data-gadget-scope='erp5_searchfield'] {
opacity: .3; opacity: .3;
flex: 2; flex: 2;
text-align: right; text-align: right;
float: right;
} }
a { a {
padding: @margin-size; padding: @margin-size;
...@@ -1621,8 +1622,6 @@ div[data-gadget-scope='erp5_searchfield'] { ...@@ -1621,8 +1622,6 @@ div[data-gadget-scope='erp5_searchfield'] {
} }
} }
} }
}
} }
/********************************************** /**********************************************
......
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