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

Functional stats line in ListBox

parent a26aa2dd
...@@ -676,8 +676,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key ...@@ -676,8 +676,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"lines": field.get_value('lines'), "lines": field.get_value('lines'),
"default_params": ensure_serializable(default_params), "default_params": ensure_serializable(default_params),
"list_method": list_method_name, "list_method": list_method_name,
"stat_method": field.get_value('stat_method').getMethodName() if field.get_value('stat_method') != "" else "", "show_stat": field.get_value('stat_method') != "" or len(field.get_value('stat_columns')) > 0,
"count_method": field.get_value('count_method').getMethodName() if field.get_value('count_method') != "" else "", "show_count": field.get_value('count_method') != "",
"query": url_template_dict["jio_search_template"] % { "query": url_template_dict["jio_search_template"] % {
"query": make_query({ "query": make_query({
"query": sql_catalog.buildQuery( "query": sql_catalog.buildQuery(
...@@ -1430,8 +1430,11 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1430,8 +1430,11 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
editable_field_dict = {} editable_field_dict = {}
listbox_form = None listbox_form = None
listbox_field_id = None listbox_field_id = None
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": if source_field is not None and source_field_meta_type == "ListBox":
listbox_field_id = source_field.id listbox_field_id = source_field.id
# XXX Proxy field are not correctly handled in traversed_document of web site # XXX Proxy field are not correctly handled in traversed_document of web site
listbox_form = getattr(traversed_document, source_field.aq_parent.id) listbox_form = getattr(traversed_document, source_field.aq_parent.id)
...@@ -1454,7 +1457,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1454,7 +1457,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
start, num_items = 0, len(search_result_iterable) start, num_items = 0, len(search_result_iterable)
contents_list = [] # resolved fields from the search result contents_list = [] # resolved fields from the search result
result_dict.update({
'_query': query,
'_local_roles': local_roles,
'_limit': limit,
'_select_list': select_list,
'_embedded': {}
})
# now fill in `contents_list` with actual information # now fill in `contents_list` with actual information
# beware that search_result_iterable can hide anything inside! # beware that search_result_iterable can hide anything inside!
for result_index, search_result in enumerate(search_result_iterable): for result_index, search_result in enumerate(search_result_iterable):
...@@ -1514,44 +1523,59 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None, ...@@ -1514,44 +1523,59 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# given search_result. This name can unfortunately mean almost anything from # given search_result. This name can unfortunately mean almost anything from
# a key name to Python Script with variable number of input parameters. # a key name to Python Script with variable number of input parameters.
contents_item[select] = resolve_field(search_result, select, property_getter, property_hasser) contents_item[select] = resolve_field(search_result, select, property_getter, property_hasser)
# endfor select # endfor select
contents_list.append(contents_item) contents_list.append(contents_item)
result_dict.update({ result_dict['_embedded'].update({
'_query': query,
'_local_roles': local_roles,
'_limit': limit,
'_select_list': select_list,
'_embedded': {
'contents': contents_list 'contents': contents_list
}
}) })
# Compute statistics if the search issuer was ListBox # Compute statistics if the search issuer was ListBox
# or in future if the stats (SUM) are required by JIO call # or in future if the stats (SUM) are required by JIO call
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
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 = [] contents_stat_list = []
if source_field is not None and source_field.meta_type == "ListBox":
# in case the search was issued by listbox we can provide results of # in case the search was issued by listbox we can provide results of
# stat_method and count_method back to the caller # stat_method and count_method back to the caller
# XXX: we should check whether they asked for it # XXX: we should check whether they asked for it
stat_method = source_field.get_value('stat_method') stat_method = source_field.get_value('stat_method')
stat_columns = source_field.get_value('stat_columns') stat_columns = source_field.get_value('stat_columns')
context.log('stat_method "{!s}", stat_columns {!s}'.format(stat_method, stat_columns))
# Selection is unfortunatelly required fot stat methods
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)
contents_stat = {} contents_stat = {}
if len(stat_columns) > 0: if len(stat_columns) > 0:
# prefer stat per columns as it is in ListBox # prefer stat per columns as it is in ListBox
# always called on current context # always called on current context
for stat_name, stat_script in stat_columns: for stat_name, stat_script in stat_columns:
contents_stat[stat_name] = getattr(traversed_document, stat_script)(**catalog_kw) contents_stat[stat_name] = getattr(traversed_document, stat_script)(
selection=selection,
selection_name=selection_name,
**catalog_kw)
contents_stat_list.append(contents_stat) contents_stat_list.append(contents_stat)
elif stat_method != "" and stat_method.getMethodName() != list_method_name: elif stat_method != "" and stat_method.getMethodName() != list_method:
# global stat_method is second - should return dictionary or list of dictionaries # general stat_method is second - should return dictionary or list of dictionaries
# where all "fields" should be accessible by its "select" name # where all "fields" should be accessible by their "select" name
contents_stat_list = getattr(traversed_document, stat_method.getMethodName())(**catalog_kw) contents_stat_list = getattr(traversed_document, stat_method.getMethodName())(**catalog_kw)
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))
if len(contents_stat_list) > 0: if len(contents_stat_list) > 0:
result_dict['_embedded'].update({ result_dict['_embedded'].update({
'contents': contents_list 'sum': contents_stat_list
}) })
# We should cleanup the selection if it exists in catalog params BUT # We should cleanup the selection if it exists in catalog params BUT
......
...@@ -179,12 +179,14 @@ ...@@ -179,12 +179,14 @@
) )
.push(function (catalog_json) { .push(function (catalog_json) {
var data = catalog_json._embedded.contents, var data = catalog_json._embedded.contents,
count = data.length, summary = catalog_json._embedded.sum,
count = catalog_json._embedded.count,
length = data.length,
k, k,
uri, uri,
item, item,
result = []; result = [];
for (k = 0; k < count; k += 1) { for (k = 0; k < length; k += 1) {
item = data[k]; item = data[k];
uri = new URI(item._links.self.href); uri = new URI(item._links.self.href);
delete item._links; delete item._links;
...@@ -198,7 +200,9 @@ ...@@ -198,7 +200,9 @@
data: { data: {
rows: result, rows: result,
total_rows: result.length total_rows: result.length
} },
sum: summary,
count: count
}; };
}); });
}) })
......
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>947.45414.13002.10052</string> </value> <value> <string>963.50499.50100.12458</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1449753994.81</float> <float>1511939345.58</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -118,55 +118,37 @@ ...@@ -118,55 +118,37 @@
</table> </table>
</script> </script>
<script id="listbox-tfoot-sum-template" type="text/x-handlebars-template"> <script id="listbox-tfoot-template" type="text/x-handlebars-template">
<table> <table>
<tfoot class="ui-bar-inherit tfoot summary"> <tfoot class="ui-bar-inherit tfoot">
{{#each row_list}} {{#each row_list}}
<tr> <tr>
{{#if ../show_anchor}}
<td></td>
{{/if}}
{{#each cell_list}} {{#each cell_list}}
<td> <td>
{{#if type}} {{#if type}}
{{#if editable}}
<div class="editable_div" data-column="{{column}}" data-line="{{line}}"></div>
{{else}}
<a href="{{href}}" class="ui-link">
<div class="editable_div" data-column="{{column}}" data-line="{{line}}"></div> <div class="editable_div" data-column="{{column}}" data-line="{{line}}"></div>
</a>
{{/if}}
{{else}} {{else}}
<a href="{{href}}" class="ui-link">{{text}}</a> {{text}}
{{/if}} {{/if}}
</td> </td>
{{/each}} {{/each}}
</tr> </tr>
{{/each}} {{/each}}
<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>
</tfoot> </tfoot>
</table> </table>
</script> </script>
<script id="listbox-tfoot-count-template" type="text/x-handlebars-template"> <script id="listbox-nav-template" type="text/x-handlebars-template">
<table> <nav class="ui-bar-inherit ui-controlgroup ui-controlgroup-horizontal ui-corner-all ui-paging-menu">
<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"> <div class="ui-controlgroup-controls">
<a class="{{previous_classname}}" data-i18n="Previous" href="{{previous_url}}">Previous</a> <a class="{{previous_classname}}" data-i18n="Previous" href="{{previous_url}}">Previous</a>
<a class="{{next_classname}}" data-i18n="Next" href="{{next_url}}">Next</a> <a class="{{next_classname}}" data-i18n="Next" href="{{next_url}}">Next</a>
<span class="ui-btn ui-disabled" data-i18n="{{record}}">{{record}}</span> <span class="ui-btn ui-disabled" data-i18n="{{record}}">{{record}}</span>
</div> </div>
</div> </nav>
</th>
</tfoot>
</table>
</script> </script>
<script id="listbox-template" type="text/x-handlebars-template"> <script id="listbox-template" type="text/x-handlebars-template">
...@@ -188,6 +170,7 @@ ...@@ -188,6 +170,7 @@
<tbody></tbody> <tbody></tbody>
<tfoot class="ui-bar-inherit tfoot"></tfoot> <tfoot class="ui-bar-inherit tfoot"></tfoot>
</table> </table>
<nav></nav>
</div> </div>
</script> </script>
......
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>963.47536.5989.57173</string> </value> <value> <string>963.50750.47688.32426</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1511764805.38</float> <float>1511952124.83</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -19,15 +19,15 @@ ...@@ -19,15 +19,15 @@
.innerHTML, .innerHTML,
listbox_show_tbody_template = Handlebars.compile(listbox_show_tbody_source), listbox_show_tbody_template = Handlebars.compile(listbox_show_tbody_source),
listbox_tfoot_sum_source = gadget_klass.__template_element listbox_tfoot_source = gadget_klass.__template_element
.getElementById("listbox-tfoot-sum-template") .getElementById("listbox-tfoot-template")
.innerHTML, .innerHTML,
listbox_tfoot_sum_template = Handlebars.compile(listbox_tfoot_sum_source), listbox_tfoot_template = Handlebars.compile(listbox_tfoot_source),
listbox_tfoot_count_source = gadget_klass.__template_element listbox_nav_source = gadget_klass.__template_element
.getElementById("listbox-tfoot-count-template") .getElementById("listbox-nav-template")
.innerHTML, .innerHTML,
listbox_tfoot_count_template = Handlebars.compile(listbox_tfoot_count_source), listbox_nav_template = Handlebars.compile(listbox_nav_source),
listbox_source = gadget_klass.__template_element listbox_source = gadget_klass.__template_element
.getElementById("listbox-template") .getElementById("listbox-template")
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
disabled_class = 'ui-disabled'; disabled_class = 'ui-disabled';
function renderEditableField(gadget, element, column_list) { function renderEditableField(gadget, element, column_list, field_table) {
var i, var i,
promise_list = [], promise_list = [],
uid_value_dict = {}, uid_value_dict = {},
...@@ -79,8 +79,9 @@ ...@@ -79,8 +79,9 @@
gadget.props.listbox_uid_dict.value.push(uid_value); gadget.props.listbox_uid_dict.value.push(uid_value);
} }
} }
promise_list.push(renderSubCell(element_list[i], promise_list.push(renderSubCell(
gadget.state.allDocs_result.data.rows[line].value[column_list[column][0]] || "")); element_list[i],
field_table[line].cell_list[column] || ""));
} }
return RSVP.all(promise_list); return RSVP.all(promise_list);
} }
...@@ -90,9 +91,9 @@ ...@@ -90,9 +91,9 @@
First, it removes all similar containers from within the table! Currently it is tricky 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. to have multiple tbody/thead/tfoot elements! Feel free to refactor.
Example call: renderListboxTbody(gadget, compiled_template, row_list, "tbody"); Example call: renderTablePart(gadget, compiled_template, row_list, "tbody");
**/ **/
function renderListboxTbody(gadget, template, row_list, container_name) { function renderTablePart(gadget, template, row_list, container_name) {
var container, var container,
column_list = JSON.parse(gadget.state.column_list_json); column_list = JSON.parse(gadget.state.column_list_json);
...@@ -106,28 +107,36 @@ ...@@ -106,28 +107,36 @@
.push(function (my_html) { .push(function (my_html) {
container = document.createElement(container_name); container = document.createElement(container_name);
container.innerHTML = my_html; container.innerHTML = my_html;
return renderEditableField(gadget, container, column_list); return renderEditableField(gadget, container, column_list, row_list);
}) })
.push(function () { .push(function () {
var table = gadget.element.querySelector("table"), var table = gadget.element.querySelector("table"),
old_container = table.querySelector(container_name); old_container = table.querySelector(container_name);
table.removeChild(old_container); if (old_container) {
table.replaceChild(container, old_container);
} else {
table.appendChild(container); table.appendChild(container);
}
return table;
}); });
} }
function renderListboxTfoot(gadget, foot_count, foot_sum) { function renderListboxTfoot(gadget, nav, foot_sum) {
return gadget.translateHtml(listbox_tfoot_count_template( return renderTablePart(gadget, listbox_tfoot_template, foot_sum, "tfoot")
.push(function () {
return gadget.translateHtml(listbox_nav_template(
{ {
"colspan": foot_count.colspan, "previous_classname": nav.previous_classname,
"previous_classname": foot_count.previous_classname, "previous_url": nav.previous_url,
"previous_url": foot_count.previous_url, "record": nav.record,
"record": foot_count.record, "next_classname": nav.next_classname,
"next_classname": foot_count.next_classname, "next_url": nav.next_url
"next_url": foot_count.next_url
} }
)); ));
}).push(function (listbox_nav_html) {
gadget.element.querySelector('nav').innerHTML = listbox_nav_html;
});
} }
/** Clojure to ease finding in lists of lists by the first item **/ /** Clojure to ease finding in lists of lists by the first item **/
...@@ -254,6 +263,9 @@ ...@@ -254,6 +263,9 @@
sort_list_json: JSON.stringify(result_list[1] || field_json.sort.map(jioize_sort)), sort_list_json: JSON.stringify(result_list[1] || field_json.sort.map(jioize_sort)),
show_anchor: field_json.show_anchor, show_anchor: field_json.show_anchor,
show_stat: field_json.show_stat,
show_count: field_json.show_count,
line_icon: field_json.line_icon, line_icon: field_json.line_icon,
query: field_json.query, query: field_json.query,
query_string: query_string, query_string: query_string,
...@@ -412,6 +424,8 @@ ...@@ -412,6 +424,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) { if (gadget.state.allDocs_result === undefined) {
// Trigger line content calculation // Trigger line content calculation
result_queue result_queue
...@@ -427,7 +441,6 @@ ...@@ -427,7 +441,6 @@
} else if ((modification_dict.hasOwnProperty('show_line_selector')) || } else if ((modification_dict.hasOwnProperty('show_line_selector')) ||
(modification_dict.hasOwnProperty('allDocs_result'))) { (modification_dict.hasOwnProperty('allDocs_result'))) {
// Render the listbox content // Render the listbox content
result_queue result_queue
.push(function () { .push(function () {
...@@ -437,7 +450,7 @@ ...@@ -437,7 +450,7 @@
counter; counter;
column_list = JSON.parse(gadget.state.column_list_json); column_list = JSON.parse(gadget.state.column_list_json);
// for actual allDocs_result structure see ref:gadget_erp5_jio.js
if (lines === 0) { if (lines === 0) {
lines = allDocs_result.data.total_rows; lines = allDocs_result.data.total_rows;
counter = allDocs_result.data.total_rows; counter = allDocs_result.data.total_rows;
...@@ -478,14 +491,17 @@ ...@@ -478,14 +491,17 @@
cell_list = []; cell_list = [];
for (j = 0; j < column_list.length; j += 1) { for (j = 0; j < column_list.length; j += 1) {
value = allDocs_result.data.rows[i].value[column_list[j][0]] || ""; value = allDocs_result.data.rows[i].value[column_list[j][0]] || "";
cell_list.push({ if (typeof value === "string") {
"type": value.type, value = {
"editable": value.editable && gadget.state.editable, 'editable': 0,
"href": tmp_url, 'default': value
"text": value, };
"line": i, }
"column": j value.href = tmp_url;
}); value.editable = value.editable && gadget.state.editable;
value.line = i;
value.column = j;
cell_list.push(value);
} }
row_list.push({ row_list.push({
"value": allDocs_result.data.rows[i].value.uid, "value": allDocs_result.data.rows[i].value.uid,
...@@ -501,7 +517,7 @@ ...@@ -501,7 +517,7 @@
listbox_tbody_template = listbox_hidden_tbody_template; listbox_tbody_template = listbox_hidden_tbody_template;
} }
return renderListboxTbody(gadget, listbox_tbody_template, row_list, "tbody"); return renderTablePart(gadget, listbox_tbody_template, row_list, "tbody", "tbody");
}) })
.push(function () { .push(function () {
var prev_param = {}, var prev_param = {},
...@@ -525,13 +541,27 @@ ...@@ -525,13 +541,27 @@
}) })
.push(function (url_list) { .push(function (url_list) {
var tfoot_count = { var summary = gadget.state.allDocs_result.sum || [], // render summary footer if available
tfoot_sum = summary.map(function (row, row_index) {
return {
"value": '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") {
field_json = {'default': 'value', 'editable': 0};
}
field_json.column = col_index;
field_json.line = row_index;
return field_json;
})
};
}),
tfoot_count = {
"previous_url": url_list[0], "previous_url": url_list[0],
"next_url": url_list[1], "next_url": url_list[1],
"previous_classname": "ui-btn ui-icon-carat-l ui-btn-icon-left responsive ui-first-child", "previous_classname": "ui-btn ui-icon-carat-l ui-btn-icon-left responsive ui-first-child",
"next_classname": "ui-btn ui-icon-carat-r ui-btn-icon-right responsive ui-last-child" "next_classname": "ui-btn ui-icon-carat-r ui-btn-icon-right responsive ui-last-child"
}, };
tfoot_sum = {};
tfoot_count.colspan = column_list.length + gadget.state.show_anchor + tfoot_count.colspan = column_list.length + gadget.state.show_anchor +
(gadget.state.line_icon ? 1 : 0); (gadget.state.line_icon ? 1 : 0);
...@@ -553,7 +583,6 @@ ...@@ -553,7 +583,6 @@
return renderListboxTfoot(gadget, tfoot_count, tfoot_sum); return renderListboxTfoot(gadget, tfoot_count, tfoot_sum);
}) })
.push(function (my_html) { .push(function (my_html) {
gadget.element.querySelector(".tfoot").innerHTML = my_html;
var loading_element_classList = gadget.element.querySelector(".listboxloader").classList; var loading_element_classList = gadget.element.querySelector(".listboxloader").classList;
loading_element_classList.remove.apply(loading_element_classList, loading_class_list); loading_element_classList.remove.apply(loading_element_classList, loading_class_list);
}); });
...@@ -594,7 +623,8 @@ ...@@ -594,7 +623,8 @@
var gadget = this, var gadget = this,
select_list = [], select_list = [],
limit_options, limit_options = [],
aggregation_option_list = [],
column_list = JSON.parse(gadget.state.column_list_json), column_list = JSON.parse(gadget.state.column_list_json),
i; i;
...@@ -609,6 +639,12 @@ ...@@ -609,6 +639,12 @@
limit_options = [gadget.state.begin_from, gadget.state.lines + 1]; 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({ return gadget.jio_allDocs({
// XXX Not jIO compatible, but until a better api is found... // XXX Not jIO compatible, but until a better api is found...
...@@ -617,6 +653,7 @@ ...@@ -617,6 +653,7 @@
"limit": limit_options, "limit": limit_options,
"select_list": select_list, "select_list": select_list,
"sort_on": JSON.parse(gadget.state.sort_list_json) "sort_on": JSON.parse(gadget.state.sort_list_json)
// "aggregation": aggregation_option_list
}) })
.push(function (result) { .push(function (result) {
return gadget.changeState({ return gadget.changeState({
......
...@@ -236,7 +236,7 @@ ...@@ -236,7 +236,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>963.47634.31999.4898</string> </value> <value> <string>963.50757.35572.58794</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -254,7 +254,7 @@ ...@@ -254,7 +254,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1511765129.91</float> <float>1511952430.52</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
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