From 4a9bba4944db62d06ec9ae6655d507348ef8a46d Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Tue, 17 Dec 2013 14:45:30 +0100 Subject: [PATCH] rough repairs of slickgrid but this is still very far off --- plugins/querygrid/__init__.py | 19 +- plugins/querygrid/static/js/querygrid.js | 112 +- .../static/js/slick.unfolddataview.js | 1080 +++++++++++++++++ .../static/js/slick.unfoldselection.js | 191 +++ plugins/querytable/static/js/querytable.js | 2 +- portal/sliceview.py | 5 +- sample/querygridview.py | 2 +- 7 files changed, 1340 insertions(+), 71 deletions(-) create mode 100644 plugins/querygrid/static/js/slick.unfolddataview.js create mode 100644 plugins/querygrid/static/js/slick.unfoldselection.js diff --git a/plugins/querygrid/__init__.py b/plugins/querygrid/__init__.py index 22a2aabd..587637a6 100644 --- a/plugins/querygrid/__init__.py +++ b/plugins/querygrid/__init__.py @@ -93,20 +93,21 @@ Current implementation makes the following assumptions # but triggers js errors when included - probably/maybe because of the jquery version ? # it might be responsible for not being able to select a row by clicking anywhere in it ? # "http://mleibman.github.io/SlickGrid/lib/jquery-ui-1.8.16.custom.min.js", - "js/jquery.event.drag-2.2.js", # from slickgrid/lib + "js/jquery.event.drag-2.2.js", # from slickgrid/lib "js/slick.core.js", - "js/slick.autotooltips.js", # from slickgrid/plugins/ + "js/slick.autotooltips.js", # from slickgrid/plugins/ "js/slick.cellrangedecorator.js", # from slickgrid/plugins/ - "js/slick.cellrangeselector.js", # from slickgrid/plugins/ - "js/slick.cellcopymanager.js", # from slickgrid/plugins/ - "js/slick.cellselectionmodel.js", # from slickgrid/plugins/ - "js/slick.rowselectionmodel.js", # from slickgrid/plugins/ - "js/slick.checkboxselectcolumn.js", # from slickgrid/plugins/ + "js/slick.cellrangeselector.js", # from slickgrid/plugins/ + "js/slick.cellcopymanager.js", # from slickgrid/plugins/ +# "js/slick.cellselectionmodel.js", # from slickgrid/plugins/ + "js/slick.rowselectionmodel.js", # from slickgrid/plugins/ + "js/slick.checkboxselectcolumn.js", # from slickgrid/plugins/ "js/slick.columnpicker.js", # from slickgrid/controls/ "js/slick.formatters.js", "js/slick.editors.js", "js/slick.grid.js", - "js/slick.dataview.js", + "js/slick.unfoldselection.js", # from plugins/querygrid + "js/slick.unfolddataview.js", # from plugins/querygrid # "js/dataTables.js", "js/dataTables.bootstrap.js", "js/with-datatables.js", "js/manifold.js", "js/manifold-query.js", @@ -134,4 +135,4 @@ Current implementation makes the following assumptions 'query_uuid', 'query_all_uuid', 'checkboxes', 'datatables_options', 'columns','hidden_columns', - 'id_key',] + 'init_key',] diff --git a/plugins/querygrid/static/js/querygrid.js b/plugins/querygrid/static/js/querygrid.js index b4c7b293..f54d1437 100644 --- a/plugins/querygrid/static/js/querygrid.js +++ b/plugins/querygrid/static/js/querygrid.js @@ -1,5 +1,6 @@ +// -*- js-indent-tab:2 -*- /** - * Description: display a query result in a slickgrid-powered + * Description: display a query result in a slickgrid-powered table * Copyright (c) 2012-2013 UPMC Sorbonne Universite - INRIA * License: GPLv3 */ @@ -9,9 +10,6 @@ * * This is very rough for now and not deemed working * - * Also it still requires adaptation for the init_key / init_id / canonical_key / id business - * if the basic logic was to become usable - * * WARNINGS */ @@ -60,20 +58,25 @@ this.elmt().on('show', this, this.on_show); var query = manifold.query_store.find_analyzed_query(this.options.query_uuid); - this.method = query.object; - - // xxx beware that this.key needs to contain a key that all records will have - // in general query_all will return well populated records, but query - // returns records with only the fields displayed on startup. - this.key = (this.options.id_key); - if (! this.key) { - // if not specified by caller, decide from metadata - var keys = manifold.metadata.get_key(this.method); - this.key = (keys && keys.length == 1) ? keys[0] : null; - } - if (! this.key) messages.warning("querygrid.init could not kind valid key"); - - if (debug) messages.debug("querygrid: key="+this.key); + this.object = query.object; + + //// we need 2 different keys + // * canonical_key is the primary key as derived from metadata (typically: urn) + // and is used to communicate about a given record with the other plugins + // * init_key is a key that both kinds of records + // (i.e. records returned by both queries) must have (typically: hrn or hostname) + // in general query_all will return well populated records, but query + // returns records with only the fields displayed on startup + var keys = manifold.metadata.get_key(this.object); + this.canonical_key = (keys && keys.length == 1) ? keys[0] : undefined; + // + this.init_key = this.options.init_key; + // have init_key default to canonical_key + this.init_key = this.init_key || this.canonical_key; + // sanity check + if ( ! this.init_key ) messages.warning ("QueryGrid : cannot find init_key"); + if ( ! this.canonical_key ) messages.warning ("QueryGrid : cannot find canonical_key"); + if (debug) messages.debug("querygrid: canonical_key="+this.canonical_key+" init_key="+this.init_key); /* Setup query and record handlers */ this.listen_query(options.query_uuid); @@ -120,13 +123,16 @@ }; this.slick_data = []; - this.slick_dataview = new Slick.Data.DataView(); + this.slick_dataview = new Slick.Data.UnfoldDataView(); +// capturing for debug +window.dv=this.slick_dataview; var self=this; this.slick_dataview.onRowCountChanged.subscribe ( function (e,args) { self.slick_grid.updateRowCount(); self.slick_grid.autosizeColumns(); self.slick_grid.render(); }); + var selector="#grid-"+this.options.domid; if (debug_deep) { @@ -140,7 +146,8 @@ } this.slick_grid = new Slick.Grid(selector, this.slick_dataview, this.slick_columns, this.slick_options); - this.slick_grid.setSelectionModel (new Slick.RowSelectionModel ({selectActiveRow: false})); +// this.slick_grid.setSelectionModel (new Slick.RowSelectionModel ({selectActiveRow: false})); + this.slick_grid.setSelectionModel (new Slick.UnfoldSelectionModel({selectActiveRow: false})); this.slick_grid.registerPlugin (checkbox_selector); // autotooltips: for showing the full column name when ellipsed var auto_tooltips = new Slick.AutoTooltips ({ enableForHeaderCells: true }); @@ -148,8 +155,6 @@ this.columnpicker = new Slick.Controls.ColumnPicker (this.slick_columns, this.slick_grid, this.slick_options) - g=this.slick_grid; - }, // initialize_table new_record: function(record) { @@ -158,7 +163,7 @@ clear_table: function() { this.slick_data=[]; - this.slick_dataview.setItems(this.slick_data,this.key); + this.slick_dataview.setItems(this.slick_data,this.init_key,this.canonical_key); }, redraw_table: function() { @@ -241,10 +246,10 @@ on_new_record: function(record) { if (this.received_all_query) { // if the 'all' query has been dealt with already we may turn on the checkbox - this._set_checkbox(record, true); + this._set_checkbox_from_record(record, true); } else { // otherwise we need to remember that and do it later on - if (debug) messages.debug("Remembering record to check " + record[this.key]); + if (debug) messages.debug("Remembering record to check, "+this.init_key+'='+ record[this.init_key]); this.buffered_records_to_check.push(record); } }, @@ -270,11 +275,11 @@ switch(data.request) { case FIELD_REQUEST_ADD: case FIELD_REQUEST_ADD_RESET: - this._set_checkbox(data.value, true); + this._set_checkbox_from_data(data.value, true); break; case FIELD_REQUEST_REMOVE: case FIELD_REQUEST_REMOVE_RESET: - this._set_checkbox(data.value, false); + this._set_checkbox_from_data(data.value, false); break; default: break; @@ -287,11 +292,11 @@ switch(data.request) { case FIELD_REQUEST_ADD: case FIELD_REQUEST_ADD_RESET: - this._set_checkbox(data.value, true); + this._set_checkbox_from_data(data.value, true); break; case FIELD_REQUEST_REMOVE: case FIELD_REQUEST_REMOVE_RESET: - this._set_checkbox(data.value, false); + this._set_checkbox_from_data(data.value, false); break; default: break; @@ -315,8 +320,8 @@ on_all_query_done: function() { var start=new Date(); if (debug) messages.debug("1-shot initializing slickgrid content with " + this.slick_data.length + " lines"); - // use this.key as the key for identifying rows - this.slick_dataview.setItems (this.slick_data, this.key); + // use this.init_key as the key for identifying rows + this.slick_dataview.setItems (this.slick_data, this.init_key,this.canonical_key); var duration=new Date()-start; if (debug) messages.debug("setItems " + duration + " ms"); if (debug_deep) { @@ -329,7 +334,7 @@ // checkboxes on the fly at that time (dom not yet created) $.each(this.buffered_records_to_check, function(i, record) { if (debug) messages.debug ("delayed turning on checkbox " + i + " record= " + record); - self._set_checkbox(record, true); + self._set_checkbox_from_record(record, true); }); this.buffered_records_to_check = []; @@ -344,38 +349,33 @@ /************************** PRIVATE METHODS ***************************/ - _set_checkbox: function(record, checked) { - /* Default: checked = true */ - if (checked === undefined) checked = true; - - var id; - /* The function accepts both records and their key */ - switch (manifold.get_type(record)) { - case TYPE_VALUE: - id = record; - break; - case TYPE_RECORD: - /* XXX Test the key before ? */ - id = record[this.key]; - break; - default: - throw "Not implemented"; - break; - } + _set_checkbox_from_record : function(record, checked) { + var init_id = record[this.init_key]; + if (debug) messages.debug("querygrid.set_checkbox_from_record, init_id="+init_id); + var index = this.slick_dataview.getIdxById(init_id); + this._set_checkbox_from_index (index,checked); + }, + _set_checkbox_from_data : function (id, checked) { + if (debug) messages.debug("querygrid.set_checkbox_from_data, id="+id); + // this is a local addition to mainstream dataview + // it's kind if slow in this first implementation (no hashing) + // but we should not notice that much + var index = this.slick_dataview.getIdxByIdKey(id,this.canonical_key); + this._set_checkbox_from_index (index,checked); + }, - if (id === undefined) { - messages.warning("querygrid._set_checkbox record has no id to figure which line to tick"); - return; - } - var index = this.slick_dataview.getIdxById(id); + _set_checkbox_from_index : function (index, checked) { + if (index === undefined) { messages.warn("querygrid.set_checkbox - cannot find index"); return;} + if (checked === undefined) checked = true; var selectedRows=this.slick_grid.getSelectedRows(); if (checked) // add index in current list selectedRows=selectedRows.concat(index); else // remove index from current list selectedRows=selectedRows.filter(function(idx) {return idx!=index;}); + // set new selection this.slick_grid.setSelectedRows(selectedRows); - }, + }, // initializing checkboxes // have tried 2 approaches, but none seems to work as we need it diff --git a/plugins/querygrid/static/js/slick.unfolddataview.js b/plugins/querygrid/static/js/slick.unfolddataview.js new file mode 100644 index 00000000..83dc040f --- /dev/null +++ b/plugins/querygrid/static/js/slick.unfolddataview.js @@ -0,0 +1,1080 @@ +// -*- js-indent-level:2 -*- +// originally cloned from slick.dataview.js +// actually I ended up just adding a method here (getIdxByIdKey) +// so there probably are much smarter ways to achieve this +(function ($) { + $.extend(true, window, { + Slick: { + Data: { + UnfoldDataView: UnfoldDataView, + Aggregators: { + Avg: AvgAggregator, + Min: MinAggregator, + Max: MaxAggregator, + Sum: SumAggregator + } + } + } + }); + + + /*** + * A sample Model implementation. + * Provides a filtered view of the underlying data. + * + * Relies on the data item having an "id" property uniquely identifying it. + */ + function UnfoldDataView(options) { + var self = this; + + var defaults = { + groupItemMetadataProvider: null, + inlineFilters: false + }; + + + // private + var idProperty = "id"; // property holding a unique row id + var items = []; // data by index + var rows = []; // data by row + var idxById = {}; // indexes by id + var rowsById = null; // rows by id; lazy-calculated + var filter = null; // filter function + var updated = null; // updated item ids + var suspend = false; // suspends the recalculation + var sortAsc = true; + var fastSortField; + var sortComparer; + var refreshHints = {}; + var prevRefreshHints = {}; + var filterArgs; + var filteredItems = []; + var compiledFilter; + var compiledFilterWithCaching; + var filterCache = []; + + // grouping + var groupingInfoDefaults = { + getter: null, + formatter: null, + comparer: function(a, b) { return a.value - b.value; }, + predefinedValues: [], + aggregators: [], + aggregateEmpty: false, + aggregateCollapsed: false, + aggregateChildGroups: false, + collapsed: false, + displayTotalsRow: true + }; + var groupingInfos = []; + var groups = []; + var toggledGroupsByLevel = []; + var groupingDelimiter = ':|:'; + + var pagesize = 0; + var pagenum = 0; + var totalRows = 0; + + // events + var onRowCountChanged = new Slick.Event(); + var onRowsChanged = new Slick.Event(); + var onPagingInfoChanged = new Slick.Event(); + + options = $.extend(true, {}, defaults, options); + + + function beginUpdate() { + suspend = true; + } + + function endUpdate() { + suspend = false; + refresh(); + } + + function setRefreshHints(hints) { + refreshHints = hints; + } + + function setFilterArgs(args) { + filterArgs = args; + } + + // the unfold addition is for spotting an index based on another key + // this is kind of slow for now but minimixes the changes to this mainstream code + function getIdxByIdKey(matching_id,matching_key) { + var id; + for (var i = 0, l = items.length; i < l; i++) { + id = items[i][matching_key]; + if (id === undefined) { messages.warning ("ignoring record with no " + matching_key); continue; } + if (id == matching_id) return i; + } + return None; + } + + function updateIdxById(startingIndex) { + startingIndex = startingIndex || 0; + var id; + for (var i = startingIndex, l = items.length; i < l; i++) { + id = items[i][idProperty]; + if (id === undefined) { + throw "Each data element must implement a unique 'id' property"; + } + idxById[id] = i; + } + } + + function ensureIdUniqueness() { + var id; + for (var i = 0, l = items.length; i < l; i++) { + id = items[i][idProperty]; + if (id === undefined || idxById[id] !== i) { + throw "Each data element must implement a unique 'id' property"; + } + } + } + + function getItems() { + return items; + } + + function setItems(data, objectIdProperty) { + if (objectIdProperty !== undefined) { + idProperty = objectIdProperty; + } + items = filteredItems = data; + idxById = {}; + updateIdxById(); + ensureIdUniqueness(); + refresh(); + } + + function setPagingOptions(args) { + if (args.pageSize != undefined) { + pagesize = args.pageSize; + pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0; + } + + if (args.pageNum != undefined) { + pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)); + } + + onPagingInfoChanged.notify(getPagingInfo(), null, self); + + refresh(); + } + + function getPagingInfo() { + var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1; + return {pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages}; + } + + function sort(comparer, ascending) { + sortAsc = ascending; + sortComparer = comparer; + fastSortField = null; + if (ascending === false) { + items.reverse(); + } + items.sort(comparer); + if (ascending === false) { + items.reverse(); + } + idxById = {}; + updateIdxById(); + refresh(); + } + + /*** + * Provides a workaround for the extremely slow sorting in IE. + * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString + * to return the value of that field and then doing a native Array.sort(). + */ + function fastSort(field, ascending) { + sortAsc = ascending; + fastSortField = field; + sortComparer = null; + var oldToString = Object.prototype.toString; + Object.prototype.toString = (typeof field == "function") ? field : function () { + return this[field] + }; + // an extra reversal for descending sort keeps the sort stable + // (assuming a stable native sort implementation, which isn't true in some cases) + if (ascending === false) { + items.reverse(); + } + items.sort(); + Object.prototype.toString = oldToString; + if (ascending === false) { + items.reverse(); + } + idxById = {}; + updateIdxById(); + refresh(); + } + + function reSort() { + if (sortComparer) { + sort(sortComparer, sortAsc); + } else if (fastSortField) { + fastSort(fastSortField, sortAsc); + } + } + + function setFilter(filterFn) { + filter = filterFn; + if (options.inlineFilters) { + compiledFilter = compileFilter(); + compiledFilterWithCaching = compileFilterWithCaching(); + } + refresh(); + } + + function getGrouping() { + return groupingInfos; + } + + function setGrouping(groupingInfo) { + if (!options.groupItemMetadataProvider) { + options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider(); + } + + groups = []; + toggledGroupsByLevel = []; + groupingInfo = groupingInfo || []; + groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo]; + + for (var i = 0; i < groupingInfos.length; i++) { + var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]); + gi.getterIsAFn = typeof gi.getter === "function"; + + // pre-compile accumulator loops + gi.compiledAccumulators = []; + var idx = gi.aggregators.length; + while (idx--) { + gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]); + } + + toggledGroupsByLevel[i] = {}; + } + + refresh(); + } + + /** + * @deprecated Please use {@link setGrouping}. + */ + function groupBy(valueGetter, valueFormatter, sortComparer) { + if (valueGetter == null) { + setGrouping([]); + return; + } + + setGrouping({ + getter: valueGetter, + formatter: valueFormatter, + comparer: sortComparer + }); + } + + /** + * @deprecated Please use {@link setGrouping}. + */ + function setAggregators(groupAggregators, includeCollapsed) { + if (!groupingInfos.length) { + throw new Error("At least one grouping must be specified before calling setAggregators()."); + } + + groupingInfos[0].aggregators = groupAggregators; + groupingInfos[0].aggregateCollapsed = includeCollapsed; + + setGrouping(groupingInfos); + } + + function getItemByIdx(i) { + return items[i]; + } + + function getIdxById(id) { + return idxById[id]; + } + + function ensureRowsByIdCache() { + if (!rowsById) { + rowsById = {}; + for (var i = 0, l = rows.length; i < l; i++) { + rowsById[rows[i][idProperty]] = i; + } + } + } + + function getRowById(id) { + ensureRowsByIdCache(); + return rowsById[id]; + } + + function getItemById(id) { + return items[idxById[id]]; + } + + function mapIdsToRows(idArray) { + var rows = []; + ensureRowsByIdCache(); + for (var i = 0; i < idArray.length; i++) { + var row = rowsById[idArray[i]]; + if (row != null) { + rows[rows.length] = row; + } + } + return rows; + } + + function mapRowsToIds(rowArray) { + var ids = []; + for (var i = 0; i < rowArray.length; i++) { + if (rowArray[i] < rows.length) { + ids[ids.length] = rows[rowArray[i]][idProperty]; + } + } + return ids; + } + + function updateItem(id, item) { + if (idxById[id] === undefined || id !== item[idProperty]) { + throw "Invalid or non-matching id"; + } + items[idxById[id]] = item; + if (!updated) { + updated = {}; + } + updated[id] = true; + refresh(); + } + + function insertItem(insertBefore, item) { + items.splice(insertBefore, 0, item); + updateIdxById(insertBefore); + refresh(); + } + + function addItem(item) { + items.push(item); + updateIdxById(items.length - 1); + refresh(); + } + + function deleteItem(id) { + var idx = idxById[id]; + if (idx === undefined) { + throw "Invalid id"; + } + delete idxById[id]; + items.splice(idx, 1); + updateIdxById(idx); + refresh(); + } + + function getLength() { + return rows.length; + } + + function getItem(i) { + return rows[i]; + } + + function getItemMetadata(i) { + var item = rows[i]; + if (item === undefined) { + return null; + } + + // overrides for grouping rows + if (item.__group) { + return options.groupItemMetadataProvider.getGroupRowMetadata(item); + } + + // overrides for totals rows + if (item.__groupTotals) { + return options.groupItemMetadataProvider.getTotalsRowMetadata(item); + } + + return null; + } + + function expandCollapseAllGroups(level, collapse) { + if (level == null) { + for (var i = 0; i < groupingInfos.length; i++) { + toggledGroupsByLevel[i] = {}; + groupingInfos[i].collapsed = collapse; + } + } else { + toggledGroupsByLevel[level] = {}; + groupingInfos[level].collapsed = collapse; + } + refresh(); + } + + /** + * @param level {Number} Optional level to collapse. If not specified, applies to all levels. + */ + function collapseAllGroups(level) { + expandCollapseAllGroups(level, true); + } + + /** + * @param level {Number} Optional level to expand. If not specified, applies to all levels. + */ + function expandAllGroups(level) { + expandCollapseAllGroups(level, false); + } + + function expandCollapseGroup(level, groupingKey, collapse) { + toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse; + refresh(); + } + + /** + * @param varArgs Either a Slick.Group's "groupingKey" property, or a + * variable argument list of grouping values denoting a unique path to the row. For + * example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of + * the 'high' setGrouping. + */ + function collapseGroup(varArgs) { + var args = Array.prototype.slice.call(arguments); + var arg0 = args[0]; + if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) { + expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true); + } else { + expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true); + } + } + + /** + * @param varArgs Either a Slick.Group's "groupingKey" property, or a + * variable argument list of grouping values denoting a unique path to the row. For + * example, calling expandGroup('high', '10%') will expand the '10%' subgroup of + * the 'high' setGrouping. + */ + function expandGroup(varArgs) { + var args = Array.prototype.slice.call(arguments); + var arg0 = args[0]; + if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) { + expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false); + } else { + expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false); + } + } + + function getGroups() { + return groups; + } + + function extractGroups(rows, parentGroup) { + var group; + var val; + var groups = []; + var groupsByVal = []; + var r; + var level = parentGroup ? parentGroup.level + 1 : 0; + var gi = groupingInfos[level]; + + for (var i = 0, l = gi.predefinedValues.length; i < l; i++) { + val = gi.predefinedValues[i]; + group = groupsByVal[val]; + if (!group) { + group = new Slick.Group(); + group.value = val; + group.level = level; + group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val; + groups[groups.length] = group; + groupsByVal[val] = group; + } + } + + for (var i = 0, l = rows.length; i < l; i++) { + r = rows[i]; + val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter]; + group = groupsByVal[val]; + if (!group) { + group = new Slick.Group(); + group.value = val; + group.level = level; + group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val; + groups[groups.length] = group; + groupsByVal[val] = group; + } + + group.rows[group.count++] = r; + } + + if (level < groupingInfos.length - 1) { + for (var i = 0; i < groups.length; i++) { + group = groups[i]; + group.groups = extractGroups(group.rows, group); + } + } + + groups.sort(groupingInfos[level].comparer); + + return groups; + } + + // TODO: lazy totals calculation + function calculateGroupTotals(group) { + // TODO: try moving iterating over groups into compiled accumulator + var gi = groupingInfos[group.level]; + var isLeafLevel = (group.level == groupingInfos.length); + var totals = new Slick.GroupTotals(); + var agg, idx = gi.aggregators.length; + while (idx--) { + agg = gi.aggregators[idx]; + agg.init(); + gi.compiledAccumulators[idx].call(agg, + (!isLeafLevel && gi.aggregateChildGroups) ? group.groups : group.rows); + agg.storeResult(totals); + } + totals.group = group; + group.totals = totals; + } + + function calculateTotals(groups, level) { + level = level || 0; + var gi = groupingInfos[level]; + var idx = groups.length, g; + while (idx--) { + g = groups[idx]; + + if (g.collapsed && !gi.aggregateCollapsed) { + continue; + } + + // Do a depth-first aggregation so that parent setGrouping aggregators can access subgroup totals. + if (g.groups) { + calculateTotals(g.groups, level + 1); + } + + if (gi.aggregators.length && ( + gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) { + calculateGroupTotals(g); + } + } + } + + function finalizeGroups(groups, level) { + level = level || 0; + var gi = groupingInfos[level]; + var groupCollapsed = gi.collapsed; + var toggledGroups = toggledGroupsByLevel[level]; + var idx = groups.length, g; + while (idx--) { + g = groups[idx]; + g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey]; + g.title = gi.formatter ? gi.formatter(g) : g.value; + + if (g.groups) { + finalizeGroups(g.groups, level + 1); + // Let the non-leaf setGrouping rows get garbage-collected. + // They may have been used by aggregates that go over all of the descendants, + // but at this point they are no longer needed. + g.rows = []; + } + } + } + + function flattenGroupedRows(groups, level) { + level = level || 0; + var gi = groupingInfos[level]; + var groupedRows = [], rows, gl = 0, g; + for (var i = 0, l = groups.length; i < l; i++) { + g = groups[i]; + groupedRows[gl++] = g; + + if (!g.collapsed) { + rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows; + for (var j = 0, jj = rows.length; j < jj; j++) { + groupedRows[gl++] = rows[j]; + } + } + + if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) { + groupedRows[gl++] = g.totals; + } + } + return groupedRows; + } + + function getFunctionInfo(fn) { + var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/; + var matches = fn.toString().match(fnRegex); + return { + params: matches[1].split(","), + body: matches[2] + }; + } + + function compileAccumulatorLoop(aggregator) { + var accumulatorInfo = getFunctionInfo(aggregator.accumulate); + var fn = new Function( + "_items", + "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" + + accumulatorInfo.params[0] + " = _items[_i]; " + + accumulatorInfo.body + + "}" + ); + fn.displayName = fn.name = "compiledAccumulatorLoop"; + return fn; + } + + function compileFilter() { + var filterInfo = getFunctionInfo(filter); + + var filterBody = filterInfo.body + .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1") + .replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1") + .replace(/return ([^;}]+?)\s*([;}]|$)/gi, + "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2"); + + // This preserves the function template code after JS compression, + // so that replace() commands still work as expected. + var tpl = [ + //"function(_items, _args) { ", + "var _retval = [], _idx = 0; ", + "var $item$, $args$ = _args; ", + "_coreloop: ", + "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ", + "$item$ = _items[_i]; ", + "$filter$; ", + "} ", + "return _retval; " + //"}" + ].join(""); + tpl = tpl.replace(/\$filter\$/gi, filterBody); + tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]); + tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]); + + var fn = new Function("_items,_args", tpl); + fn.displayName = fn.name = "compiledFilter"; + return fn; + } + + function compileFilterWithCaching() { + var filterInfo = getFunctionInfo(filter); + + var filterBody = filterInfo.body + .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1") + .replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1") + .replace(/return ([^;}]+?)\s*([;}]|$)/gi, + "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2"); + + // This preserves the function template code after JS compression, + // so that replace() commands still work as expected. + var tpl = [ + //"function(_items, _args, _cache) { ", + "var _retval = [], _idx = 0; ", + "var $item$, $args$ = _args; ", + "_coreloop: ", + "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ", + "$item$ = _items[_i]; ", + "if (_cache[_i]) { ", + "_retval[_idx++] = $item$; ", + "continue _coreloop; ", + "} ", + "$filter$; ", + "} ", + "return _retval; " + //"}" + ].join(""); + tpl = tpl.replace(/\$filter\$/gi, filterBody); + tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]); + tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]); + + var fn = new Function("_items,_args,_cache", tpl); + fn.displayName = fn.name = "compiledFilterWithCaching"; + return fn; + } + + function uncompiledFilter(items, args) { + var retval = [], idx = 0; + + for (var i = 0, ii = items.length; i < ii; i++) { + if (filter(items[i], args)) { + retval[idx++] = items[i]; + } + } + + return retval; + } + + function uncompiledFilterWithCaching(items, args, cache) { + var retval = [], idx = 0, item; + + for (var i = 0, ii = items.length; i < ii; i++) { + item = items[i]; + if (cache[i]) { + retval[idx++] = item; + } else if (filter(item, args)) { + retval[idx++] = item; + cache[i] = true; + } + } + + return retval; + } + + function getFilteredAndPagedItems(items) { + if (filter) { + var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter; + var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching; + + if (refreshHints.isFilterNarrowing) { + filteredItems = batchFilter(filteredItems, filterArgs); + } else if (refreshHints.isFilterExpanding) { + filteredItems = batchFilterWithCaching(items, filterArgs, filterCache); + } else if (!refreshHints.isFilterUnchanged) { + filteredItems = batchFilter(items, filterArgs); + } + } else { + // special case: if not filtering and not paging, the resulting + // rows collection needs to be a copy so that changes due to sort + // can be caught + filteredItems = pagesize ? items : items.concat(); + } + + // get the current page + var paged; + if (pagesize) { + if (filteredItems.length < pagenum * pagesize) { + pagenum = Math.floor(filteredItems.length / pagesize); + } + paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize); + } else { + paged = filteredItems; + } + + return {totalRows: filteredItems.length, rows: paged}; + } + + function getRowDiffs(rows, newRows) { + var item, r, eitherIsNonData, diff = []; + var from = 0, to = newRows.length; + + if (refreshHints && refreshHints.ignoreDiffsBefore) { + from = Math.max(0, + Math.min(newRows.length, refreshHints.ignoreDiffsBefore)); + } + + if (refreshHints && refreshHints.ignoreDiffsAfter) { + to = Math.min(newRows.length, + Math.max(0, refreshHints.ignoreDiffsAfter)); + } + + for (var i = from, rl = rows.length; i < to; i++) { + if (i >= rl) { + diff[diff.length] = i; + } else { + item = newRows[i]; + r = rows[i]; + + if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) && + item.__group !== r.__group || + item.__group && !item.equals(r)) + || (eitherIsNonData && + // no good way to compare totals since they are arbitrary DTOs + // deep object comparison is pretty expensive + // always considering them 'dirty' seems easier for the time being + (item.__groupTotals || r.__groupTotals)) + || item[idProperty] != r[idProperty] + || (updated && updated[item[idProperty]]) + ) { + diff[diff.length] = i; + } + } + } + return diff; + } + + function recalc(_items) { + rowsById = null; + + if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing || + refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) { + filterCache = []; + } + + var filteredItems = getFilteredAndPagedItems(_items); + totalRows = filteredItems.totalRows; + var newRows = filteredItems.rows; + + groups = []; + if (groupingInfos.length) { + groups = extractGroups(newRows); + if (groups.length) { + calculateTotals(groups); + finalizeGroups(groups); + newRows = flattenGroupedRows(groups); + } + } + + var diff = getRowDiffs(rows, newRows); + + rows = newRows; + + return diff; + } + + function refresh() { + if (suspend) { + return; + } + + var countBefore = rows.length; + var totalRowsBefore = totalRows; + + var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit + + // if the current page is no longer valid, go to last page and recalc + // we suffer a performance penalty here, but the main loop (recalc) remains highly optimized + if (pagesize && totalRows < pagenum * pagesize) { + pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1); + diff = recalc(items, filter); + } + + updated = null; + prevRefreshHints = refreshHints; + refreshHints = {}; + + if (totalRowsBefore != totalRows) { + onPagingInfoChanged.notify(getPagingInfo(), null, self); + } + if (countBefore != rows.length) { + onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self); + } + if (diff.length > 0) { + onRowsChanged.notify({rows: diff}, null, self); + } + } + + function syncGridSelection(grid, preserveHidden) { + var self = this; + var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());; + var inHandler; + + function update() { + if (selectedRowIds.length > 0) { + inHandler = true; + var selectedRows = self.mapIdsToRows(selectedRowIds); + if (!preserveHidden) { + selectedRowIds = self.mapRowsToIds(selectedRows); + } + grid.setSelectedRows(selectedRows); + inHandler = false; + } + } + + grid.onSelectedRowsChanged.subscribe(function(e, args) { + if (inHandler) { return; } + selectedRowIds = self.mapRowsToIds(grid.getSelectedRows()); + }); + + this.onRowsChanged.subscribe(update); + + this.onRowCountChanged.subscribe(update); + } + + function syncGridCellCssStyles(grid, key) { + var hashById; + var inHandler; + + // since this method can be called after the cell styles have been set, + // get the existing ones right away + storeCellCssStyles(grid.getCellCssStyles(key)); + + function storeCellCssStyles(hash) { + hashById = {}; + for (var row in hash) { + var id = rows[row][idProperty]; + hashById[id] = hash[row]; + } + } + + function update() { + if (hashById) { + inHandler = true; + ensureRowsByIdCache(); + var newHash = {}; + for (var id in hashById) { + var row = rowsById[id]; + if (row != undefined) { + newHash[row] = hashById[id]; + } + } + grid.setCellCssStyles(key, newHash); + inHandler = false; + } + } + + grid.onCellCssStylesChanged.subscribe(function(e, args) { + if (inHandler) { return; } + if (key != args.key) { return; } + if (args.hash) { + storeCellCssStyles(args.hash); + } + }); + + this.onRowsChanged.subscribe(update); + + this.onRowCountChanged.subscribe(update); + } + + $.extend(this, { + // methods + "beginUpdate": beginUpdate, + "endUpdate": endUpdate, + "setPagingOptions": setPagingOptions, + "getPagingInfo": getPagingInfo, + "getItems": getItems, + "setItems": setItems, + "setFilter": setFilter, + "sort": sort, + "fastSort": fastSort, + "reSort": reSort, + "setGrouping": setGrouping, + "getGrouping": getGrouping, + "groupBy": groupBy, + "setAggregators": setAggregators, + "collapseAllGroups": collapseAllGroups, + "expandAllGroups": expandAllGroups, + "collapseGroup": collapseGroup, + "expandGroup": expandGroup, + "getGroups": getGroups, + "getIdxById": getIdxById, + "getIdxByIdKey": getIdxByIdKey, + "getRowById": getRowById, + "getItemById": getItemById, + "getItemByIdx": getItemByIdx, + "mapRowsToIds": mapRowsToIds, + "mapIdsToRows": mapIdsToRows, + "setRefreshHints": setRefreshHints, + "setFilterArgs": setFilterArgs, + "refresh": refresh, + "updateItem": updateItem, + "insertItem": insertItem, + "addItem": addItem, + "deleteItem": deleteItem, + "syncGridSelection": syncGridSelection, + "syncGridCellCssStyles": syncGridCellCssStyles, + + // data provider methods + "getLength": getLength, + "getItem": getItem, + "getItemMetadata": getItemMetadata, + + // events + "onRowCountChanged": onRowCountChanged, + "onRowsChanged": onRowsChanged, + "onPagingInfoChanged": onPagingInfoChanged + }); + } + + function AvgAggregator(field) { + this.field_ = field; + + this.init = function () { + this.count_ = 0; + this.nonNullCount_ = 0; + this.sum_ = 0; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + this.count_++; + if (val != null && val !== "" && val !== NaN) { + this.nonNullCount_++; + this.sum_ += parseFloat(val); + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.avg) { + groupTotals.avg = {}; + } + if (this.nonNullCount_ != 0) { + groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_; + } + }; + } + + function MinAggregator(field) { + this.field_ = field; + + this.init = function () { + this.min_ = null; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + if (val != null && val !== "" && val !== NaN) { + if (this.min_ == null || val < this.min_) { + this.min_ = val; + } + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.min) { + groupTotals.min = {}; + } + groupTotals.min[this.field_] = this.min_; + } + } + + function MaxAggregator(field) { + this.field_ = field; + + this.init = function () { + this.max_ = null; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + if (val != null && val !== "" && val !== NaN) { + if (this.max_ == null || val > this.max_) { + this.max_ = val; + } + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.max) { + groupTotals.max = {}; + } + groupTotals.max[this.field_] = this.max_; + } + } + + function SumAggregator(field) { + this.field_ = field; + + this.init = function () { + this.sum_ = null; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + if (val != null && val !== "" && val !== NaN) { + this.sum_ += parseFloat(val); + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.sum) { + groupTotals.sum = {}; + } + groupTotals.sum[this.field_] = this.sum_; + } + } + + // TODO: add more built-in aggregators + // TODO: merge common aggregators in one to prevent needles iterating + +})(jQuery); diff --git a/plugins/querygrid/static/js/slick.unfoldselection.js b/plugins/querygrid/static/js/slick.unfoldselection.js new file mode 100644 index 00000000..982fde08 --- /dev/null +++ b/plugins/querygrid/static/js/slick.unfoldselection.js @@ -0,0 +1,191 @@ +// -*- js-indent-level:2 -*- +// originally cloned from slick.rowselectionmodel.js +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "UnfoldSelectionModel": UnfoldSelectionModel + } + }); + + function UnfoldSelectionModel(options) { + var _grid; + var _ranges = []; + var _self = this; + var _handler = new Slick.EventHandler(); + var _inHandler; + var _options; + var _defaults = { + selectActiveRow: true + }; + + function init(grid) { + messages.debug ("init: incoming grid="+grid+" onclick="+grid.onClick); + _options = $.extend(true, {}, _defaults, options); + _grid = grid; + _handler.subscribe(_grid.onActiveCellChanged, + wrapHandler(handleActiveCellChange)); + _handler.subscribe(_grid.onKeyDown, + wrapHandler(handleKeyDown)); + _handler.subscribe(_grid.onClick, + wrapHandler(handleClick)); + } + + function destroy() { + _handler.unsubscribeAll(); + } + + function wrapHandler(handler) { + return function () { + if (!_inHandler) { + _inHandler = true; + handler.apply(this, arguments); + _inHandler = false; + } + }; + } + + function rangesToRows(ranges) { + var rows = []; + for (var i = 0; i < ranges.length; i++) { + for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) { + rows.push(j); + } + } + return rows; + } + + function rowsToRanges(rows) { + var ranges = []; + var lastCell = _grid.getColumns().length - 1; + for (var i = 0; i < rows.length; i++) { + ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell)); + } + return ranges; + } + + function getRowsRange(from, to) { + var i, rows = []; + for (i = from; i <= to; i++) { + rows.push(i); + } + for (i = to; i < from; i++) { + rows.push(i); + } + return rows; + } + + function getSelectedRows() { + return rangesToRows(_ranges); + } + + function setSelectedRows(rows) { + setSelectedRanges(rowsToRanges(rows)); + } + + function setSelectedRanges(ranges) { + _ranges = ranges; + _self.onSelectedRangesChanged.notify(_ranges); + } + + function getSelectedRanges() { + return _ranges; + } + + function handleActiveCellChange(e, data) { + if (_options.selectActiveRow && data.row != null) { + setSelectedRanges([new Slick.Range(data.row, 0, data.row, _grid.getColumns().length - 1)]); + } + } + + function handleKeyDown(e) { + var activeRow = _grid.getActiveCell(); + if (activeRow && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && (e.which == 38 || e.which == 40)) { + var selectedRows = getSelectedRows(); + selectedRows.sort(function (x, y) { + return x - y + }); + + if (!selectedRows.length) { + selectedRows = [activeRow.row]; + } + + var top = selectedRows[0]; + var bottom = selectedRows[selectedRows.length - 1]; + var active; + + if (e.which == 40) { + active = activeRow.row < bottom || top == bottom ? ++bottom : ++top; + } else { + active = activeRow.row < bottom ? --bottom : --top; + } + + if (active >= 0 && active < _grid.getDataLength()) { + _grid.scrollRowIntoView(active); + _ranges = rowsToRanges(getRowsRange(top, bottom)); + setSelectedRanges(_ranges); + } + + e.preventDefault(); + e.stopPropagation(); + } + } + + function handleClick(e) { + messages.debug("UnfoldSelectionModel.handleClick"); + var cell = _grid.getCellFromEvent(e); + if (!cell || !_grid.canCellBeActive(cell.row, cell.cell)) { + return false; + } + + var selection = rangesToRows(_ranges); + var idx = $.inArray(cell.row, selection); + + if (!e.ctrlKey && !e.shiftKey && !e.metaKey) { + return false; + } + else if (_grid.getOptions().multiSelect) { + if (idx === -1 && (e.ctrlKey || e.metaKey)) { + selection.push(cell.row); + _grid.setActiveCell(cell.row, cell.cell); + } else if (idx !== -1 && (e.ctrlKey || e.metaKey)) { + selection = $.grep(selection, function (o, i) { + return (o !== cell.row); + }); + _grid.setActiveCell(cell.row, cell.cell); + } else if (selection.length && e.shiftKey) { + var last = selection.pop(); + var from = Math.min(cell.row, last); + var to = Math.max(cell.row, last); + selection = []; + for (var i = from; i <= to; i++) { + if (i !== last) { + selection.push(i); + } + } + selection.push(last); + _grid.setActiveCell(cell.row, cell.cell); + } + } + + _ranges = rowsToRanges(selection); + setSelectedRanges(_ranges); + e.stopImmediatePropagation(); + + return true; + } + + $.extend(this, { + "getSelectedRows": getSelectedRows, + "setSelectedRows": setSelectedRows, + + "getSelectedRanges": getSelectedRanges, + "setSelectedRanges": setSelectedRanges, + + "init": init, + "destroy": destroy, + + "onSelectedRangesChanged": new Slick.Event() + }); + } +})(jQuery); diff --git a/plugins/querytable/static/js/querytable.js b/plugins/querytable/static/js/querytable.js index bcb81927..ca2c5da2 100644 --- a/plugins/querytable/static/js/querytable.js +++ b/plugins/querytable/static/js/querytable.js @@ -7,7 +7,7 @@ (function($){ var debug=false; - debug=true +// debug=true var QueryTable = Plugin.extend({ diff --git a/portal/sliceview.py b/portal/sliceview.py index db25e64d..60a95d39 100644 --- a/portal/sliceview.py +++ b/portal/sliceview.py @@ -224,7 +224,6 @@ class SliceView (LoginRequiredAutoLogoutView): # this is the query at the core of the slice list query = sq_resource, query_all = query_resource_all, - # use 'hrn' as the internal unique key for this plugin init_key = main_query_init_key, checkboxes = True, datatables_options = { @@ -242,9 +241,7 @@ class SliceView (LoginRequiredAutoLogoutView): # this is the query at the core of the slice list query = sq_resource, query_all = query_resource_all, - # use 'hrn' as the internal unique key for this plugin - # xxx todo on querygrid as well - # init_key = main_query_init_key, + init_key = main_query_init_key, checkboxes = True, ) diff --git a/sample/querygridview.py b/sample/querygridview.py index 0e2fc8ea..abbecbbf 100644 --- a/sample/querygridview.py +++ b/sample/querygridview.py @@ -51,7 +51,7 @@ class QueryGridView (TemplateView): query = sq_resource, query_all = query_resource_all, # safer to use 'hrn' as the internal unique key for this plugin - id_key = main_query_init_key, + init_key = main_query_init_key, checkboxes = True, datatables_options = { 'iDisplayLength': 25, -- 2.43.0