Commit 8fdc1a15 authored by Tomáš Peterka's avatar Tomáš Peterka Committed by Romain Courteaud

[hal_json+renderjs] Implement "stat" line for ListBox

/reviewed-on nexedi/erp5!528
parent 78bd5926
......@@ -47,10 +47,26 @@ 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):
......@@ -684,6 +700,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"lines": lines,
"default_params": 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') != "",
"query": url_template_dict["jio_search_template"] % {
"query": make_query({
"query": sql_catalog.buildQuery(
......@@ -1478,6 +1496,55 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
contents_list.append(contents_item)
result_dict['_embedded']['contents'] = contents_list
# Compute statistics if the search issuer was ListBox
# or in future if the stats (SUM) are required by JIO call
source_field_meta_type = source_field.meta_type if source_field is not None else ""
if source_field_meta_type == "ProxyField":
source_field_meta_type = source_field.getRecursiveTemplateField().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
# stat_method and count_method back to the caller
# XXX: we should check whether they asked for it
stat_method = source_field.get_value('stat_method')
stat_columns = source_field.get_value('stat_columns')
# support only selection_name for stat methods because any `selection` is deprecated
# and should be removed
# Romain wants full backward compatibility so putting `selection` back in parameters
selection_name = source_field.get_value('selection_name')
if selection_name and 'selection_name' not in catalog_kw:
catalog_kw['selection_name'] = selection_name
catalog_kw['selection'] = context.getPortalObject().portal_selections.getSelectionFor(selection_name, REQUEST)
contents_stat = {}
if len(stat_columns) > 0:
# 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)(REQUEST=REQUEST, **catalog_kw)
contents_stat_list.append(contents_stat)
elif stat_method != "" and stat_method.getMethodName() != list_method:
# 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())(REQUEST=REQUEST, **catalog_kw)
# stat method can return simple dictionary or subscriptable object thus we put it into one-item list
if stat_method_result is not None and not isinstance(stat_method_result, (list, tuple)):
stat_method_result = [stat_method_result, ]
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')
if len(contents_stat_list) > 0:
result_dict['_embedded']['sum'] = contents_stat_list
# We should cleanup the selection if it exists in catalog params BUT
# we cannot because it requires escalated Permission.'modifyPortal' so
# the correct solution would be to ReportSection.popReport but unfortunately
# we don't have it anymore because we are asynchronous
return result_dict
elif mode == 'form':
......
"""Compute stats from actual Foo Lines on a Foo object"""
column_list = ['getQuantity', 'id']
result = {c: 0.0 for c in column_list}
for line in context.contentValues(portal_type="Foo"):
for column in column_list:
value = getattr(line, column)
if callable(value):
value = value()
result[column] = result[column] + float(value)
return [result, ]
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Python Script" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>**kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>FooModule_statMethod</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Python Script</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
counter = 0
for value in context.contentValues():
counter = counter + int(value.getQuantity())
return counter
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Python Script" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>selection, **kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>FooModule_statQuantity</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Python Script</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -178,27 +178,33 @@
]
)
.push(function (catalog_json) {
var data = catalog_json._embedded.contents,
count = data.length,
k,
uri,
item,
result = [];
for (k = 0; k < count; k += 1) {
item = data[k];
uri = new URI(item._links.self.href);
delete item._links;
result.push({
id: uri.segment(2),
doc: {},
value: item
});
}
var data = catalog_json._embedded.contents || [],
summary = catalog_json._embedded.sum || [],
count = catalog_json._embedded.count;
return {
data: {
rows: result,
total_rows: result.length
}
"data": {
"rows": data.map(function (item) {
var uri = new URI(item._links.self.href);
delete item._links;
return {
"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
},
"count": count
};
});
})
......
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>947.45414.13002.10052</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>1449753994.81</float>
<float>1512454358.33</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -60,18 +60,18 @@
</thead>
</table>
</script>
<script id="listbox-hidden-tbody-template" type="text/x-handlebars-template">
<table>
<tbody class="tbody">
{{#each body_value}}
{{#each row_list}}
<tr>
{{#if ../show_anchor}}
<th>
<a class="ui-link ui-btn ui-corner-all ui-icon-carat-r ui-btn-icon-notext" href="{{jump}}"></a>
</th>
{{/if}}
{{#each tr_value}}
{{#each cell_list}}
<td>
{{#if type}}
{{#if editable}}
......@@ -82,7 +82,7 @@
</a>
{{/if}}
{{else}}
<a href="{{href}}" class="ui-link">{{text}}</a>
<a href="{{href}}" class="ui-link">{{default}}</a>
{{/if}}
</td>
{{/each}}
......@@ -101,7 +101,7 @@
<script id="listbox-show-tbody-template" type="text/x-handlebars-template">
<table>
<tbody class="tbody">
{{#each body_value}}
{{#each row_list}}
<tr>
{{#if ../show_anchor}}
<th>
......@@ -111,7 +111,7 @@
<td>
<input value="{{value}}" type="checkbox" checked="true" class="hide_element">
</td>
{{#each tr_value}}
{{#each cell_list}}
<td>
{{#if type}}
{{#if editable}}
......@@ -122,7 +122,7 @@
</a>
{{/if}}
{{else}}
<a href="{{href}}" class="ui-link">{{text}}</a>
<a href="{{href}}" class="ui-link">{{default}}</a>
{{/if}}
</td>
{{/each}}
......@@ -140,19 +140,40 @@
<script id="listbox-tfoot-template" type="text/x-handlebars-template">
<table>
<tfoot class="ui-bar-inherit tfoot">
<th colspan="{{colspan}}">
<div class="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>
</div>
</th>
{{#each row_list}}
<tr>
{{#if ../show_anchor}}
<td>Total</td>
{{/if}}
{{#each cell_list}}
<td>
{{#if type}}
<div class="editable_div" data-column="{{column}}" data-line="{{line}}"></div>
{{else}}
{{#if default}}
{{default}}
{{else}}
{{#unless ../../show_anchor }}
{{#if @first}}
Total
{{/if}}
{{/unless}}
{{/if}}
{{/if}}
</td>
{{/each}}
</tr>
{{/each}}
</tfoot>
</table>
</script>
<script id="listbox-nav-template" type="text/x-handlebars-template">
<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-disabled ui-right" data-i18n="{{record}}">{{record}}</span>
</script>
<script id="listbox-template" type="text/x-handlebars-template">
<div class="ui-table-header ui-header ui-bar-c ui-corner-all">
<h1 data-i18n="{{title}}" class="ui-title ui-override-theme">{{title}}<span> <span class="listboxloader ui-icon-spinner ui-btn-icon-left"></span></span></h1>
......@@ -165,11 +186,11 @@
<tbody></tbody>
<tfoot class="ui-bar-inherit tfoot"></tfoot>
</table>
<nav></nav>
</div>
</script>
<script id="error-message-template" type="text/x-handlebars-template">
<div class="ui-listbox-error">
<a class="ui-btn ui-corner-all ui-btn-inline" href="{{reset_url}}">
......
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>964.27340.60822.54681</string> </value>
<value> <string>964.45882.29366.36147</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,7 +252,7 @@
</tuple>
<state>
<tuple>
<float>1514393372.0</float>
<float>1515514305.55</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -28,6 +28,11 @@
.innerHTML,
listbox_tfoot_template = Handlebars.compile(listbox_tfoot_source),
listbox_nav_source = gadget_klass.__template_element
.getElementById("listbox-nav-template")
.innerHTML,
listbox_nav_template = Handlebars.compile(listbox_nav_source),
listbox_source = gadget_klass.__template_element
.getElementById("listbox-template")
.innerHTML,
......@@ -41,88 +46,80 @@
loading_class_list = ['ui-icon-spinner', 'ui-btn-icon-left'],
disabled_class = 'ui-disabled';
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({
field_type: sub_field_json.type,
field_json: sub_field_json,
label: false
});
});
}
function renderEditableField(gadget, element, column_list) {
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})
.push(function (cell_gadget) {
gadget.props.cell_gadget_list.push(cell_gadget);
return cell_gadget.render({
field_type: sub_field_json.type,
field_json: sub_field_json,
label: false
});
});
}
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(element_list[i],
gadget.state.allDocs_result.data.rows[line].value[column_list[column][0]] || ""));
promise_list.push(renderSubField(
gadget,
element_list[i],
field_table[line].cell_list[column] || ""
));
}
return RSVP.all(promise_list);
}
/**Put resulting `row_list` into `template` together with necessary gadget.state parameters.
function renderListboxTbody(gadget, template, body_value) {
var tmp,
First, it removes all similar containers from within the table! Currently it is tricky
to have multiple tbody/thead/tfoot elements! Feel free to refactor.
Example call: renderTablePart(gadget, compiled_template, row_list, "tbody");
**/
function renderTablePart(gadget, template, row_list, container_name) {
var container,
column_list = JSON.parse(gadget.state.column_list_json);
return gadget.translateHtml(template(
{
"body_value": body_value,
"row_list": row_list,
"show_anchor": gadget.state.show_anchor,
"column_list": column_list
}
))
.push(function (my_html) {
tmp = document.createElement("tbody");
tmp.innerHTML = my_html;
return renderEditableField(gadget, tmp, column_list);
.push(function (table_part_html) {
container = document.createElement(container_name);
container.innerHTML = table_part_html;
return renderEditableField(gadget, container, column_list, row_list);
})
.push(function () {
var table = gadget.element.querySelector("table"),
tbody = table.querySelector("tbody");
table.removeChild(tbody);
table.appendChild(tmp);
old_container = table.querySelector(container_name);
if (old_container) {
table.replaceChild(container, old_container);
} else {
table.appendChild(container);
}
return table;
});
}
function renderListboxTfoot(gadget, foot) {
return gadget.translateHtml(listbox_tfoot_template(
{
"colspan": foot.colspan,
"previous_classname": foot.previous_classname,
"previous_url": foot.previous_url,
"record": foot.record,
"next_classname": foot.next_classname,
"next_url": foot.next_url
}
));
}
/** Clojure to ease finding in lists of lists by the first item **/
function hasSameFirstItem(a) {
return function (b) {
......@@ -137,7 +134,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: {}
};
})
......@@ -272,6 +272,9 @@
sort_list_json: JSON.stringify(result_list[1] || field_json.sort.map(jioize_sort)),
show_anchor: field_json.show_anchor,
show_stat: field_json.show_stat,
show_count: field_json.show_count,
line_icon: field_json.line_icon,
query: field_json.query,
query_string: query_string,
......@@ -438,6 +441,8 @@
});
}
/* Function `fetchLineContent` calls changeState({"allDocs_result": JIO.allDocs()})
so this if gets re-evaluated later with allDocs_result defined. */
if (gadget.state.allDocs_result === undefined) {
// Trigger line content calculation
result_queue
......@@ -453,7 +458,6 @@
} else if ((modification_dict.hasOwnProperty('show_line_selector')) ||
(modification_dict.hasOwnProperty('allDocs_result'))) {
// Render the listbox content
result_queue
.push(function () {
......@@ -463,7 +467,7 @@
counter;
column_list = JSON.parse(gadget.state.column_list_json);
// for actual allDocs_result structure see ref:gadget_erp5_jio.js
if (lines === 0) {
lines = allDocs_result.data.total_rows;
counter = allDocs_result.data.total_rows;
......@@ -471,7 +475,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({
......@@ -492,31 +496,52 @@
return RSVP.all(promise_list);
})
.push(function (result_list) {
var value,
body_value = [],
tr_value = [],
tmp_url,
listbox_tbody_template;
.push(function (line_link_list) {
var row_list = [],
value,
cell_list,
listbox_tbody_template,
setNonEditable = function (cell) {cell.editable = false; };
// 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];
tr_value = [];
cell_list = [];
for (j = 0; j < column_list.length; j += 1) {
value = allDocs_result.data.rows[i].value[column_list[j][0]] || "";
tr_value.push({
"type": value.type,
"editable": value.editable && gadget.state.editable,
"href": tmp_url,
"text": value,
"line": i,
"column": j
});
// value can be simply just a value in case of non-editable field
// thus we construct "field_json" manually and insert the value in "default"
if (value.constructor !== Object) {
value = {
'editable': 0,
'default': value
};
}
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(setNonEditable);
}
body_value.push({
"value": allDocs_result.data.rows[i].value.uid,
"jump": tmp_url,
"tr_value": tr_value,
row_list.push({
"uid": allDocs_result.data.rows[i].value.uid,
"jump": line_link_list[i],
"cell_list": cell_list,
"line_icon": gadget.state.line_icon
});
}
......@@ -527,7 +552,7 @@
listbox_tbody_template = listbox_hidden_tbody_template;
}
return renderListboxTbody(gadget, listbox_tbody_template, body_value);
return renderTablePart(gadget, listbox_tbody_template, row_list, "tbody");
})
.push(function () {
var prev_param = {},
......@@ -551,31 +576,70 @@
})
.push(function (url_list) {
var foot = {};
foot.colspan = column_list.length + gadget.state.show_anchor +
(gadget.state.line_icon ? 1 : 0) + gadget.state.show_line_selector;
foot.previous_classname = "ui-btn ui-icon-carat-l ui-btn-icon-left responsive ui-first-child";
foot.previous_url = url_list[0];
foot.next_classname = "ui-btn ui-icon-carat-r ui-btn-icon-right responsive ui-last-child";
foot.next_url = url_list[1];
var record,
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";
if ((gadget.state.begin_from === 0) && (counter === 0)) {
foot.record = variable.translated_no_record;
record = variable.translated_no_record;
} else if ((allDocs_result.data.rows.length <= lines) && (gadget.state.begin_from === 0)) {
foot.record = counter + " " + variable.translated_records;
record = counter + " " + variable.translated_records;
} else {
foot.record = variable.translated_records + " " + (((gadget.state.begin_from + lines) / lines - 1) * lines + 1) + " - " + (((gadget.state.begin_from + lines) / lines - 1) * lines + counter);
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) {
foot.previous_classname += " ui-disabled";
previous_classname += " ui-disabled";
}
if (allDocs_result.data.rows.length <= lines) {
foot.next_classname += " ui-disabled";
next_classname += " ui-disabled";
}
return renderListboxTfoot(gadget, foot);
return gadget.translateHtml(
listbox_nav_template({
"previous_classname": previous_classname,
"previous_url": previous_url,
"record": record,
"next_classname": next_classname,
"next_url": next_url
})
)
.push(function (listbox_nav_html) {
gadget.element.querySelector('nav').innerHTML = listbox_nav_html;
});
})
.push(function (my_html) {
gadget.element.querySelector(".tfoot").innerHTML = my_html;
.push(function () {
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 {
"uid": 'summary' + row_index,
"cell_list": column_list.map(function (col_name, col_index) {
var field_json = row.value[col_name[0]] || "";
if (field_json.constructor !== Object) {
field_json = {'default': field_json, 'editable': 0};
}
field_json.editable = field_json.editable && row_editability;
field_json.column = col_index;
field_json.line = row_index;
return field_json;
})
};
}),
element;
if (counter === 0) {
// do not render footer (summary) when no data in Listbox because it is ugly
element = gadget.element.querySelector("table tfoot tr");
if (element !== null) {
element.remove();
}
return null;
}
return renderTablePart(gadget, listbox_tfoot_template, summary, "tfoot");
})
.push(function () {
var loading_element_classList = gadget.element.querySelector(".listboxloader").classList;
loading_element_classList.remove.apply(loading_element_classList, loading_class_list);
});
......@@ -616,7 +680,8 @@
var gadget = this,
select_list = [],
limit_options,
limit_options = [],
aggregation_option_list = [],
column_list = JSON.parse(gadget.state.column_list_json),
i;
......@@ -631,6 +696,12 @@
limit_options = [gadget.state.begin_from, gadget.state.lines + 1];
}
if (gadget.state.show_stat === true) {
aggregation_option_list.push("sum");
}
if (gadget.state.show_count === true) {
aggregation_option_list.push("count");
}
return gadget.jio_allDocs({
// XXX Not jIO compatible, but until a better api is found...
......@@ -638,6 +709,7 @@
"query": gadget.state.query_string,
"limit": limit_options,
"select_list": select_list,
// "aggregation": aggregation_option_list
"sort_on": JSON.parse(gadget.state.sort_list_json)
})
.push(function (result) {
......@@ -647,7 +719,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;
}
......
......@@ -75,12 +75,6 @@
<none/>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>gadget_erp5_field_listbox.js</string> </value>
......@@ -242,7 +236,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>964.28739.16428.44868</string> </value>
<value> <string>964.47502.56518.25890</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -260,7 +254,7 @@
</tuple>
<state>
<tuple>
<float>1514478789.32</float>
<float>1515602869.96</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -1305,24 +1305,30 @@ div[data-gadget-scope='erp5_searchfield'] div.search_parsed_value 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;
}
}
......@@ -1342,7 +1348,6 @@ div[data-gadget-scope='erp5_searchfield'] div.search_parsed_value 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) {
......@@ -1411,41 +1416,46 @@ div[data-gadget-scope='erp5_searchfield'] div.search_parsed_value 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::before {
.document_table nav a::before {
margin-right: 6pt;
}
.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;
}
.document_table table tfoot .ui-controlgroup-controls a:last-of-type {
.document_table nav a:last-of-type {
margin-right: 0;
}
.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;
}
}
@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;
}
......
......@@ -1523,9 +1523,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;
......@@ -1565,7 +1566,6 @@ div[data-gadget-scope='erp5_searchfield'] {
@media @desktop, @tablet {
a {
display: block;
padding: @half-margin-size;
}
}
......@@ -1646,40 +1646,43 @@ div[data-gadget-scope='erp5_searchfield'] {
}
}
}
}
nav {
display: flex;
padding-top: @margin-size;
border-top: 2px solid rgba(0, 0, 0, 0.14902);
tfoot .ui-controlgroup-controls {
display: flex;
padding-top: @margin-size;
border-top: 2px solid rgba(0, 0, 0, 0.14902);
span {
opacity: .3;
flex: 2;
text-align: right;
float: right;
}
a {
.button();
span {
opacity: .3;
flex: 2;
text-align: right;
margin-right: @margin-size;
&:last-of-type {
margin-right: 0;
}
a {
.button();
margin-right: @margin-size;
&:last-of-type {
margin-right: 0;
}
&:hover, &:active {
background-color: @colorblocklinkbackground;
}
@media @smartphone {
.hide_text(@width: initial);
}
@media @smartphone {
.hide_text(@width: initial);
}
&::before {
&::before {
@media @smartphone {
float: left;
text-indent: @margin-size;
}
@media @smartphone {
float: left;
text-indent: @margin-size;
}
}
}
}
}
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testEmptyListboxWithStat</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<!--
Ensure empty listbox does not show stat line even though it receives stat data.
-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test RenderJS UI ListBox No Stat Line on No Data</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test RenderJS UI ListBox No Stat Line on No Data</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<!-- Clean Up -->
<tr><td>open</td>
<td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Reset Successfully.</td><td></td></tr>
<!-- Shortcut for full renderjs url -->
<tr><td>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<!-- Let's set up the default sort correctly: id | ASC -->
<tr><td>open</td>
<td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_stat_method=FooModule_statMethod</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Set Successfully.</td><td></td></tr>
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="No records"]</td><td></td></tr>
<tr><td>assertElementNotPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tfoot/tr</td>
<td></td></tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
......@@ -128,11 +128,6 @@
<td>//thead/tr/th[3]</td>
<td>Quantity</td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>//tfoot/tr/th[@colspan="3"]</td>
<td></td>
</tr>
<tr>
......@@ -169,11 +164,6 @@
<td>//thead/tr/th[4]</td>
<td>Quantity</td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>//tfoot/tr/th[@colspan="4"]</td>
<td></td>
</tr>
<!-- Line checkbox -->
......@@ -233,11 +223,6 @@
<td>//thead/tr/th[3]</td>
<td>Quantity</td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>//tfoot/tr/th[@colspan="3"]</td>
<td></td>
</tr>
<!-- only one element present -->
<tr>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testStatColumns</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<!--
Ensure Stat Column methods are executed correctly and result displayed in tfoot element of the listbox table.
- if anchor, then text "Total" is present
- columns which are not present in Stat Columns do not display any data
-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test RenderJS ListBox Stat Columns</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test RenderJS ListBox Stat Columns</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<!-- Clean Up -->
<tr><td>open</td>
<td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Reset Successfully.</td><td></td></tr>
<!-- Shortcut for full renderjs url -->
<tr><td>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<!-- Create Foo objects with IDs 0-9 -->
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=2</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Created Successfully.</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<!-- Let's set up stat column property on listbox -->
<tr><td>open</td>
<td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_stat_columns=getQuantity+%7C+FooModule_statQuantity+%0A+title+%7C+FooModule_statTitle</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Set Successfully.</td><td></td></tr>
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="2 Records"]</td><td></td></tr>
<tr><td>store</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table</td>
<td>listbox_table</td></tr>
<!-- Default sort on ID column has to be ASCENDING -->
<tr><td>assertFloat</td>
<td>${listbox_table}/tbody/tr[1]/td[3]/a</td>
<td>9</td></tr>
<tr><td>assertFloat</td>
<td>${listbox_table}/tbody/tr[2]/td[3]/a</td>
<td>8</td></tr>
<tr><td>assertText</td><!-- This tests that "Total" appears when first column has no stat defined -->
<td>${listbox_table}/tfoot/tr[1]/td[1]</td>
<td>Total</td></tr>
<tr><td>assertText</td><!-- Test multiple Stat Columns -->
<td>${listbox_table}/tfoot/tr[1]/td[2]</td>
<td>Foos</td></tr>
<tr><td>assertFloat</td>
<td>${listbox_table}/tfoot/tr[1]/td[3]</td>
<td>17</td></tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testStatMethod</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<!--
Ensure stat_method gets executed and result displayed in tfoot element of the listbox table.
- if anchor, then text "Total" is present
- columns for which stat_method does not return any data remain empty
-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test RenderJS UI ListBox Stat Method</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test RenderJS UI ListBox Stat Method</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<!-- Clean Up -->
<tr><td>open</td>
<td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Reset Successfully.</td><td></td></tr>
<!-- Shortcut for full renderjs url -->
<tr><td>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<!-- Create Foo objects with IDs 0-9 -->
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=3</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Created Successfully.</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<!-- Let's set up the default sort correctly: id | ASC -->
<tr><td>open</td>
<td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_stat_method=FooModule_statMethod</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Set Successfully.</td><td></td></tr>
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="3 Records"]</td><td></td></tr>
<tr><td>store</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table</td>
<td>listbox_table</td></tr>
<!-- Default sort on ID column has to be ASCENDING -->
<tr><td>assertFloat</td>
<td>${listbox_table}/tbody/tr[1]/td[3]/a</td>
<td>9</td></tr>
<tr><td>assertFloat</td>
<td>${listbox_table}/tbody/tr[2]/td[3]/a</td>
<td>8</td></tr>
<tr><td>assertFloat</td>
<td>${listbox_table}/tbody/tr[3]/td[3]/a</td>
<td>7</td></tr>
<tr><td>assertFloat</td><!-- This tests that "Total" does not appear when first column has stat defined -->
<td>${listbox_table}/tfoot/tr[1]/td[1]</td>
<td>6</td></tr>
<tr><td>assertFloat</td>
<td>${listbox_table}/tfoot/tr[1]/td[3]</td>
<td>24</td></tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testStatMissing</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<!--
Ensure no stat line is displayed when no stat_method and no stat_columns are defined.
-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test RenderJS UI ListBox Stat Missing</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test RenderJS UI ListBox Stat Missing</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<!-- Clean Up -->
<tr><td>open</td>
<td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Reset Successfully.</td><td></td></tr>
<!-- Shortcut for full renderjs url -->
<tr><td>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<!-- Create Foo objects with IDs 0-9 -->
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=3</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Created Successfully.</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<!-- Let's set up the default sort correctly: id | ASC -->
<tr><td>open</td>
<td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Set Successfully.</td><td></td></tr>
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="3 Records"]</td><td></td></tr>
<tr><td>assertElementNotPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tfoot/tr</td>
<td></td></tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
......@@ -247,7 +247,7 @@
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//tfoot//span[contains(@data-i18n, "Records")]</td>
<td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav//span[contains(@data-i18n, "Records")]</td>
<td></td>
</tr>
<tr>
......
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