From 9c80e89622704e2b9fbe4c59c766fea5a92848a3 Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Wed, 20 Mar 2013 18:50:13 +0100 Subject: [PATCH] a first stab at porting datatables - new code - temporary - name is 'hazelnut' for now as i'm still not exactly sure what this does --- myslice/urls.py | 1 + myslice/viewutils.py | 1 + plugins/hazelnut/__init__.py | 0 plugins/hazelnut/hazelnut.css | 27 ++ plugins/hazelnut/hazelnut.html | 7 + plugins/hazelnut/hazelnut.js | 477 +++++++++++++++++++++++++++++++++ plugins/hazelnut/hazelnut.py | 34 +++ trash/haze.py | 92 +++++++ 8 files changed, 639 insertions(+) create mode 100644 plugins/hazelnut/__init__.py create mode 100644 plugins/hazelnut/hazelnut.css create mode 100644 plugins/hazelnut/hazelnut.html create mode 100644 plugins/hazelnut/hazelnut.js create mode 100644 plugins/hazelnut/hazelnut.py create mode 100644 trash/haze.py diff --git a/myslice/urls.py b/myslice/urls.py index 5c185290..5fa99b45 100644 --- a/myslice/urls.py +++ b/myslice/urls.py @@ -47,4 +47,5 @@ urlpatterns = patterns( (r'^scroll/?$', 'trash.sampleviews.scroll_view'), (r'^plugin/?$', 'trash.pluginview.test_plugin_view'), (r'^dashboard/?$', 'trash.dashboard.dashboard_view'), + (r'^hazelnut/?$', 'trash.haze.hazelnut_view'), ) diff --git a/myslice/viewutils.py b/myslice/viewutils.py index 2073ab48..99d74d29 100644 --- a/myslice/viewutils.py +++ b/myslice/viewutils.py @@ -8,6 +8,7 @@ standard_topmenu_items = [ { 'label':'Slice', 'href': '/slice/'}, { 'label':'Plugin', 'href': '/plugin/'}, { 'label':'Dashboard', 'href': '/dashboard/'}, + { 'label':'Hazelnut', 'href': '/hazelnut/'}, ] #login_out_items = { False: { 'label':'Login', 'href':'/login/'}, diff --git a/plugins/hazelnut/__init__.py b/plugins/hazelnut/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/hazelnut/hazelnut.css b/plugins/hazelnut/hazelnut.css new file mode 100644 index 00000000..9e1674fe --- /dev/null +++ b/plugins/hazelnut/hazelnut.css @@ -0,0 +1,27 @@ +.tophat-datatables { + margin-top: 1em; + background: #F5F5F5; +} +/* +.dataTables_wrapper { + border: 1px solid #999999; + padding-top: 1em; + / padding-bottom: 3.5em; / + margin-bottom: 3.5em; +} +.dataTables_scroll { + border-top: 1px solid #999999; + border-bottom: 1px solid #999999; +} +*/ + +.dataTables_length { + padding-left: 1em; + padding-bottom: 1em; +} + +.dataTables_filter { + padding-right: 1em; + padding-bottom: 1em; +} + diff --git a/plugins/hazelnut/hazelnut.html b/plugins/hazelnut/hazelnut.html new file mode 100644 index 00000000..cbaac59b --- /dev/null +++ b/plugins/hazelnut/hazelnut.html @@ -0,0 +1,7 @@ + + {% for subject_field in subject_fields %} +{% endfor %} + + + +
{{ subject_field }}
diff --git a/plugins/hazelnut/hazelnut.js b/plugins/hazelnut/hazelnut.js new file mode 100644 index 00000000..c264065a --- /dev/null +++ b/plugins/hazelnut/hazelnut.js @@ -0,0 +1,477 @@ +/** + * MySlice Hazelnut plugin + * URL: http://trac.myslice.info + * Description: display a query result in a datatables-powered + * Author: The MySlice Team + * Copyright (c) 2012 UPMC Sorbonne Universite - INRIA + * License: GPLv3 + */ + +/* + * It's a best practice to pass jQuery to an IIFE (Immediately Invoked Function + * Expression) that maps it to the dollar sign so it can't be overwritten by + * another library in the scope of its execution. + */ +(function($){ + + var debug=false; + debug=true + + // routing calls + $.fn.Hazelnut = function( method ) { + if ( methods[method] ) { + return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); + } else if ( typeof method === 'object' || ! method ) { + return methods.init.apply( this, arguments ); + } else { + $.error( 'Method ' + method + ' does not exist on jQuery.Hazelnut' ); + } + }; + + /*************************************************************************** + * Public methods + ***************************************************************************/ + + var methods = { + + init : function ( options ) { + /* Default settings */ + var options = $.extend( { + 'checkboxes': false + }, options); + + return this.each(function() { + var $this = $(this); + /* Events */ + $(this).on('show.Datatables', methods.show); + + /* An object that will hold private variables and methods */ + var hazelnut = new Hazelnut (options); + if (debug) console.log("Hazelnut object created"); + $(this).data('Hazelnut', hazelnut); + + var query_channel = '/query/' + options.query_uuid + '/changed'; + var update_channel = '/update-set/' + options.query_uuid; + var results_channel = '/results/' + options.query_uuid + '/changed'; + + $.subscribe(query_channel, function(e, query) { hazelnut.set_query(query); });; + $.subscribe(update_channel, function(e, resources, instance) { hazelnut.set_resources(resources, instance); }); + $.subscribe(results_channel, function(e, rows) { hazelnut.update_table(rows); }); + + }); // this.each + }, // init + + destroy : function( ) { + return this.each(function() { + var $this = $(this); + var hazelnut = $this.data('Hazelnut'); + + // Unbind all events using namespacing + $(window).unbind('Hazelnut'); + + // Remove associated data + hazelnut.remove(); + $this.removeData('Hazelnut'); + }); + }, // destroy + + show : function( ) { + var $this=$(this); + // xxx wtf. why [1] ? would expect 0... + var oTable = $($('.dataTable', $this)[1]).dataTable(); + oTable.fnAdjustColumnSizing() + + /* Refresh dataTabeles if click on the menu to display it : fix dataTables 1.9.x Bug */ + $(this).each(function(i,elt) { + if (jQuery(elt).hasClass('dataTables')) { + var myDiv=jQuery('#hazelnut-' + this.id).parent(); + if(myDiv.height()==0) { + var oTable=$('#hazelnut-' + this.id).dataTable(); + oTable.fnDraw(); + } + } + }); + } // show + + }; // var methods; + + + /*************************************************************************** + * Hazelnut object + ***************************************************************************/ + function Hazelnut(options) { + /* member variables */ + this.options = options; + /* constructor */ + this.table = null; + // xxx thierry : init this here - it was not, I expect this relied on set_query somehow.. + //this.current_query = null; + this.current_query=manifold.find_query(this.options.query_uuid); + this.query_update = null; + this.current_resources = Array(); + + var object = this; + + /* Transforms the table into DataTable, and keep a pointer to it */ + this.table = $('#hazelnut-' + options.plugin_uuid).dataTable({ + // Customize the position of Datatables elements (length,filter,button,...) + // inspired from : + // http://datatables.net/release-datatables/examples/advanced_init/dom_toolbar.html + // http://www.datatables.net/forums/discussion/3914/adding-buttons-to-header-or-footer/p1 + //"sDom": 'lf<"#datatableSelectAll-'+ options.plugin_uuid+'">rtip', + sDom: '<"H"Tfr>t<"F"ip>', + bJQueryUI: true, + sPaginationType: 'full_numbers', + // Handle the null values & the error : Datatables warning Requested unknown parameter + // http://datatables.net/forums/discussion/5331/datatables-warning-...-requested-unknown-parameter/p2 + aoColumnDefs: [{sDefaultContent: '',aTargets: [ '_all' ]}], + bRetrieve: true, + sScrollX: '100%', /* Horizontal scrolling */ + bProcessing: true, /* Loading */ + fnRowCallback: function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { + $(nRow).attr('id', get_value(aData[3])); + return nRow; + }, + fnDrawCallback: function() { hazelnut_draw_callback.call(object, options); } + }); + + /* Setup the SelectAll button in the dataTable header */ + var oSelectAll = $('#datatableSelectAll-'+ options.plugin_uuid); + oSelectAll.html("Select All"); + oSelectAll.button(); + oSelectAll.css('font-size','11px'); + oSelectAll.css('float','right'); + oSelectAll.css('margin-right','15px'); + oSelectAll.css('margin-bottom','5px'); + oSelectAll.unbind('click'); + oSelectAll.click(selectAll); + + /* Spinner (could be done when the query is received = a query is in progress, also for update) */ + $('#' + options.plugin_uuid).spin() + + if (debug) console.log ("bing 005"); + + /* Add a filtering function to the current table + * Note: we use closure to get access to the 'options' + */ + $.fn.dataTableExt.afnFiltering.push(function( oSettings, aData, iDataIndex ) { + /* No filtering if the table does not match */ + if (oSettings.nTable.id != "hazelnut-" + options.plugin_uuid) + return true; + return hazelnut_filter.call(object, oSettings, aData, iDataIndex); + }); + + if (debug) console.log ("bing 010"); + + + /* methods */ + + this.set_query = function(query) { + if (debug) console.log("entering set_query"); + var o = this.options; + /* Compare current and advertised query to get added and removed fields */ + previous_query = this.current_query; + /* Save the query as the current query */ + this.current_query = query; + /* We check all necessary fields : in column editor I presume XXX */ + // XXX ID naming has no plugin_uuid + if (typeof(query.fields) != 'undefined') { + $.each (query.fields, function(index, value) { + if (!$('#hazelnut-checkbox-' + o.plugin_uuid + "-" + value).attr('checked')) + $('#hazelnut-checkbox-' + o.plugin_uuid + "-" + value).attr('checked', true); + }); + } + /*Process updates in filters / current_query must be updated before this call for filtering ! */ + this.table.fnDraw(); + + /* + * Process updates in fields + */ + if (typeof(query.fields) != 'undefined') { + /* DataTable Settings */ + var oSettings = this.table.dataTable().fnSettings(); + var cols = oSettings.aoColumns; + var colnames = cols.map(function(x) {return x.sTitle}); + colnames = $.grep(colnames, function(value) {return value != "+/-";}); + + if (previous_query == null) { + var added_fields = query.fields; + var removed_fields = colnames; + removed_fields = $.grep(colnames, function(x) { return $.inArray(x, added_fields) == -1}); + } else { + var tmp = previous_query.diff_fields(query); + var added_fields = tmp.added; + var removed_fields = tmp.removed; + } + + /* Hide/unhide columns to match added/removed fields */ + var object = this; + $.each(added_fields, function (index, field) { + var index = object.getColIndex(field,cols); + if(index != -1) + object.table.fnSetColumnVis(index, true); + }); + $.each(removed_fields, function (index, field) { + var index = object.getColIndex(field,cols); + if(index != -1) + object.table.fnSetColumnVis(index, false); + }); + } + } + + this.set_resources = function(resources, instance) { + if (debug) console.log("entering set_resources"); + var o = this.options; + var previous_resources = this.current_resources; + this.current_resources = resources; + + /* We uncheck all checkboxes ... */ + $('hazelnut-checkbox-' + o.plugin_uuid).attr('checked', false); + /* ... and check the ones specified in the resource list */ + $.each(this.current_resources, function(index, urn) { + $('#hazelnut-checkbox-' + o.plugin_uuid + "-" + urn).attr('checked', true) + }); + + } + + /** + * @brief Determine index of key in the table columns + * @param key + * @param cols + */ + this.getColIndex = function(key, cols) { + var tabIndex = $.map(cols, function(x, i) { if (x.sTitle == key) return i; }); + return (tabIndex.length > 0) ? tabIndex[0] : -1; + } + + /** + * @brief + * XXX will be removed/replaced + */ + this.selected_changed = function(e, change) { + if (debug) console.log("entering selected_changed"); + var actions = change.split("/"); + if (actions.length > 1) { + var oNodes = this.table.fnGetNodes(); + var myNode = $.grep(oNodes, function(value) { + if (value.id == actions[1]) { return value; } + }); + if( myNode.length>0 ) { + if ((actions[2]=="add" && actions[0]=="cancel") || actions[0]=="del") + checked=''; + else + checked="checked='checked' "; + var newValue = this.checkbox(actions[1], 'node', checked, false); + var columnPos = this.table.fnSettings().aoColumns.length - 1; + this.table.fnUpdate(newValue, myNode[0], columnPos); + this.table.fnDraw(); + } + } + } + + /** + * @brief + * @param plugin_uuid + * @param header + * @param field + * @param selected_str + * @param disabled_str + */ + this.checkbox = function(plugin_uuid, header, field, selected_str, disabled_str) { + /* Prefix id with plugin_uuid */ + return ""; + } + + this.update_table = function(rows) { + var o = this.options; + var object = this; + + $('#' + o.plugin_uuid).spin(false) + if (rows.length==0) { + this.table.html(errorDisplay("No Result")); + return; + } else { + if (typeof(rows[0].error) != 'undefined') { + this.table.html(errorDisplay(rows[0].error)); + return; + } + } + newlines = new Array(); + + this.current_resources = Array(); + + $.each(rows, function(index, obj) { + newline = new Array(); + + // go through table headers to get column names we want + // in order (we have temporarily hack some adjustments in names) + var cols = object.table.fnSettings().aoColumns; + var colnames = cols.map(function(x) {return x.sTitle}) + var nb_col = colnames.length; + if (o.checkboxes) + nb_col -= 1; + for (var j = 0; j < nb_col; j++) { + if (typeof colnames[j] == 'undefined') { + newline.push('...'); + } else if (colnames[j] == 'hostname') { + if (obj['type'] == 'resource,link') + //TODO: we need to add source/destination for links + newline.push(''); + else + newline.push(obj['hostname']); + } else { + if (obj[colnames[j]]) + newline.push(obj[colnames[j]]); + else + newline.push(''); + } + } + + if (o.checkboxes) { + var checked = ''; + if (typeof(obj['sliver']) != 'undefined') { /* It is equal to null when is present */ + checked = 'checked '; + object.current_resources.push(obj['urn']); + } + // Use a key instead of hostname (hard coded...) + newline.push(object.checkbox(o.plugin_uuid, obj['urn'], obj['type'], checked, false)); + } + + newlines.push(newline); + + + }); + + this.table.fnAddData(newlines); + + } + } + /*************************************************************************** + * Private methods + ***************************************************************************/ + + /** + * @brief Hazelnut filtering function + */ + function hazelnut_filter (oSettings, aData, iDataIndex) { + var cur_query = this.current_query; + var ret = true; + + /* We have an array of filters : a filter is an array (key op val) + * field names (unless shortcut) : oSettings.aoColumns = [ sTitle ] + * can we exploit the data property somewhere ? + * field values (unless formatting) : aData + * formatting should leave original data available in a hidden field + * + * The current line should validate all filters + */ + $.each (cur_query.filter, 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=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; + } + + function hazelnut_draw_callback() { + var o = this.options; + /* + * Handle clicks on checkboxes: reassociate checkbox click every time + * the table is redrawn + */ + $('.hazelnut-checkbox-' + o.plugin_uuid).unbind('click'); + $('.hazelnut-checkbox-' + o.plugin_uuid).click({instance: this}, check_click); + + if (!this.table) + return; + + /* Remove pagination if we show only a few results */ + var wrapper = this.table; //.parent().parent().parent(); + var rowsPerPage = this.table.fnSettings()._iDisplayLength; + var rowsToShow = this.table.fnSettings().fnRecordsDisplay(); + var minRowsPerPage = this.table.fnSettings().aLengthMenu[0]; + + if ( rowsToShow <= rowsPerPage || rowsPerPage == -1 ) { + $('.hazelnut_paginate', wrapper).css('visibility', 'hidden'); + } else { + $('.hazelnut_paginate', wrapper).css('visibility', 'visible'); + } + + if ( rowsToShow <= minRowsPerPage ) { + $('.hazelnut_length', wrapper).css('visibility', 'hidden'); + } else { + $('.hazelnut_length', wrapper).css('visibility', 'visible'); + } + } + + function check_click (e) { + var object = e.data.instance; + var value = this.value; + + if (this.checked) { + object.current_resources.push(value); + } else { + tmp = $.grep(object.current_resources, function(x) { return x != value; }); + object.current_resources = tmp; + } + + /* inform slice that our selected resources have changed */ + $.publish('/update-set/' + object.options.query_uuid, [object.current_resources, true]); + + } + + function selectAll() { + // requires jQuery id + var uuid=this.id.split("-"); + var oTable=$("#hazelnut-"+uuid[1]).dataTable(); + // Function available in Hazelnut 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=get_value(last[0]); + if(typeof($(last[0]).attr('checked'))=="undefined"){ + $.publish('selected', 'add/'+key_value); + } + }); + } + } + +})( jQuery ); diff --git a/plugins/hazelnut/hazelnut.py b/plugins/hazelnut/hazelnut.py new file mode 100644 index 00000000..d24194d7 --- /dev/null +++ b/plugins/hazelnut/hazelnut.py @@ -0,0 +1,34 @@ +from unfold.plugin import Plugin + +class Hazelnut (Plugin): + + def __init__ (self, query, **settings): + Plugin.__init__ (self, **settings) + self.query=query + + def template_file (self): + return "hazelnut.html" + + def template_env (self, request): + env={} + env.update(self.__dict__) + # xxx need to retrieve metadata +# $method_keys = Plugins::get_default_fields($query->method, $is_unique); +# $fields = Plugins::metadata_get_fields($query->method); + env['subject_fields']=[ 'the','available','default','fields'] + return env + + def requirements (self): + reqs = { + 'js_files' : [ "js/hazelnut.js", + "js/manifold.js", "js/manifold-query.js", + "js/dataTables.js", "js/with-datatables.js", + "js/spin.presets.js", "js/spin.min.js", "js/jquery.spin.js", + "js/unfold-helper.js", + ] , + 'css_files': [ "css/hazelnut.css" ], + } + return reqs + + # the list of things passed to the js plugin + def json_settings_list (self): return ['plugin_uuid','query_uuid'] diff --git a/trash/haze.py b/trash/haze.py new file mode 100644 index 00000000..970faff3 --- /dev/null +++ b/trash/haze.py @@ -0,0 +1,92 @@ +# Create your views here. + +from django.template import RequestContext +from django.shortcuts import render_to_response + +from django.contrib.auth.decorators import login_required + +from unfold.page import Page +from manifold.manifoldquery import ManifoldQuery + +from plugins.stack.stack import Stack +from plugins.hazelnut.hazelnut import Hazelnut +from plugins.lists.slicelist import SliceList +from plugins.querycode.querycode import QueryCode +from plugins.quickfilter.quickfilter import QuickFilter + +from myslice.viewutils import quickfilter_criterias + +from myslice.viewutils import topmenu_items, the_user + +@login_required +def hazelnut_view (request): + + page = Page(request) + + main_query = ManifoldQuery (action='get', + method='resource', + timestamp='latest', + fields=['hrn','hostname'], + filters= [ + # xxx filter : should filter on the slices the logged user can see + # we don't have the user's hrn yet + # in addition this currently returns all slices anyways + # filter = ... + sort='slice_hrn', + ) + page.enqueue_query (main_query) + + main_plugin = Stack ( + page=page, + title="global container", + sons=[ + Hazelnut ( # setting visible attributes first + page=page, + title='a sample and simple hazelnut', + # this is the query at the core of the slice list + query=main_query, + ), + ]) + + # variables that will get passed to the view-plugin.html template + template_env = {} + + # define 'unfold1_main' to the template engine + template_env [ 'unfold1_main' ] = main_plugin.render(request) + + # more general variables expected in the template + template_env [ 'title' ] = 'Test view for hazelnut' + # the menu items on the top + template_env [ 'topmenu_items' ] = topmenu_items('hazelnut', request) + # so we can sho who is logged + template_env [ 'username' ] = the_user (request) + +# ########## add another plugin with the same request, on the RHS pane +# will show up in the right-hand side area named 'related' + related_plugin = SliceList ( + page=page, + title='Same request, other layout', + domid='sidelist', + with_datatables=True, + header='paginated main', + # share the query + query=main_query, + ) + # likewise but on the side view + template_env [ 'unfold1_margin' ] = related_plugin.render (request) + + # add our own css in the mix + page.add_css_files ( 'css/hazelnut.css') + + # don't forget to run the requests + page.exec_queue_asynchroneously () + + # xxx create another plugin with the same query and a different layout (with_datatables) + # show that it worls as expected, one single api call to backend and 2 refreshed views + + # the prelude object in page contains a summary of the requirements() for all plugins + # define {js,css}_{files,chunks} + prelude_env = page.prelude_env() + template_env.update(prelude_env) + return render_to_response ('view-plugin.html',template_env, + context_instance=RequestContext(request)) -- 2.43.0