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

WWIP

parent 45641950
......@@ -21,8 +21,13 @@ Only in mode == 'form'
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
import json
from base64 import urlsafe_b64encode, urlsafe_b64decode
......@@ -35,13 +40,32 @@ import re
from zExceptions import Unauthorized
from Products.ERP5Type.Utils import UpperCase
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
from Products.ERP5Type.Log import log
MARKER = []
if REQUEST is None:
REQUEST = context.REQUEST
# raise Unauthorized
if response is None:
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
def byteify(string):
if isinstance(string, dict):
......@@ -53,11 +77,12 @@ def byteify(string):
else:
return string
def ensure_serializable(obj):
def ensureSerializable(obj):
"""Ensure obj and all sub-objects are JSON serializable."""
if isinstance(obj, dict):
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
if isinstance(obj, DateTime):
return obj.ISO()
......@@ -66,17 +91,18 @@ def ensure_serializable(obj):
# we don't check other isinstances - we believe that iterables don't contain 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 ensure_deserialized(obj):
"""Deserialize classes serialized by our own `ensure_serializable`.
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] = ensure_deserialized(obj[key])
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
......@@ -97,16 +123,49 @@ def getProtectedProperty(document, select):
See https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5Form/ListBox.py#L2293
"""
try:
if "." in select:
#see https://lab.nexedi.com/nexedi/erp5/blob/master/product/ERP5Form/ListBox.py#L2293
try:
select = select[select.rindex('.') + 1:]
except ValueError:
pass
return document.getProperty(select, d=None)
except (ConflictError, RuntimeError):
raise
except:
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`.
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):
result[uid] = {'url': portal.abolute_url() + url}
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"):
# search_result = search_result.getObject()
# "Brain" object - which simulates DB Cursor thus result must have UID
contents_uid = search_result.uid
# every document indexed in catalog has to have relativeUrl
contents_relative_url = getRealRelativeUrl(search_result)
......@@ -133,7 +193,7 @@ def object_uids_and_accessors(search_result, result_index, traversed_document):
try:
return doc.hasProperty(attr)
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))
return False
elif hasattr(search_result, "aq_self"):
......@@ -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
def resolve_field(search_result, select, search_property_getter, search_property_hasser):
"""Given `data_source` extract fields defined in `field_list` and render them using `field_template_dict`.
def getAttrFromAnything(search_result, select, search_property_getter, search_property_hasser, kwargs):
"""Given `data_source` extract value named `select` using helper getter/hasser.
: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
......@@ -175,7 +236,7 @@ def resolve_field(search_result, select, search_property_getter, search_property
contents_value = None
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
if "." in select:
......@@ -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
contents_value = getattr(search_result, accessor_name)
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
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
try:
contents_value = getattr(search_result, select, None)
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)
if callable(contents_value):
......@@ -242,7 +303,7 @@ def resolve_field(search_result, select, search_property_getter, search_property
else:
contents_value = contents_value()
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
# make resulting value JSON serializable
......@@ -309,12 +370,15 @@ def getFormRelativeUrl(form):
)[0].relative_url
def getFieldDefault(traversed_document, field, key, value=None):
# REQUEST.get(field.id, field.get_value("default"))
result = traversed_document.Field_getDefaultValue(field, key, value, REQUEST)
if getattr(result, 'translate', None) is not None:
result = "%s" % result
return result
def getFieldDefault(form, field, key, value=None):
"""Get available value for `field` preferably in python-object from REQUEST or from field's default."""
if value is None:
value = REQUEST.form.get(field.id, REQUEST.form.get(key, None)) or field.get_value('default')
if field.has_value("unicode") and field.get_value("unicode") and isinstance(value, 'unicode'):
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):
......@@ -328,6 +392,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if key is None:
key = field.generate_field_key(key_prefix=key_prefix)
if meta_type == "ProxyField":
# resolve the base meta_type
meta_type = field.getRecursiveTemplateField().meta_type
# 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 = {
"type": meta_type,
"title": Base_translateString(field.get_value("title")),
......@@ -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)
result.update({
"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":
return renderField(traversed_document, field, form, value,
meta_type=field.getRecursiveTemplateField().meta_type,
key=key, key_prefix=key_prefix,
selection_params=selection_params)
# start the actuall "switch" on field's meta_type here
if meta_type in ("ListField", "RadioField", "ParallelListField", "MultiListField"):
result.update({
# XXX Message can not be converted to json as is
......@@ -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_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",
"PasswordField", "EditorField"):
if meta_type == "FloatField":
......@@ -396,9 +461,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if v))
if parameters:
result["default"] = '%s?%s' % (result["default"], parameters)
return result
if meta_type == "DateTimeField":
elif meta_type == "DateTimeField":
result.update({
"date_only": field.get_value("date_only"),
"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
"hide_day": field.get_value('hide_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'):
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
result["default"] = date_value
for subkey in ("year", "month", "day", "hour", "minute", "ampm", "timezone"):
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')
translated_portal_type = []
jump_reference_list = []
......@@ -462,11 +525,16 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
for (listbox_path, listbox_name) in listbox_ids:
(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
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
subfield = renderField(context, listbox_form_field, form)
subfield = renderField(context, listbox_form_field, rel_form)
# overwrite, like Base_getRelatedObjectParameter does
if subfield["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
subfield["column_list"].append((tmp_column[0], Base_translateString(tmp_column[1])))
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({
"url": relative_url,
"translated_portal_types": translated_portal_type,
......@@ -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_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":
result["items"] = field.get_value("items"),
return result
if meta_type == "GadgetField":
elif meta_type == "GadgetField":
result.update({
"url": field.get_value("gadget_url"),
"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.
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
# Search for non-editable documents - all reports goes here
# Reports have custom search scripts which wants parameters from the form
# thus we introspect such parameters and try to find them in REQUEST
list_method = None
list_method_name = traversed_document.Listbox_getListMethodName(field)
list_method = field.get_value('list_method') or None
list_method_name = list_method.getMethodName() if list_method is not None else ""
if list_method_name not in ("", "portal_catalog", "searchFolder", "objectValues"):
# we avoid accessing known protected objects and builtin functions above
try:
......@@ -585,32 +655,15 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
except (Unauthorized, AttributeError, ValueError) as error:
# we are touching some specially protected (usually builtin) methods
# 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)
else:
list_method = None
# 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
# 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"):
for list_method_param in list_method.ZScriptHTML_tryParams():
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
if list_method is not None:
kwargsForCallable(list_method, list_method_query_dict, REQUEST)
# Now if the list_method does not specify **kwargs we need to remove
# unwanted parameters like "portal_type" which is everywhere
if "**" not in list_method.params():
......@@ -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),
"list_method": list_method_name,
"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
# 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
"script_id": script.id,
"relative_url": traversed_document.getRelativeUrl().replace("/", "%2F"),
"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 = {}
# 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({
"column_list": column_list,
"search_column_list": search_column_list,
......@@ -674,7 +708,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"show_anchor": field.get_value("anchor"),
"portal_type": portal_type_list,
"lines": field.get_value('lines'),
"default_params": ensure_serializable(default_params),
"default_params": ensureSerializable(default_params),
"list_method": list_method_name,
"show_stat": field.get_value('stat_method') != "" or len(field.get_value('stat_columns')) > 0,
"show_count": field.get_value('count_method') != "",
......@@ -686,9 +720,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
})
if (list_method_custom is not None):
result["list_method_template"] = list_method_custom
return result
if meta_type == "FormBox":
elif meta_type == "FormBox":
embedded_document = {
'_links': {},
'_actions': {},
......@@ -711,9 +744,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
result['_embedded'] = {
'_view': embedded_document
}
return result
if meta_type == "MatrixBox":
elif meta_type == "MatrixBox":
# data are generated by python code for MatrixBox.py
# template_fields are better rendered here because they can be part of "hidden"
# 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
for template_field in template_field_names
if template_field in form},
})
return result
else:
# All other fields are not implemented and we'll return only basic info about them
result["_debug"] = "Unknown field type " + meta_type
if previous_request_field is not None:
REQUEST.other['field_id'] = previous_request_field
return result
......@@ -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
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('form_id', form.id)
field_errors = REQUEST.get('field_errors', {})
#hardcoded
......@@ -860,6 +901,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
if 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)
# this should load selections with correct values - since it is modifying
# 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
report_result['title'] = report_title
report_result_list.append(report_result)
response_dict['report_section_list'] = report_result_list
# end-if report_section
for key, value in previous_request_other.items():
if value is not None:
REQUEST.set(key, value)
# XXX form action update, etc
def renderRawField(field):
......@@ -1329,7 +1376,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
#
# Possible call arguments example:
# 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)
# query: <str> (term for fulltext search)
# 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,
response.setStatus(405)
return ""
# set 'here' for field rendering which contain TALES expressions
REQUEST.set('here', traversed_document)
# in case we have custom list method
catalog_kw = {}
......@@ -1374,7 +1424,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
}
if default_param_json is not None:
catalog_kw.update(
ensure_deserialized(
ensureDeserialized(
byteify(
json.loads(urlsafe_b64decode(default_param_json)))))
if query:
......@@ -1392,7 +1442,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
sort_order = "DESC"
else:
# 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
return (sort_col, sort_order)
......@@ -1414,7 +1464,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
#
# for k, v in catalog_kw.items():
# REQUEST.set(k, v)
context.log('list_method >>> {}({!s})'.format(list_method, catalog_kw))
search_result_iterable = callable_list_method(**catalog_kw)
# 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,
listbox_field_id = source_field.id
# XXX Proxy field are not correctly handled in traversed_document of web site
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:
# See Listbox.py getValueList --> getEditableField & getColumnAliasList method
# 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,
if result_index >= start + num_items:
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 = \
object_uids_and_accessors(search_result, result_index, traversed_document)
anythingUidAndAccessor(search_result, result_index, traversed_document)
# this dict will hold all resolved values
contents_item = {
'_links': {
# _links.self.href is mandatory for JIO so it can create reference to items alone
# _links.self.href is mandatory for JIO so it can create reference to the
# (listbox) item alone
contents_item['_links'] = {
'self': {
"href": default_document_uri_template % {
"root_url": site_root.absolute_url(),
......@@ -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
# only database results can be editable so it belongs here
if editable_field_dict and listbox_field_id:
......@@ -1505,10 +1561,17 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
REQUEST.set('cell', search_result)
# if default value is given by evaluating Tales expression then we only
# 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 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(
traversed_document,
......@@ -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
# given search_result. This name can unfortunately mean almost anything from
# 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
contents_list.append(contents_item)
result_dict['_embedded'].update({
'contents': contents_list
})
result_dict['_embedded']['contents'] = ensureSerializable(contents_list)
# Compute statistics if the search issuer was ListBox
# or in future if the stats (SUM) are required by JIO call
......@@ -1536,8 +1597,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if source_field_meta_type == "ProxyField":
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":
contents_stat_list = []
# 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,
stat_method = source_field.get_value('stat_method')
stat_columns = source_field.get_value('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 = None
if selection_name:
selection_tool = context.getPortalObject().portal_selections
selection = selection_tool.getSelectionFor(selection_name, REQUEST)
if selection_name and 'selection_name' not in catalog_kw:
catalog_kw['selection_name'] = selection_name
context.log('stat_method will receive selection_name "{}"'.format(catalog_kw['selection_name']))
contents_stat = {}
if len(stat_columns) > 0:
# prefer stat per columns as it is in ListBox
# always called on current context
# prefer stat per column (follow original ListBox.py implementation)
for stat_name, stat_script in stat_columns:
contents_stat[stat_name] = getattr(traversed_document, stat_script)(
selection=selection,
selection_name=selection_name,
**catalog_kw)
contents_stat[stat_name] = getattr(traversed_document, stat_script)(**catalog_kw)
contents_stat_list.append(contents_stat)
elif stat_method != "" and stat_method.getMethodName() != list_method:
# general stat_method is second - should return dictionary or list of dictionaries
# where all "fields" should be accessible by their "select" name
contents_stat_list = getattr(traversed_document, stat_method.getMethodName())(**catalog_kw)
# 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 (no "listbox_" prefix)
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 key, value in contents_stat.items():
if key in editable_field_dict:
contents_stat[key] = renderField(
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:
result_dict['_embedded'].update({
'sum': contents_stat_list
})
result_dict['_embedded']['sum'] = ensureSerializable(contents_stat_list)
# We should cleanup the selection if it exists in catalog params BUT
# we cannot because it requires escalated Permission.'modifyPortal' so
......@@ -1683,6 +1739,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
return result_dict
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,
traversed_document=temp_traversed_document,
relative_url=relative_url,
......@@ -1692,7 +1749,22 @@ hateoas = calculateHateoas(is_portal=temp_is_portal, is_site_root=temp_is_site_r
default_param_json=default_param_json,
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 == "":
return hateoas
else:
# deepInspection(hateoas, '')
return json.dumps(hateoas, indent=2)
......@@ -178,31 +178,33 @@
]
)
.push(function (catalog_json) {
var data = catalog_json._embedded.contents,
summary = catalog_json._embedded.sum,
count = catalog_json._embedded.count,
length = data.length,
k,
uri,
item,
result = [];
for (k = 0; k < length; k += 1) {
item = data[k];
uri = new URI(item._links.self.href);
var data = catalog_json._embedded.contents || [],
summary = catalog_json._embedded.sum || [],
count = catalog_json._embedded.count;
return {
"data": {
"rows": data.map(function (item) {
var uri = new URI(item._links.self.href);
delete item._links;
result.push({
id: uri.segment(2),
doc: {},
value: item
});
}
return {
data: {
rows: result,
total_rows: result.length
"id": uri.segment(2),
"doc": {},
"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 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>963.50499.50100.12458</string> </value>
<value> <string>963.59331.40212.55432</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1511939345.58</float>
<float>1512454358.33</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -63,7 +63,7 @@
</a>
{{/if}}
{{else}}
<a href="{{href}}" class="ui-link">{{text}}</a>
<a href="{{href}}" class="ui-link">{{default}}</a>
{{/if}}
</td>
{{/each}}
......@@ -124,7 +124,7 @@
{{#each row_list}}
<tr>
{{#if ../show_anchor}}
<td></td>
<td>Total</td>
{{/if}}
{{#each cell_list}}
<td>
......@@ -142,13 +142,9 @@
</script>
<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="{{next_classname}}" data-i18n="Next" href="{{next_url}}">Next</a>
<span class="ui-btn ui-disabled" data-i18n="{{record}}">{{record}}</span>
</div>
</nav>
<span class="ui-disabled ui-right" data-i18n="{{record}}">{{record}}</span>
</script>
<script id="listbox-template" type="text/x-handlebars-template">
......
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>963.50750.47688.32426</string> </value>
<value> <string>963.52015.15055.51592</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,7 +252,7 @@
</tuple>
<state>
<tuple>
<float>1511952124.83</float>
<float>1512039069.26</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -42,20 +42,15 @@
loading_class_list = ['ui-icon-spinner', 'ui-btn-icon-left'],
disabled_class = 'ui-disabled';
function renderEditableField(gadget, element, column_list, field_table) {
var i,
promise_list = [],
uid_value_dict = {},
uid_value,
column,
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})
function renderSubField(gadget, element, sub_field_json) {
sub_field_json.editable = sub_field_json.editable && gadget.state.editable;
return gadget.declareGadget(
'gadget_erp5_label_field.html',
{
element: element,
scope: sub_field_json.key
}
)
.push(function (cell_gadget) {
gadget.props.cell_gadget_list.push(cell_gadget);
return cell_gadget.render({
......@@ -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) {
column = element_list[i].getAttribute("data-column");
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;
gadget.props.listbox_uid_dict.value = [gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].value];
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(
promise_list.push(renderSubField(
gadget,
element_list[i],
field_table[line].cell_list[column] || ""));
field_table[line].cell_list[column] || ""
));
}
return RSVP.all(promise_list);
}
......@@ -104,9 +99,9 @@
"column_list": column_list
}
))
.push(function (my_html) {
.push(function (table_part_html) {
container = document.createElement(container_name);
container.innerHTML = my_html;
container.innerHTML = table_part_html;
return renderEditableField(gadget, container, column_list, row_list);
})
.push(function () {
......@@ -122,8 +117,8 @@
}
function renderListboxTfoot(gadget, nav, foot_sum) {
return renderTablePart(gadget, listbox_tfoot_template, foot_sum, "tfoot")
function renderListboxTfoot(gadget, nav, foot) {
return renderTablePart(gadget, listbox_tfoot_template, foot, "tfoot")
.push(function () {
return gadget.translateHtml(listbox_nav_template(
{
......@@ -153,7 +148,10 @@
// Init local properties
.ready(function () {
this.props = {
// holds references to all editable sub-fields
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: {}
};
})
......@@ -194,7 +192,7 @@
}
/** 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) {
if (field_json.column_list[i][0] === item[0] && field_json.column_list[i][1] === item[1]) {
return true;
......@@ -205,12 +203,12 @@
// use only visible columns for sort
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
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"]);
......@@ -458,7 +456,7 @@
counter = Math.min(allDocs_result.data.total_rows, lines);
}
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) {
promise_list.push(
gadget.getUrlFor({
......@@ -479,33 +477,51 @@
return RSVP.all(promise_list);
})
.push(function (result_list) {
.push(function (line_link_list) {
var row_list = [],
value,
cell_list,
tmp_url,
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) {
tmp_url = result_list[i];
cell_list = [];
for (j = 0; j < column_list.length; j += 1) {
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") {
value = {
'editable': 0,
'default': value
};
}
value.href = tmp_url;
value.href = line_link_list[i];
value.editable = value.editable && gadget.state.editable;
value.line = i;
value.column = j;
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({
"value": allDocs_result.data.rows[i].value.uid,
"jump": tmp_url,
"uid": allDocs_result.data.rows[i].value.uid,
"jump": line_link_list[i],
"cell_list": cell_list,
"line_icon": gadget.state.line_icon
});
......@@ -517,7 +533,7 @@
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 () {
var prev_param = {},
......@@ -541,48 +557,50 @@
})
.push(function (url_list) {
var summary = gadget.state.allDocs_result.sum || [], // render summary footer if available
tfoot_sum = summary.map(function (row, row_index) {
var result_sum = (gadget.state.allDocs_result.sum || {}).rows || [], // render summary footer if available
summary = result_sum.map(function (row, row_index) {
var row_editability = row['listbox_uid:list'] !== undefined;
return {
"value": 'summary' + row_index,
"uid": 'summary' + row_index,
"cell_list": column_list.map(function (col_name, col_index) {
var field_json = row[col_name[0]] || "";
if (typeof field_json == "string") {
var field_json = row.value[col_name[0]] || "";
if (typeof field_json === "string") {
field_json = {'default': 'value', 'editable': 0};
}
field_json.editable = field_json.editable && row_editability;
field_json.column = col_index;
field_json.line = row_index;
return field_json;
})
};
}),
tfoot_count = {
navigation = {
"previous_url": url_list[0],
"next_url": url_list[1],
"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"
};
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);
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)) {
tfoot_count.record = counter + " " + variable.translated_records;
navigation.record = counter + " " + variable.translated_records;
} 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) {
tfoot_count.previous_classname += " ui-disabled";
navigation.previous_classname += " ui-disabled";
}
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;
loading_element_classList.remove.apply(loading_element_classList, loading_class_list);
});
......@@ -662,7 +680,7 @@
}, function (error) {
// 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) {
throw error;
}
......@@ -674,10 +692,10 @@
})
.declareMethod("getContent", function (options) {
var form_gadget = this,
var gadget = this,
k,
field_gadget,
count = form_gadget.props.cell_gadget_list.length,
sub_gadget,
count = gadget.props.cell_gadget_list.length,
data = {},
queue = new RSVP.Queue();
......@@ -691,17 +709,18 @@
}
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
if (field_gadget.getContent !== undefined) {
if (sub_gadget.getContent !== undefined) {
queue
.push(field_gadget.getContent.bind(field_gadget, options))
.push(sub_gadget.getContent.bind(sub_gadget, options))
.push(extendData);
}
}
return queue
.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;
});
})
......
......@@ -236,7 +236,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>963.50757.35572.58794</string> </value>
<value> <string>963.60288.35957.62805</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -254,7 +254,7 @@
</tuple>
<state>
<tuple>
<float>1511952430.52</float>
<float>1512439237.91</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -1221,6 +1221,9 @@ div[data-gadget-scope='erp5_searchfield'] .ui-input-text div[data-gadget-scope='
div[data-gadget-scope='erp5_searchfield'] button {
padding: 3pt;
}
.document_table {
/* end-of table */
}
.document_table .ui-table-header {
display: flex;
padding-bottom: 6pt;
......@@ -1264,24 +1267,30 @@ div[data-gadget-scope='erp5_searchfield'] button {
.document_table table {
width: 100%;
text-align: left;
/* end-of tbody, tfoot*/
}
.document_table table th,
.document_table table td {
vertical-align: middle;
padding: 3pt;
}
.document_table table thead {
.document_table table thead,
.document_table table tfoot {
background-color: #0E81C2;
color: #FFFFFF;
}
.document_table table thead a {
.document_table table thead a,
.document_table table tfoot a {
color: #FFFFFF;
text-decoration: underline;
}
.document_table table thead tr th {
.document_table table thead tr th,
.document_table table tfoot tr th {
padding: 6pt 3pt;
}
@media not screen and (min-width: 45em) {
.document_table table thead {
.document_table table thead,
.document_table table tfoot {
display: none;
}
}
......@@ -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) {
.document_table table tbody a {
display: block;
padding: 3pt;
}
}
@media not screen and (min-width: 45em) {
......@@ -1370,41 +1378,42 @@ div[data-gadget-scope='erp5_searchfield'] button {
content: " ~ ";
}
}
.document_table table tfoot .ui-controlgroup-controls {
.document_table nav {
display: flex;
padding-top: 6pt;
border-top: 2px solid rgba(0, 0, 0, 0.14902);
}
.document_table table tfoot .ui-controlgroup-controls span {
.document_table nav span {
opacity: .3;
flex: 2;
text-align: right;
float: right;
}
.document_table table tfoot .ui-controlgroup-controls a {
.document_table nav a {
padding: 6pt;
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 0.325em;
margin-right: 6pt;
}
.document_table table tfoot .ui-controlgroup-controls a:last-of-type {
.document_table nav a:last-of-type {
margin-right: 0;
}
.document_table table tfoot .ui-controlgroup-controls a:hover,
.document_table table tfoot .ui-controlgroup-controls a:active {
.document_table nav a:hover,
.document_table nav a:active {
background-color: #e0e0e0;
}
@media not screen and (min-width: 45em) {
.document_table table tfoot .ui-controlgroup-controls a {
.document_table nav a {
overflow: hidden;
text-indent: -9999px;
white-space: nowrap;
}
}
.document_table table tfoot .ui-controlgroup-controls a::before {
.document_table nav a::before {
margin-right: 6pt;
}
@media not screen and (min-width: 45em) {
.document_table table tfoot .ui-controlgroup-controls a::before {
.document_table nav a::before {
float: left;
text-indent: 6pt;
}
......
......@@ -242,7 +242,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>962.1204.63259.57958</string> </value>
<value> <string>963.54869.55137.61115</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -260,7 +260,7 @@
</tuple>
<state>
<tuple>
<float>1505133981.01</float>
<float>1512455490.33</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -1458,9 +1458,10 @@ div[data-gadget-scope='erp5_searchfield'] {
th, td {
// line-height: 1.5em;
vertical-align: middle;
padding: @half-margin-size;
}
thead {
thead, tfoot {
background-color: @colorsubheaderbackground;
color: @white;
......@@ -1500,7 +1501,6 @@ div[data-gadget-scope='erp5_searchfield'] {
@media @desktop, @tablet {
a {
display: block;
padding: @half-margin-size;
}
}
......@@ -1580,10 +1580,10 @@ div[data-gadget-scope='erp5_searchfield'] {
}
}
}
}
} /* end-of tbody, tfoot*/
} /* end-of table */
tfoot .ui-controlgroup-controls {
nav {
display: flex;
padding-top: @margin-size;
border-top: 2px solid rgba(0, 0, 0, 0.14902);
......@@ -1592,6 +1592,7 @@ div[data-gadget-scope='erp5_searchfield'] {
opacity: .3;
flex: 2;
text-align: right;
float: right;
}
a {
padding: @margin-size;
......@@ -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