adding querygrid plugin from the slickgrid branch
authorThierry Parmentelat <thierry.parmentelat@inria.fr>
Fri, 6 Dec 2013 15:36:14 +0000 (16:36 +0100)
committerThierry Parmentelat <thierry.parmentelat@inria.fr>
Fri, 6 Dec 2013 15:36:14 +0000 (16:36 +0100)
plugins/querygrid/__init__.py [new file with mode: 0644]
plugins/querygrid/static/css/querygrid.css [new file with mode: 0644]
plugins/querygrid/static/js/querygrid.js [new file with mode: 0644]
plugins/querygrid/templates/querygrid.html [new file with mode: 0644]

diff --git a/plugins/querygrid/__init__.py b/plugins/querygrid/__init__.py
new file mode 100644 (file)
index 0000000..5b0c466
--- /dev/null
@@ -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 <th> 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 (file)
index 0000000..888c905
--- /dev/null
@@ -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 (file)
index 0000000..6f9694f
--- /dev/null
@@ -0,0 +1,495 @@
+/**
+ * Description: display a query result in a slickgrid-powered <table>
+ * 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 <div> 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 <div> that is the <input>
+               $(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 (file)
index 0000000..63e4762
--- /dev/null
@@ -0,0 +1,3 @@
+<div id='spacer-{{ domid }}' class='querygrid-spacer'>
+<div class="querygrid" id="grid-{{ domid }}"></div>
+</div>