a first stab at porting datatables - new code - temporary - name is 'hazelnut' for...
authorThierry Parmentelat <thierry.parmentelat@inria.fr>
Wed, 20 Mar 2013 17:50:13 +0000 (18:50 +0100)
committerThierry Parmentelat <thierry.parmentelat@inria.fr>
Wed, 20 Mar 2013 17:50:13 +0000 (18:50 +0100)
myslice/urls.py
myslice/viewutils.py
plugins/hazelnut/__init__.py [new file with mode: 0644]
plugins/hazelnut/hazelnut.css [new file with mode: 0644]
plugins/hazelnut/hazelnut.html [new file with mode: 0644]
plugins/hazelnut/hazelnut.js [new file with mode: 0644]
plugins/hazelnut/hazelnut.py [new file with mode: 0644]
trash/haze.py [new file with mode: 0644]

index 5c18529..5fa99b4 100644 (file)
@@ -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'),
 )
index 2073ab4..99d74d2 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/plugins/hazelnut/hazelnut.css b/plugins/hazelnut/hazelnut.css
new file mode 100644 (file)
index 0000000..9e1674f
--- /dev/null
@@ -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 (file)
index 0000000..cbaac59
--- /dev/null
@@ -0,0 +1,7 @@
+<table class='display' id='hazelnut-{{ domid }}'>
+<thead><tr> {% for subject_field in subject_fields %}
+<th>{{ subject_field }}</th>{% endfor %}
+</tr></thead>
+<tbody>
+</tbody>
+</table>
diff --git a/plugins/hazelnut/hazelnut.js b/plugins/hazelnut/hazelnut.js
new file mode 100644 (file)
index 0000000..c264065
--- /dev/null
@@ -0,0 +1,477 @@
+/**
+ * MySlice Hazelnut plugin
+ * URL: http://trac.myslice.info
+ * Description: display a query result in a datatables-powered <table>
+ * 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("<span class='ui-icon ui-icon-check' style='float:right;display:inline-block;'></span>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 "<input class='hazelnut-checkbox-" + plugin_uuid + "' id='hazelnut-checkbox-" + plugin_uuid + "-" + get_value(header) + "' name='" + get_value(field) + "' type='checkbox' " + selected_str + disabled_str + "autocomplete='off' value='" + get_value(header) + "'></input>";
+        }
+    
+        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 <sliver/> 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 (file)
index 0000000..d24194d
--- /dev/null
@@ -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 (file)
index 0000000..970faff
--- /dev/null
@@ -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))