From: Thierry Parmentelat Date: Fri, 6 Dec 2013 15:36:14 +0000 (+0100) Subject: adding querygrid plugin from the slickgrid branch X-Git-Tag: myslice-0.3-0~91^2~5 X-Git-Url: http://git.onelab.eu/?p=myslice.git;a=commitdiff_plain;h=b3f21324b9818b110814ce42d566b49de06f9a59 adding querygrid plugin from the slickgrid branch --- diff --git a/plugins/querygrid/__init__.py b/plugins/querygrid/__init__.py new file mode 100644 index 00000000..5b0c4662 --- /dev/null +++ b/plugins/querygrid/__init__.py @@ -0,0 +1,137 @@ +from unfold.plugin import Plugin + +class QueryGrid (Plugin): + + """A plugin for displaying a query as a list + +More accurately, we consider a subject entity (say, a slice) +that can be linked to any number of related entities (say, resources, or users) +The 'query' argument will correspond to the subject, while +'query_all' will fetch the complete list of +possible candidates for the relationship. + +Current implementation makes the following assumptions +* query will only retrieve for the related items a list of fields + that corresponds to the initial set of fields displayed in the table +* query_all on the contrary is expected to return the complete set of + available attributes that may be of interest, so that using a QueryEditor + one can easily extend this table without having to query the backend +* checkboxes is a boolean flag, set to true if a rightmost column + with checkboxes is desired +* optionally pass columns as the initial set of columns + if None then this is taken from the query's fields +* id_key is the name of a column used internally in the plugin + for checkboxes management. Caller should specify a column that is present + in the fields returned by 'query' and that has unique values. + If not specified, metadata will be used to find out a primary key. + However in the case of nodes & slice for example, the default key + as returned by the metadata would be 'urn', but it is not necessarily + a good idea to show urn's initially - if at all. + This is why a slice view would use 'hrn' here instead. +* datatables_options are passed to dataTables as-is; + however please refrain from passing an 'aoColumns' + as we use 'aoColumnDefs' instead. +""" + + def __init__ (self, query=None, query_all=None, + checkboxes=False, columns=None, + id_key=None, + datatables_options={}, **settings): + Plugin.__init__ (self, **settings) + self.query = query + # Until we have a proper way to access queries in Python + self.query_all = query_all + self.query_all_uuid = query_all.query_uuid if query_all else None + self.checkboxes = checkboxes + # XXX We need to have some hidden columns until we properly handle dynamic queries + if columns is not None: + self.columns=columns + self.hidden_columns = [] + elif self.query: + self.columns = self.query.fields + if query_all: + # We need a list because sets are not JSON-serializable + self.hidden_columns = list(self.query_all.fields - self.query.fields) + else: + self.hidden_columns = [] + else: + self.columns = [] + self.hidden_columns = [] + # needs to be json-serializable, and sets are not + self.columns=list(self.columns) + self.id_key=id_key + self.datatables_options=datatables_options + # if checkboxes were required, we tell datatables about this column's type + # so that sorting can take place on a selected-first basis (or -last of course) + # this relies on the template exposing the checkboxes 'th' with class 'checkbox' + if self.checkboxes: + # we use aoColumnDefs rather than aoColumns -- ignore user-provided aoColumns + if 'aoColumns' in self.datatables_options: + print 'WARNING: querygrid uses aoColumnDefs, your aoColumns spec. is discarded' + del self.datatables_options['aoColumns'] + # set aoColumnDefs in datatables_options - might already have stuff in there + aoColumnDefs = self.datatables_options.setdefault ('aoColumnDefs',[]) + # here 'checkbox' is the class that we give to the dom elem + # dom-checkbox is a sorting type that we define in querygrid.js + aoColumnDefs.append ( {'aTargets': ['checkbox'], 'sSortDataType': 'dom-checkbox' } ) + + def template_file (self): + return "querygrid.html" + + def template_env (self, request): + env={} + env.update(self.__dict__) + env['columns']=self.columns + return env + + def requirements (self): + reqs = { + 'js_files' : [ + "js/spin.presets.js", "js/spin.min.js", "js/jquery.spin.js", +# this one was in the slickgrid demo +# http://mleibman.github.io/SlickGrid/examples/example-checkbox-row-select.html +# 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/slick.core.js", + "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.columnpicker.js", # from slickgrid/controls/ + "js/slick.formatters.js", + "js/slick.editors.js", + "js/slick.grid.js", + "js/slick.dataview.js", + +# "js/dataTables.js", "js/dataTables.bootstrap.js", "js/with-datatables.js", + "js/manifold.js", "js/manifold-query.js", + "js/unfold-helper.js", + # querygrid.js needs to be loaded after dataTables.js as it extends + # dataTableExt.afnSortData + "js/querygrid.js", + ] , + 'css_files': [ +# "css/dataTables.bootstrap.css", + # hopefully temporary, when/if datatables supports sPaginationType=bootstrap3 + # for now we use full_numbers, with our own ad hoc css +# "css/dataTables.full_numbers.css", + "css/querygrid.css" , + "http://mleibman.github.io/SlickGrid/slick.grid.css", +# "http://mleibman.github.io/SlickGrid/css/smoothness/jquery-ui-1.8.16.custom.css", +# "http://mleibman.github.io/SlickGrid/examples/examples.css", + ], + } + return reqs + + # the list of things passed to the js plugin + def json_settings_list (self): + return ['plugin_uuid', 'domid', + 'query_uuid', 'query_all_uuid', + 'checkboxes', 'datatables_options', + 'columns','hidden_columns', + 'id_key',] diff --git a/plugins/querygrid/static/css/querygrid.css b/plugins/querygrid/static/css/querygrid.css new file mode 100644 index 00000000..888c9053 --- /dev/null +++ b/plugins/querygrid/static/css/querygrid.css @@ -0,0 +1,36 @@ +/* the bottom of the datatable needs more space */ +div.querygrid-spacer { padding: 8px 4px 15px 4px; } + +/* use same height as the googlemap plugin for nicer effect */ +div.querygrid { + width: 100%; + height: 600px; +} + +/* this is crucial for slickgrid and bootstrap3 to play together nicely + https://github.com/mleibman/SlickGrid/issues/742 + */ +div.querygrid div { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +div.querygrid .slick-column-name { + font-weight: bold; + font-size: medium; + padding: 5px; + } + +div.querygrid { + font-size: small; +} + +/* an example of the css classes used */ +.querygrid-column-network_hrn { + background-color:red; +} + +.slick-checkbox { + background-color: blue; +} diff --git a/plugins/querygrid/static/js/querygrid.js b/plugins/querygrid/static/js/querygrid.js new file mode 100644 index 00000000..6f9694f3 --- /dev/null +++ b/plugins/querygrid/static/js/querygrid.js @@ -0,0 +1,495 @@ +/** + * Description: display a query result in a slickgrid-powered + * Copyright (c) 2012-2013 UPMC Sorbonne Universite - INRIA + * License: GPLv3 + */ + +/* ongoing adaptation to slickgrid + still missing are +. checkboxes really running properly +. ability to sort on columns (should be straightforward + IIRC this got broken when moving to dataview, see dataview doc +. ability to sort on the checkboxes column + (e.g. have resources 'in' the slice show up first) + not quite clear how to do this +. searching +. filtering +. style improvement +. rendering in the sliceview - does not use up all space, + this is different from the behaviour with simpleview +*/ + +(function($) { + + var debug=false; + debug=true + var debug_deep=false; +// debug_deep=true; + + var QueryGrid = Plugin.extend({ + + init: function(options, element) { + this._super(options, element); + + /* Member variables */ + // in general we expect 2 queries here + // query_uuid refers to a single object (typically a slice) + // query_all_uuid refers to a list (typically resources or users) + // these can return in any order so we keep track of which has been received yet + this.received_all_query = false; + this.received_query = false; + +// // We need to remember the active filter for filtering +// this.filters = Array(); + + // an internal buffer for records that are 'in' and thus need to be checked + this.buffered_records_to_check = []; + + /* Events */ + 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); + + /* Setup query and record handlers */ + this.listen_query(options.query_uuid); + this.listen_query(options.query_all_uuid, 'all'); + + /* GUI setup and event binding */ + this.initialize_table(); + }, + + /* PLUGIN EVENTS */ + + on_show: function(e) { + var self = e.data; + self.redraw_table(); + }, // on_show + + /* GUI EVENTS */ + + /* GUI MANIPULATION */ + + initialize_table: function() { + // compute columns based on columns and hidden_columns + this.slick_columns = []; + var all_columns = this.options.columns; // .concat(this.options.hidden_columns) + // xxx would be helpful to support a column_renamings options arg + // for redefining some labels like 'network_hrn' that really are not meaningful + for (c in all_columns) { + var column=all_columns[c]; + this.slick_columns.push ( {id:column, name:column, field:column, + cssClass: "querygrid-column-"+column, + width:100, minWidth:40, }); + } + var checkbox_selector = new Slick.CheckboxSelectColumn({ + cssClass: "slick-checkbox" + }); + this.slick_columns.push(checkbox_selector.getColumnDefinition()); + + // xxx should be extensible from caller with this.options.slickgrid_options + this.slick_options = { + enableCellNavigation: false, + enableColumnReorder: true, + showHeaderRow: true, + syncColumnCellResize: true, + }; + + this.slick_data = []; + this.slick_dataview = new Slick.Data.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) { + messages.debug("slick grid selector is " + selector); + for (c in this.slick_columns) { + var col=this.slick_columns[c]; + var msg=""; + for (k in col) msg = msg+" col["+k+"]="+col[k]; + messages.debug("slick_column["+c+"]:"+msg); + } + } + + 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.registerPlugin (checkbox_selector); + // autotooltips: for showing the full column name when ellipsed + var auto_tooltips = new Slick.AutoTooltips ({ enableForHeaderCells: true }); + this.slick_grid.registerPlugin (auto_tooltips); + + 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) { + this.slick_data.push(record); + }, + + clear_table: function() { + this.slick_data=[]; + this.slick_dataview.setItems(this.slick_data,this.key); + }, + + redraw_table: function() { + this.slick_grid.autosizeColumns(); + this.slick_grid.render(); + }, + + show_column: function(field) { + console.log ("querygrid.show_column not yet implemented with slickgrid - field="+field); + }, + + hide_column: function(field) { + console.log("querygrid.hide_column not implemented with slickgrid - field="+field); + }, + + /*************************** QUERY HANDLER ****************************/ + + on_filter_added: function(filter) { + this.filters.push(filter); + this.redraw_table(); + }, + + on_filter_removed: function(filter) { + // Remove corresponding filters + this.filters = $.grep(this.filters, function(x) { + return x != filter; + }); + this.redraw_table(); + }, + + on_filter_clear: function() { + this.redraw_table(); + }, + + on_field_added: function(field) { + this.show_column(field); + }, + + on_field_removed: function(field) { + this.hide_column(field); + }, + + on_field_clear: function() { + alert('QueryGrid::clear_fields() not implemented'); + }, + + /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */ + /*************************** ALL QUERY HANDLER ****************************/ + + on_all_filter_added: function(filter) { + // XXX + this.redraw_table(); + }, + + on_all_filter_removed: function(filter) { + // XXX + this.redraw_table(); + }, + + on_all_filter_clear: function() { + // XXX + this.redraw_table(); + }, + + on_all_field_added: function(field) { + this.show_column(field); + }, + + on_all_field_removed: function(field) { + this.hide_column(field); + }, + + on_all_field_clear: function() { + alert('QueryGrid::clear_fields() not implemented'); + }, + + + /*************************** RECORD HANDLER ***************************/ + + 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); + } else { + // otherwise we need to remember that and do it later on + if (debug) messages.debug("Remembering record to check " + record[this.key]); + this.buffered_records_to_check.push(record); + } + }, + + on_clear_records: function() { + }, + + // Could be the default in parent + on_query_in_progress: function() { + this.spin(); + }, + + on_query_done: function() { + this.received_query = true; + // unspin once we have received both + if (this.received_all_query && this.received_query) { + this._init_checkboxes(); + this.unspin(); + } + }, + + on_field_state_changed: function(data) { + switch(data.request) { + case FIELD_REQUEST_ADD: + case FIELD_REQUEST_ADD_RESET: + this._set_checkbox(data.value, true); + break; + case FIELD_REQUEST_REMOVE: + case FIELD_REQUEST_REMOVE_RESET: + this._set_checkbox(data.value, false); + break; + default: + break; + } + }, + + /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */ + // all + on_all_field_state_changed: function(data) { + switch(data.request) { + case FIELD_REQUEST_ADD: + case FIELD_REQUEST_ADD_RESET: + this._set_checkbox(data.value, true); + break; + case FIELD_REQUEST_REMOVE: + case FIELD_REQUEST_REMOVE_RESET: + this._set_checkbox(data.value, false); + break; + default: + break; + } + }, + + on_all_new_record: function(record) { + this.new_record(record); + }, + + on_all_clear_records: function() { + this.clear_table(); + + }, + + on_all_query_in_progress: function() { + // XXX parent + this.spin(); + }, // on_all_query_in_progress + + 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); + var duration=new Date()-start; + if (debug) messages.debug("setItems " + duration + " ms"); + if (debug_deep) { + // show full contents of first row app + for (k in this.slick_data[0]) messages.debug("slick_data[0]["+k+"]="+this.slick_data[0][k]); + } + + var self = this; + // if we've already received the slice query, we have not been able to set + // 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); + }); + this.buffered_records_to_check = []; + + this.received_all_query = true; + // unspin once we have received both + if (this.received_all_query && this.received_query) { + this._init_checkboxes(); + this.unspin(); + } + + }, // on_all_query_done + + /************************** 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; + } + + + 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); + 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;}); + this.slick_grid.setSelectedRows(selectedRows); + }, + +// initializing checkboxes +// have tried 2 approaches, but none seems to work as we need it +// issue summarized in here +// http://stackoverflow.com/questions/20425193/slickgrid-selection-changed-callback-how-to-tell-between-manual-and-programmat + // arm the click callback on checkboxes + _init_checkboxes_manual : function () { + // xxx looks like checkboxes can only be the last column?? + var checkbox_col = this.slick_grid.getColumns().length-1; // -1 +1 =0 + console.log ("checkbox_col="+checkbox_col); + var self=this; + console.log ("HERE 1 with "+this.slick_dataview.getLength()+" sons"); + for (var index=0; index < this.slick_dataview.getLength(); index++) { + // retrieve key (i.e. hrn) for this line + var key=this.slick_dataview.getItem(index)[this.key]; + // locate cell
for the checkbox + var div=this.slick_grid.getCellNode(index,checkbox_col); + if (index <=30) console.log("HERE2 div",div," index="+index+" col="+checkbox_col); + // arm callback on single son of
that is the + $(div).children("input").each(function () { + if (index<=30) console.log("HERE 3, index="+index+" key="+key); + $(this).click(function() {self._checkbox_clicked(self,this,key);}); + }); + } + }, + + // onSelectedRowsChanged will fire even when + _init_checkboxes : function () { + console.log("_init_checkboxes"); + var grid=this.slick_grid; + this.slick_grid.onSelectedRowsChanged.subscribe(function(){ + row_ids = grid.getSelectedRows(); + console.log(row_ids); + }); + }, + + // the callback for when user clicks + _checkbox_clicked: function(querygrid,input,key) { + // XXX this.value = key of object to be added... what about multiple keys ? + if (debug) messages.debug("querygrid click handler checked=" + input.checked + " key=" + key); + manifold.raise_event(querygrid.options.query_uuid, input.checked?SET_ADD:SET_REMOVED, key); + //return false; // prevent checkbox to be checked, waiting response from manifold plugin api + + }, + + // xxx from this and down, probably needs further tweaks for slickgrid + + _querygrid_filter: function(oSettings, aData, iDataIndex) { + var ret = true; + $.each (this.filters, function(index, filter) { + /* XXX How to manage checkbox ? */ + var key = filter[0]; + var op = filter[1]; + var value = filter[2]; + + /* Determine index of key in the table columns */ + var col = $.map(oSettings.aoColumns, function(x, i) {if (x.sTitle == key) return i;})[0]; + + /* Unknown key: no filtering */ + if (typeof(col) == 'undefined') + return; + + col_value=unfold.get_value(aData[col]); + /* Test whether current filter is compatible with the column */ + if (op == '=' || op == '==') { + if ( col_value != value || col_value==null || col_value=="" || col_value=="n/a") + ret = false; + }else if (op == '!=') { + if ( col_value == value || col_value==null || col_value=="" || col_value=="n/a") + ret = false; + } else if(op=='<') { + if ( parseFloat(col_value) >= value || col_value==null || col_value=="" || col_value=="n/a") + ret = false; + } else if(op=='>') { + if ( parseFloat(col_value) <= value || col_value==null || col_value=="" || col_value=="n/a") + ret = false; + } else if(op=='<=' || op=='≤') { + if ( parseFloat(col_value) > value || col_value==null || col_value=="" || col_value=="n/a") + ret = false; + } else if(op=='>=' || op=='≥') { + if ( parseFloat(col_value) < value || col_value==null || col_value=="" || col_value=="n/a") + ret = false; + }else{ + // How to break out of a loop ? + alert("filter not supported"); + return false; + } + + }); + return ret; + }, + + _selectAll: function() { + // requires jQuery id + var uuid=this.id.split("-"); + var oTable=$("#querygrid-"+uuid[1]).dataTable(); + // Function available in QueryGrid 1.9.x + // Filter : displayed data only + var filterData = oTable._('tr', {"filter":"applied"}); + /* TODO: WARNING if too many nodes selected, use filters to reduce nuber of nodes */ + if(filterData.length<=100){ + $.each(filterData, function(index, obj) { + var last=$(obj).last(); + var key_value=unfold.get_value(last[0]); + if(typeof($(last[0]).attr('checked'))=="undefined"){ + $.publish('selected', 'add/'+key_value); + } + }); + } + }, + + }); + + $.plugin('QueryGrid', QueryGrid); + +// /* define the 'dom-checkbox' type for sorting in datatables +// http://datatables.net/examples/plug-ins/dom_sort.html +// using trial and error I found that the actual column number +// was in fact given as a third argument, and not second +// as the various online resources had it - go figure */ +// $.fn.dataTableExt.afnSortData['dom-checkbox'] = function ( oSettings, _, iColumn ) { +// return $.map( oSettings.oApi._fnGetTrNodes(oSettings), function (tr, i) { +// return result=$('td:eq('+iColumn+') input', tr).prop('checked') ? '1' : '0'; +// } ); +// } + +})(jQuery); + diff --git a/plugins/querygrid/templates/querygrid.html b/plugins/querygrid/templates/querygrid.html new file mode 100644 index 00000000..63e47627 --- /dev/null +++ b/plugins/querygrid/templates/querygrid.html @@ -0,0 +1,3 @@ +
+
+