major updates to slice reservation page and plugins
authorJordan Augé <jordan.auge@lip6.fr>
Fri, 4 Jul 2014 09:09:46 +0000 (11:09 +0200)
committerJordan Augé <jordan.auge@lip6.fr>
Fri, 4 Jul 2014 09:09:46 +0000 (11:09 +0200)
31 files changed:
manifoldapi/static/js/manifold.js
manifoldapi/static/js/plugin.js
plugins/filter_status/static/js/filter_status.js
plugins/filter_status/templates/filter_status.html
plugins/querytable/__init__.py
plugins/querytable/static/css/querytable.css
plugins/querytable/static/js/querytable.js
plugins/querytable/templates/querytable.html
plugins/queryupdater/static/js/queryupdater.js
plugins/scheduler2/__init__.py
plugins/scheduler2/static/css/scheduler2.css
plugins/scheduler2/static/js/scheduler-SchedulerCtrl.js [deleted file]
plugins/scheduler2/static/js/scheduler2.js
plugins/scheduler2/static/js/selectRangeWorker.js [deleted file]
plugins/scheduler2/templates/scheduler.html
plugins/testbeds/__init__.py
plugins/testbeds/static/js/testbeds.js
plugins/testbeds/templates/testbeds.html
portal/sliceresourceview.py
portal/static/js/myslice-ui.js
portal/templates/base.html
portal/templates/fed4fire/fed4fire_base.html
portal/templates/onelab/onelab_base.html
portal/templates/slice-resource-view.html
third-party/bootstrap-datepicker [new symlink]
third-party/bootstrap-datepicker-1/bootstrap-datepicker.js [new file with mode: 0644]
third-party/bootstrap-datepicker-1/datepicker.css [new file with mode: 0644]
third-party/bootstrap-slider [new symlink]
third-party/bootstrap-slider-1/bootstrap-slider.js [new file with mode: 0644]
third-party/bootstrap-slider-1/slider.css [new file with mode: 0644]
ui/templates/messages-transient-header.html

index 15f2123..b9cd225 100644 (file)
@@ -188,6 +188,10 @@ function QueryExt(query, parent_query_ext, main_query_ext, update_query_ext, dis
     // Filters that impact visibility in the local interface
     this.filters = [];
 
+    // XXX Until we find a better solution
+    this.num_pending = 0;
+    this.num_unconfigured = 0;
+
     // update_query null unless we are a main_query (aka parent_query == null); only main_query_fields can be updated...
 }
 
@@ -342,10 +346,15 @@ function QueryStore() {
 
     this.add_record = function(query_uuid, record, new_state)
     {
-        var query_ext = this.find_analyzed_query_ext(query_uuid);
+        var query_ext, key, record_key;
+        query_ext = this.find_analyzed_query_ext(query_uuid);
         
-        var key = manifold.metadata.get_key(query_ext.query.object);
-        var record_key = manifold.record_get_value(record, key);
+        if (typeof(record) == 'object') {
+            key = manifold.metadata.get_key(query_ext.query.object);
+            record_key = manifold.record_get_value(record, key);
+        } else {
+            record_key = record;
+        }
 
         var record_entry = query_ext.records.get(record_key);
         if (!record_entry)
@@ -356,10 +365,15 @@ function QueryStore() {
 
     this.remove_record = function(query_uuid, record, new_state)
     {
-        var query_ext = this.find_analyzed_query_ext(query_uuid);
+        var query_ext, key, record_key;
+        query_ext = this.find_analyzed_query_ext(query_uuid);
         
-        var key = manifold.metadata.get_key(query_ext.query.object);
-        var record_key = manifold.record_get_value(record, key);
+        if (typeof(record) == 'object') {
+            key = manifold.metadata.get_key(query_ext.query.object);
+            record_key = manifold.record_get_value(record, key);
+        } else {
+            record_key = record;
+        }
 
         manifold.query_store.set_record_state(query_uuid, record_key, STATE_SET, new_state);
     }
@@ -440,19 +454,78 @@ function QueryStore() {
         return query_ext.filters;
     }
 
+    this.recount = function(query_uuid)
+    {
+        var query_ext;
+        var is_reserved, is_pending, in_set,  is_unconfigured;
+
+        query_ext = manifold.query_store.find_analyzed_query_ext(query_uuid);
+        query_ext.num_pending = 0;
+        query_ext.num_unconfigured = 0;
+
+        this.iter_records(query_uuid, function(record_key, record) {
+            var record_state = manifold.query_store.get_record_state(query_uuid, record_key, STATE_SET);
+            var record_warnings = manifold.query_store.get_record_state(query_uuid, record_key, STATE_WARNINGS);
+
+            is_reserved = (record_state == STATE_SET_IN) 
+                       || (record_state == STATE_SET_OUT_PENDING)
+                       || (record_state == STATE_SET_IN_SUCCESS)
+                       || (record_state == STATE_SET_OUT_FAILURE);
+
+            is_pending = (record_state == STATE_SET_IN_PENDING) 
+                      || (record_state == STATE_SET_OUT_PENDING);
+
+            in_set = (record_state == STATE_SET_IN) // should not have warnings
+                  || (record_state == STATE_SET_IN_PENDING)
+                  || (record_state == STATE_SET_IN_SUCCESS)
+                  || (record_state == STATE_SET_OUT_FAILURE); // should not have warnings
+
+            is_unconfigured = (in_set && !$.isEmptyObject(record_warnings));
+
+            /* Let's update num_pending and num_unconfigured at this stage */
+            if (is_pending)
+                query_ext.num_pending++;
+            if (is_unconfigured)
+                query_ext.num_unconfigured++;
+        });
+
+    }
+
     this.apply_filters = function(query_uuid)
     {
+        var start = new Date().getTime();
+
         // Toggle visibility of records according to the different filters.
 
         var self = this;
         var filters = this.get_filters(query_uuid);
         var col_value;
+        /* Let's update num_pending and num_unconfigured at this stage */
 
         // Adapted from querytable._querytable_filter()
 
         this.iter_records(query_uuid, function(record_key, record) {
+            var is_reserved, is_pending, in_set,  is_unconfigured;
             var visible = true;
 
+            var record_state = manifold.query_store.get_record_state(query_uuid, record_key, STATE_SET);
+            var record_warnings = manifold.query_store.get_record_state(query_uuid, record_key, STATE_WARNINGS);
+
+            is_reserved = (record_state == STATE_SET_IN) 
+                       || (record_state == STATE_SET_OUT_PENDING)
+                       || (record_state == STATE_SET_IN_SUCCESS)
+                       || (record_state == STATE_SET_OUT_FAILURE);
+
+            is_pending = (record_state == STATE_SET_IN_PENDING) 
+                      || (record_state == STATE_SET_OUT_PENDING);
+
+            in_set = (record_state == STATE_SET_IN) // should not have warnings
+                  || (record_state == STATE_SET_IN_PENDING)
+                  || (record_state == STATE_SET_IN_SUCCESS)
+                  || (record_state == STATE_SET_OUT_FAILURE); // should not have warnings
+
+            is_unconfigured = (in_set && !$.isEmptyObject(record_warnings));
+
             // We go through each filter and decide whether it affects the visibility of the record
             $.each(filters, function(index, filter) {
                 var key = filter[0];
@@ -470,37 +543,24 @@ function QueryStore() {
                         return true; // ~ continue
                     }
 
-                    var record_state = manifold.query_store.get_record_state(query_uuid, record_key, STATE_SET);
-                    var record_warnings = manifold.query_store.get_record_state(query_uuid, record_key, STATE_WARNINGS);
-
                     switch (value) {
                         case 'reserved':
-                            visible = (record_state == STATE_SET_IN) 
-                                   || (record_state == STATE_SET_OUT_PENDING)
-                                   || (record_state == STATE_SET_IN_SUCCESS)
-                                   || (record_state == STATE_SET_OUT_FAILURE);
-                            // visible = true  => ~ continue
-                            // visible = false => ~ break
-                            return visible; 
-
+                            // true  => ~ continue
+                            // false => ~ break
+                            visible = is_reserved;
+                            return visible;
                         case 'unconfigured':
-                            var in_set = (record_state == STATE_SET_IN) // should not have warnings
-                                   || (record_state == STATE_SET_IN_PENDING)
-                                   || (record_state == STATE_SET_IN_SUCCESS)
-                                   || (record_state == STATE_SET_OUT_FAILURE); // should not have warnings
-                            visible = (in_set && !$.isEmptyObject(record_warnings));
-                            return visible; 
-
+                            visible = is_unconfigured;
+                            return visible;
                         case 'pending':
-                            visible = (record_state == STATE_SET_IN_PENDING) 
-                                   || (record_state == STATE_SET_OUT_PENDING);
-                            return visible; 
+                            visible = is_pending;
+                            return visible;
                     }
                     return false; // ~ break
                 }
 
                 /* Normal filtering behaviour (according to the record content) follows... */
-                col_value = manifold.record_get_value(record, record_key);
+                col_value = manifold.record_get_value(record, key);
 
                 // When the filter does not match, we hide the column by default
                 if (col_value === 'undefined') {
@@ -551,6 +611,9 @@ function QueryStore() {
             self.set_record_state(query_uuid, record_key, STATE_VISIBLE, visible);
         });
 
+        var end = new Date().getTime();
+        console.log("APPLY FILTERS took", end - start, "ms");
+
     }
 
 }
@@ -775,7 +838,9 @@ var manifold = {
         manifold.query_store.insert(query);
 
         // Run
+        $(document).ready(function() {
         manifold.run_query(query);
+        });
 
         // FORMER API
         if (query.analyzed_query == null) {
@@ -1082,7 +1147,8 @@ var manifold = {
             switch (this.get_type(result_value)) {
                 case TYPE_RECORD:
                     var subobject = manifold.metadata.get_type(object, field);
-                    if (subobject)
+                    // if (subobject) XXX Bugs with fields declared string while they are not : network.version is a dict in fact
+                    if (subobject && subobject != 'string')
                         manifold.make_record(subobject, result_value);
                     break;
                 case TYPE_LIST_OF_RECORDS:
@@ -1390,6 +1456,8 @@ var manifold = {
 
 
     raise_event: function(query_uuid, event_type, value) {
+        var query, query_ext;
+
         // Query uuid has been updated with the key of a new element
         query_ext    = manifold.query_store.find_analyzed_query_ext(query_uuid);
         query = query_ext.query;
@@ -1457,8 +1525,10 @@ var manifold = {
                 });
                 value.key = value_key;
 
+                manifold.query_store.recount(cur_query.query_uuid);
                 manifold.raise_record_event(cur_query.query_uuid, event_type, value);
 
+
                 // XXX make this DOT a global variable... could be '/'
                 break;
 
@@ -1529,46 +1599,90 @@ var manifold = {
 
                 /* CONSTRAINTS */
 
-                // CONSTRAINT_RESERVABLE_LEASE
-                // 
-                // +) If a reservable node is added to the slice, then it should have a corresponding lease
-                var is_reservable = (record.exclusive == true);
-                if (is_reservable) {
-                    var warnings = manifold.query_store.get_record_state(query_uuid, resource_key, STATE_WARNINGS);
-
-                    if (event_type == SET_ADD) {
-                        // We should have a lease_query associated
-                        var lease_query = query_ext.parent_query_ext.query.subqueries['lease'];
-                        var lease_query_ext = manifold.query_store.find_analyzed_query_ext(lease_query.query_uuid);
-                        // Do we have lease records with this resource
-                        var lease_records = $.grep(lease_query_ext.records, function(lease_key, lease) {
-                            return lease['resource'] == value;
-                        });
-                        if (lease_records.length == 0) {
-                            // Sets a warning
-                            // XXX Need for a better function to manage warnings
-                            var warn = "No lease defined for this reservable resource.";
-                            warnings[CONSTRAINT_RESERVABLE_LEASE] = warn;
-                        } else {
-                            // Lease are defined, delete the warning in case it was set previously
-                            delete warnings[CONSTRAINT_RESERVABLE_LEASE];
+                // XXX When we add a lease we must update the warnings
+
+                switch(query.object) {
+
+                    case 'resource':
+                        // CONSTRAINT_RESERVABLE_LEASE
+                        // 
+                        // +) If a reservable node is added to the slice, then it should have a corresponding lease
+                        // XXX Not always a resource
+                        var is_reservable = (record.exclusive == true);
+                        if (is_reservable) {
+                            var warnings = manifold.query_store.get_record_state(query_uuid, resource_key, STATE_WARNINGS);
+
+                            if (event_type == SET_ADD) {
+                                // We should have a lease_query associated
+                                var lease_query = query_ext.parent_query_ext.query.subqueries['lease']; // in  options
+                                var lease_query_ext = manifold.query_store.find_analyzed_query_ext(lease_query.query_uuid);
+                                // Do we have lease records with this resource
+                                var lease_records = $.grep(lease_query_ext.records, function(lease_key, lease) {
+                                    return lease['resource'] == value;
+                                });
+                                if (lease_records.length == 0) {
+                                    // Sets a warning
+                                    // XXX Need for a better function to manage warnings
+                                    var warn = "No lease defined for this reservable resource.";
+                                    warnings[CONSTRAINT_RESERVABLE_LEASE] = warn;
+                                } else {
+                                    // Lease are defined, delete the warning in case it was set previously
+                                    delete warnings[CONSTRAINT_RESERVABLE_LEASE];
+                                }
+                            } else {
+                                // Remove warnings attached to this resource
+                                delete warnings[CONSTRAINT_RESERVABLE_LEASE];
+                            }
+
+                            manifold.query_store.set_record_state(query_uuid, resource_key, STATE_WARNINGS, warnings);
+                            break;
                         }
-                    } else {
-                        // Remove warnings attached to this resource
-                        delete warnings[CONSTRAINT_RESERVABLE_LEASE];
-                    }
 
-                    manifold.query_store.set_record_state(query_uuid, resource_key, STATE_WARNINGS, warnings);
+                        // Signal the change to plugins (even if the constraint does not apply, so that the plugin can display a checkmark)
+                        data = {
+                            request: null,
+                            key   : null,
+                            value : resource_key,
+                            status: STATE_WARNINGS
+                        };
+                        manifold.raise_record_event(query_uuid, FIELD_STATE_CHANGED, data);
+
+                    case 'lease':
+                        var resource_key = record.resource;
+                        var resource_query = query_ext.parent_query_ext.query.subqueries['resource'];
+                        var warnings = manifold.query_store.get_record_state(resource_query.query_uuid, resource_key, STATE_WARNINGS);
+
+                        if (event_type == SET_ADD) {
+                             // A lease is added, it removes the constraint
+                            delete warnings[CONSTRAINT_RESERVABLE_LEASE];
+                        } else {
+                            // A lease is removed, it might trigger the warning
+                            var lease_records = $.grep(query_ext.records, function(lease_key, lease) {
+                                return lease['resource'] == value;
+                            });
+                            if (lease_records.length == 0) { // XXX redundant cases
+                                // Sets a warning
+                                // XXX Need for a better function to manage warnings
+                                var warn = "No lease defined for this reservable resource.";
+                                warnings[CONSTRAINT_RESERVABLE_LEASE] = warn;
+                            } else {
+                                // Lease are defined, delete the warning in case it was set previously
+                                delete warnings[CONSTRAINT_RESERVABLE_LEASE];
+                            }
+                            
+                        }
 
+                        // Signal the change to plugins (even if the constraint does not apply, so that the plugin can display a checkmark)
+                        data = {
+                            request: null,
+                            key   : null,
+                            value : resource_key,
+                            status: STATE_WARNINGS
+                        };
+                        manifold.raise_record_event(resource_query.query_uuid, FIELD_STATE_CHANGED, data);
+                        break;
                 }
-                // Signal the change to plugins (even if the constraint does not apply, so that the plugin can display a checkmark)
-                data = {
-                    request: null,
-                    key   : null,
-                    value : resource_key,
-                    status: STATE_WARNINGS
-                };
-                manifold.raise_record_event(query_uuid, FIELD_STATE_CHANGED, data);
+
 
                 // -) When a lease is added, it might remove the warning associated to a reservable node
 
index da26f1f..76e1cb5 100644 (file)
@@ -1,3 +1,24 @@
+// Common parts for angularjs plugins
+// only one ng-app is allowed
+
+var ManifoldApp = angular.module('ManifoldApp', []);
+ManifoldApp.config(function ($interpolateProvider) {
+    $interpolateProvider.startSymbol('{[{').endSymbol('}]}');
+});
+
+ManifoldApp.factory('$exceptionHandler', function () {
+    return function (exception, cause) {
+        console.log(exception.message);
+    };
+});
+
+ManifoldApp.filter('offset', function() {
+  return function(input, start) {
+    start = parseInt(start, 10);
+    return input.slice(start);
+  };
+});
+
 // INHERITANCE
 // http://alexsexton.com/blog/2010/02/using-inheritance-patterns-to-organize-large-jquery-applications/
 // We will use John Resig's proposal
index 9c3fe4f..d5a13be 100644 (file)
             this.elts('list-group-item').click({'instance': this}, this._on_click);
 
             this.prev_filter_status = null;
+
+            /* Initialize tooltips */
+            $("[rel='tooltip']").tooltip();
+
         },
 
     /**************************************************************************
     // These functions are here to react on external filters, which we don't
     // use at the moment
 
-    on_filter_added: function(filter) {
+    on_filter_added: function(filter) 
+    {
         // XXX
     },
 
-    on_filter_removed: function(filter) {
+    on_filter_removed: function(filter) 
+    {
         // XXX
     },
 
+    on_field_state_changed: function(data) 
+    {
+        var query_ext;
+        
+        switch (data.status) {
+            case STATE_SET:
+                /* Get the number of pending / unconfigured resources */
+                /* Let's store it in query_ext */
+                query_ext = manifold.query_store.find_analyzed_query_ext(this.options.query_uuid);
+
+                $('#badge-pending').text(query_ext.num_pending);
+                if (query_ext.num_pending > 0) {
+                                       $('#badge-pending').show();
+                } else {
+                                       $('#badge-pending').hide();
+                }
+
+                $('#badge-unconfigured').text(query_ext.num_unconfigured);
+                if (query_ext.num_unconfigured > 0) {
+                                       $('#badge-unconfigured').show();
+                } else {
+                                       $('#badge-unconfigured').hide();
+                }
+            default:
+                break;
+        }
+    },
+
     /**************************************************************************
      *                            PRIVATE METHODS                             *
      **************************************************************************/
index 0023796..b6ce25d 100644 (file)
@@ -1,7 +1,43 @@
 <div id={{ domid }}>
-<span class="list-group-item-heading">Filter on status:</span>
-<a href="#" class="list-group-item sl-platform active" style='display: inline-block !important;' id="{{ domid }}__all" data-status="all"><p class="list-group-item-heading">All</p></a>
-<a href="#" class="list-group-item sl-platform" style='display: inline-block !important;' id="{{ domid }}__reserved" data-status="reserved"><p class="list-group-item-heading">Reserved</p></a>
-<a href="#" class="list-group-item sl-platform" style='display: inline-block !important;' id="{{ domid }}__unconfigured" data-status="unconfigured"><p class="list-group-item-heading">Unconfigured</p></a>
-<a href="#" class="list-group-item sl-platform" style='display: inline-block !important;' id="{{ domid }}__pending" data-status="pending"><p class="list-group-item-heading">Pending</p></a>
+  <span class="list-group-item-heading">View:</span>
+  
+  <a href="#" 
+     class="list-group-item sl-platform active" 
+     style='display: inline-block !important;' 
+     id="{{ domid }}__all" 
+     data-status="all"
+        title="View resources that are available to be reserved."
+        rel='tooltip'>
+       <p class="list-group-item-heading">Available</p>
+  </a>
+  
+  <a href="#"
+     class="list-group-item sl-platform" 
+     style='display: inline-block !important;' 
+     id="{{ domid }}__reserved" 
+     data-status="reserved"
+     title="View resources that you have previously reserved for the slice."
+        rel='tooltip'>
+       <p class="list-group-item-heading">Reserved</p>
+  </a>
+  
+  <a href="#" class="list-group-item sl-platform"
+     style='display: inline-block !important;' 
+     id="{{ domid }}__unconfigured" 
+     data-status="unconfigured"
+     title="View resources that you have selected to add to your slice, that require configuration before they can be reserved. Hover you mouse over the symbol aside the checkbox for more details."
+     rel='tooltip'>
+       <p class="list-group-item-heading">Unconfigured</p>
+       <span class="badge" id="badge-unconfigured" style="display:none;"></span></a>
+  </a>
+  
+  <a href="#" class="list-group-item sl-platform" 
+     style='display: inline-block !important;' 
+     id="{{ domid }}__pending" 
+     data-status="pending"
+     title="View pending changes to your slice, resources that you have selected to add, and resources that you have selected to remove. Click on the Apply button to apply those changes, and on the Cancel button to cancel them."
+     rel='tooltip'>
+       <p class="list-group-item-heading">Pending</p>
+       <span class="badge" id="badge-pending" style="display:none;"></span></a>
+  </a>
 </div> 
index 2922906..2182e70 100644 (file)
@@ -33,6 +33,12 @@ Current implementation makes the following assumptions
   as we use 'aoColumnDefs' instead.
 """
 
+    MAP = {
+        'network_hrn'   :   'Testbed',
+        'hostname'      :   'Resource name',
+        'type'          :   'Type',
+    }
+
     def __init__ (self, query=None, query_all=None, 
                   checkboxes=False, columns=None, 
                   init_key=None,
@@ -43,20 +49,28 @@ Current implementation makes the following assumptions
         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 = []
+            _columns = columns
+            _hidden_columns = []
         elif self.query:
-            self.columns = self.query.fields
+            _columns = [field for field in self.query.fields if not field == 'urn']
             if query_all:
                 # We need a list because sets are not JSON-serializable
-                self.hidden_columns = list(self.query_all.fields - self.query.fields)
+                _hidden_columns = list(self.query_all.fields - self.query.fields)
+                _hidden_columns.append('urn')
             else:
-                self.hidden_columns = []
+                _hidden_columns = []
         else:
-            self.columns = []
-            self.hidden_columns = []
+            _columns = []
+            _hidden_columns = []
+
+        print "_columns=", _columns
+        self.columns = { self.MAP.get(c, c) : c for c in _columns }
+        self.hidden_columns = { self.MAP.get(c, c) : c for c in _hidden_columns }
+        print "self.columns", self.columns
+
         self.init_key=init_key
         self.datatables_options=datatables_options
         # if checkboxes were required, we tell datatables about this column's type
index e4a3308..6879321 100644 (file)
@@ -1,4 +1,6 @@
-
+tr.even {
+    background-color: #FAFAFA;
+}
 div .added {
        background-color: #FFFF99;
 }
index 3731f7a..47e0a97 100644 (file)
@@ -10,6 +10,13 @@ BGCOLOR_REMOVED = 2;
 
 (function($){
 
+    
+    var QUERYTABLE_MAP = {
+        'Testbed': 'network_hrn',
+        'Resource name': 'hostname',
+        'Type': 'type',
+    };
+
     var debug=false;
 //    debug=true
 
@@ -24,17 +31,11 @@ BGCOLOR_REMOVED = 2;
            // 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 datatables filtering
             this.filters = Array(); 
 
-            // an internal buffer for records that are 'in' and thus need to be checked 
-            this.buffered_records_to_check = [];
-           // an internal buffer for keeping lines and display them in one call to fnAddData
-           this.buffered_lines = [];
-
             /* Events */
            // xx somehow non of these triggers at all for now
             this.elmt().on('show', this, this.on_show);
@@ -115,7 +116,7 @@ BGCOLOR_REMOVED = 2;
                     var key = self.canonical_key;
 
                     // Get the index of the key in the columns
-                    var cols = self.table.fnSettings().aoColumns;
+                    var cols = self._get_columns();
                     var index = self.getColIndex(key, cols);
                     if (index != -1) {
                         // The key is found in the table, set the TR id after the data
@@ -177,7 +178,8 @@ BGCOLOR_REMOVED = 2;
          * @param cols
          */
         getColIndex: function(key, cols) {
-            var tabIndex = $.map(cols, function(x, i) { if (x.sTitle == key) return i; });
+            var self = this;
+            var tabIndex = $.map(cols, function(x, i) { if (self._get_map(x.sTitle) == key) return i; });
             return (tabIndex.length > 0) ? tabIndex[0] : -1;
         }, // getColIndex
 
@@ -209,13 +211,15 @@ BGCOLOR_REMOVED = 2;
 
         new_record: function(record)
         {
+            var self = this;
+
             // this models a line in dataTables, each element in the line describes a cell
             line = new Array();
      
             // go through table headers to get column names we want
             // in order (we have temporarily hack some adjustments in names)
-            var cols = this.table.fnSettings().aoColumns;
-            var colnames = cols.map(function(x) {return x.sTitle})
+            var cols = this._get_columns();
+            var colnames = cols.map(function(x) {return self._get_map(x.sTitle)})
             var nb_col = cols.length;
             /* if we've requested checkboxes, then forget about the checkbox column for now */
             //if (this.options.checkboxes) nb_col -= 1;
@@ -224,9 +228,10 @@ BGCOLOR_REMOVED = 2;
                 // Use a key instead of hostname (hard coded...)
                 line.push(this.checkbox_html(record));
                }
+            line.push('<span id="' + this.id_from_key('status', record[this.init_key]) + '"></span>'); // STATUS
                
             /* fill in stuff depending on the column name */
-            for (var j = 1; j < nb_col - 1; j++) { // nb_col includes status
+            for (var j = 2; j < nb_col - 1; j++) { // nb_col includes status
                 if (typeof colnames[j] == 'undefined') {
                     line.push('...');
                 } else if (colnames[j] == 'hostname') {
@@ -258,7 +263,6 @@ BGCOLOR_REMOVED = 2;
                         line.push('');
                 }
             }
-            line.push('<span id="' + this.id_from_key('status', record[this.init_key]) + '"></span>'); // STATUS
     
            // adding an array in one call is *much* more efficient
                // this.table.fnAddData(line);
@@ -371,7 +375,7 @@ BGCOLOR_REMOVED = 2;
                 elt.addClass((class_name == BGCOLOR_ADDED ? 'added' : 'removed'));
         },
 
-        do_filter: function()
+        populate_table: function()
         {
             // Let's clear the table and only add lines that are visible
             var self = this;
@@ -383,7 +387,7 @@ BGCOLOR_REMOVED = 2;
 
             lines = Array();
             var record_keys = [];
-            manifold.query_store.iter_visible_records(this.options.query_uuid, function (record_key, record) {
+            manifold.query_store.iter_records(this.options.query_uuid, function (record_key, record) {
                 lines.push(self.new_record(record));
                 record_keys.push(record_key);
             });
@@ -420,29 +424,17 @@ BGCOLOR_REMOVED = 2;
 
         on_filter_added: function(filter)
         {
-            this.do_filter();
-
-            /*
-            this.filters.push(filter);
             this.redraw_table();
-            */
         },
 
         on_filter_removed: function(filter)
         {
-            this.do_filter();
-            /*
-            // Remove corresponding filters
-            this.filters = $.grep(this.filters, function(x) {
-                return x == filter;
-            });
             this.redraw_table();
-            */
         },
         
         on_filter_clear: function()
         {
-            this.do_filter();
+            this.redraw_table();
         },
 
         on_field_added: function(field)
@@ -460,56 +452,8 @@ BGCOLOR_REMOVED = 2;
             alert('QueryTable::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)
-        {
-            this.do_filter();
-        },
-
-        on_all_filter_removed: function(filter)
-        {
-            this.do_filter();
-        },
-        
-        on_all_filter_clear: function()
-        {
-            this.do_filter();
-        },
-
-        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('QueryTable::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_from_record(record, true);
-            } else {
-                this.buffered_records_to_check.push(record);
-            }
-        },
-
-        on_clear_records: function()
-        {
-        },
-
         // Could be the default in parent
         on_query_in_progress: function()
         {
@@ -518,12 +462,8 @@ BGCOLOR_REMOVED = 2;
 
         on_query_done: function()
         {
-            this.do_filter();
-/*
-            this.received_query = true;
-           // unspin once we have received both
-            if (this.received_all_query && this.received_query) this.unspin();
-*/
+            this.populate_table();
+            this.spin(false);
         },
         
         on_field_state_changed: function(data)
@@ -563,62 +503,38 @@ BGCOLOR_REMOVED = 2;
 
         /************************** PRIVATE METHODS ***************************/
 
+        _get_columns: function()
+        {
+            return this.table.fnSettings().aoColumns;
+            // XXX return $.map(table.fnSettings().aoColumns, function(x, i) { return QUERYTABLE_MAP[x]; });
+        },
+
+        _get_map: function(column_title) {
+            return (column_title in QUERYTABLE_MAP) ? QUERYTABLE_MAP[column_title] : column_title;
+        },
         /** 
-         * @brief QueryTable filtering function
+         * @brief QueryTable filtering function, called for every line in the datatable.
+         * 
+         * Return value:
+         *   boolean determining whether the column is visible or not.
          */
         _querytable_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 == 'included') {
-                    $.each(value, function(i,x) {
-                      if(x == col_value){
-                          ret = true;
-                          return false;
-                      }else{
-                          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;
-                }
+            var self = this;
+            var key_col, record_key_value;
 
-            });
-            return ret;
+            /* Determine index of key in the table columns */
+            key_col = $.map(oSettings.aoColumns, function(x, i) {if (self._get_map(x.sTitle) == self.canonical_key) return i;})[0];
+
+            /* Unknown key: no filtering */
+            if (typeof(key_col) == 'undefined') {
+                console.log("Unknown key");
+                return true;
+            }
+
+            record_key_value = unfold.get_value(aData[key_col]);
+
+            return manifold.query_store.get_record_state(this.options.query_uuid, record_key_value, STATE_VISIBLE);
         },
 
         _querytable_draw_callback: function()
index ddeb9cf..a1792b4 100644 (file)
@@ -2,20 +2,20 @@
   <table class="table dataTable" id="{{domid}}__table" width="100%">
     <thead>
       <tr>
-       {% if checkboxes %}<th class="checkbox">+/-</th>{% endif %}
-        {% for column in columns %} <th>{{ column }}</th> {% endfor %} 
-        {% for column in hidden_columns %} <th>{{ column }}</th> {% endfor %}
-        <th class="checkbox">status</th>
+       {% if checkboxes %}<th class="checkbox"><input type="checkbox" disabled/></th>{% endif %}
+        <th></th>
+        {% for column, field in columns.items %} <th>{{ column }}</th> {% endfor %} 
+        {% for column, field in hidden_columns.items %} <th>{{ column }}</th> {% endfor %}
       </tr>
     </thead> 
     <tbody>
     </tbody>
     <tfoot>
       <tr>
-       {% if checkboxes %} <th>+/-</th> {% endif %}
-        {% for column in columns %} <th>{{ column }}</th> {% endfor %} 
-        {% for column in hidden_columns %} <th>{{ column }}</th> {% endfor %} 
-        <th class="checkbox">status</th>
+       {% if checkboxes %} <th><input type="checkbox" disabled/></th> {% endif %}
+        <th></th>
+        {% for column, field in columns.items %} <th>{{ column }}</th> {% endfor %} 
+        {% for column, field in hidden_columns.items %} <th>{{ column }}</th> {% endfor %} 
       </tr>
     </tfoot> 
   </table>
index 167e381..4bd5189 100644 (file)
                     row = this.find_row(data.value);
                     if (row)
                         this.table.fnDeleteRow(row.nTr);
+                        /* indent was wrong !!
                         $("#badge-pending").data('number', $("#badge-pending").data('number') - 1 );
                         $("#badge-pending").text($("#badge-pending").data('number'));
+                        */
                     return;
                     break;  
                 case STATE_SET_IN_SUCCESS:
                 // XXX second parameter refresh = false can improve performance. todo in querytable also
                 this.table.fnAddData(newline);
                 row = this.find_row(data.value);
+                /*
                 $("#badge-pending").data('number', $("#badge-pending").data('number') + 1 );
                 $("#badge-pending").text($("#badge-pending").data('number'));
+                */
             } else {
                 // Update row text...
                 this.table.fnUpdate(newline, row.nTr);
index 040b274..abe8eea 100755 (executable)
@@ -5,15 +5,10 @@ from datetime import timedelta
 class Scheduler2 (Plugin):\r
 \r
 \r
-    def __init__ (self, query, query_lease, query_all_resources, query_all_leases, **settings):\r
+    def __init__ (self, query, query_lease, **settings):\r
         Plugin.__init__ (self, **settings)\r
         \r
         self.query=query\r
-        self.query_all_resources = query_all_resources\r
-        self.query_all_resources_uuid = query_all_resources.query_uuid\r
-\r
-        self.query_all_leases = query_all_leases\r
-        self.query_all_leases_uuid = query_all_leases.query_uuid\r
 \r
         self.query_lease = query_lease\r
         self.query_lease_uuid = query_lease.query_uuid\r
@@ -35,12 +30,9 @@ class Scheduler2 (Plugin):
     def requirements (self):\r
         reqs = {\r
             'js_files' : [\r
-                'js/angular/angular.min.js',\r
                 'js/scheduler2.js',\r
-                'js/scheduler-SchedulerCtrl.js',\r
                 #'js/slider/jquery-ui-1.10.3.slider.min.js',\r
                 'js/scheduler-helpers.js',\r
-                'js/scheduler-table-selector.js',\r
             ],\r
             'css_files': [\r
                 'css/scheduler2.css', \r
@@ -54,7 +46,7 @@ class Scheduler2 (Plugin):
         # query_uuid will pass self.query results to the javascript\r
         # and will be available as "record" in :\r
         # on_new_record: function(record)\r
-        return ['plugin_uuid', 'domid', 'query_uuid', 'time_slots', 'nodes', 'query_lease_uuid', 'query_all_resources_uuid', 'query_all_leases_uuid']\r
+        return ['plugin_uuid', 'domid', 'query_uuid', 'time_slots', 'nodes', 'query_lease_uuid']\r
     \r
 \r
     def export_json_settings (self):\r
index e4de904..cb844a8 100755 (executable)
 \r
 /** tables css **/\r
 \r
+#scheduler-reservation-table > tbody > tr > th {\r
+       font-weight: normal;\r
+}\r
+#scheduler-reservation-table > thead > tr > th {\r
+       font-weight: normal;\r
+}\r
+\r
 /*#ShedulerNodes-scroll-container {\r
     float: left;\r
     overflow-x: scroll;\r
     border: none !important;\r
 }\r
 #scheduler-reservation-table tbody tr td{\r
-    background-color: #A6C9E2 ;\r
-    border: 1px solid #111111;\r
+    background-color: #FFFFFF; /*#A6C9E2 ;*/\r
+/*    border: 1px solid #111111;*/\r
 }\r
 \r
 #scheduler-reservation-table tbody tr.even td{\r
-    background-color: #E0E0E0 ;\r
+    background-color: #FAFAFA; /*E0E0E0 ;*/\r
 }\r
 \r
 #scheduler-reservation-table tbody tr th::selection {color:    #000000;background:transparent;}\r
 }\r
 \r
 #scheduler-reservation-table tbody tr td.reserved {\r
-    background: url("../img/closed-lock-15.png") no-repeat scroll 50% 50% #DD4444;\r
+    background: url("../img/closed-lock-15.png") no-repeat scroll 50% 50%; /* #DD4444;*/\r
     cursor: not-allowed;\r
 }\r
 \r
     background: url("../img/tools-15.png") no-repeat scroll 50% 50% #EDA428;\r
 }\r
 \r
+#scheduler-reservation-table tbody tr td.pendingin {\r
+    background: #FFFF99;\r
+}\r
+\r
+\r
+#scheduler-reservation-table tbody tr td.pendingout {\r
+    background: #E8E8E8;\r
+}\r
+\r
 #scheduler-reservation-table tbody tr td.free:hover ,#scheduler-reservation-table tbody tr td.selected, #scheduler-reservation-table tbody tr td.selected_tmp {\r
     background: #25BA25;\r
 }\r
@@ -266,4 +282,4 @@ td.no-image {
 }\r
 .table-responsive{\r
     overflow: hidden !important;\r
-}
\ No newline at end of file
+}\r
diff --git a/plugins/scheduler2/static/js/scheduler-SchedulerCtrl.js b/plugins/scheduler2/static/js/scheduler-SchedulerCtrl.js
deleted file mode 100755 (executable)
index 2732044..0000000
+++ /dev/null
@@ -1,403 +0,0 @@
-var myApp = angular.module('myApp', []);\r
-myApp.config(function ($interpolateProvider) {\r
-    $interpolateProvider.startSymbol('{[{').endSymbol('}]}');\r
-});\r
-myApp.factory('$exceptionHandler', function () {\r
-    return function (exception, cause) {\r
-        if (exception.message.contains('leases')) {\r
-            console.log(exception.message);\r
-            \r
-            var tmpScope = angular.element(document.getElementById('SchedulerCtrl')).scope();\r
-            //tmpScope.initSlots(_schedulerCurrentCellPosition, _schedulerCurrentCellPosition + SchedulerTotalVisibleCells);\r
-        }\r
-            \r
-    };\r
-});\r
-\r
-myApp.filter('offset', function() {\r
-  return function(input, start) {\r
-    start = parseInt(start, 10);\r
-    return input.slice(start);\r
-  };\r
-});\r
-\r
-// Create a private execution space for our controller. When\r
-// executing this function expression, we're going to pass in\r
-// the Angular reference and our application module.\r
-(function (ng, app) {\r
-\r
-\r
-    // Define our Controller constructor.\r
-    function Controller($scope) {\r
-\r
-        // Store the scope so we can reference it in our\r
-        // class methods\r
-        this.scope = $scope;\r
-\r
-        // Set up the default scope value.\r
-        this.scope.errorMessage = null;\r
-        this.scope.name = "";\r
-\r
-        //Pagin\r
-        $scope.current_page = 1;\r
-        this.scope.items_per_page = 10;\r
-        $scope.from = 0; // JORDAN\r
-\r
-        $scope.resources = new Array();\r
-        $scope.slots = SchedulerSlotsViewData;\r
-        $scope.granularity = DEFAULT_GRANULARITY; /* Will be setup */\r
-        //$scope.msg = "hello";\r
-\r
-        angular.element(document).ready(function() {\r
-            //console.log('Hello World');\r
-            //alert('Hello World');\r
-            //afterAngularRendered();\r
-        });\r
-\r
-        // Jordan\r
-/*\r
-        $scope.redraw = function()\r
-        {\r
-\r
-            // Refresh slots\r
-            $scope.slots = [];\r
-            for (var i = $scope.from; i < $scope.from + SchedulerTotalVisibleCells; i++)\r
-                $scope.slots.push(SchedulerSlots[i]);\r
-\r
-\r
-            // Collect lease information. This could be made once if no refresh... \r
-            lease_by_resource = {};\r
-            manifold.query_store.iter_visible_records($scope.options.query_lease_uuid, function (record_key, record) {\r
-                lease_by_resource[record['resource']] = record;\r
-                // Need something to interrupt the loop\r
-            });\r
-\r
-            // Create resources\r
-            $scope.resources = [];\r
-            // current_page, items_per_page indicates which resources to show\r
-            manifold.query_store.iter_visible_records($scope.options.query_uuid, function (record_key, record) {\r
-                // copy not to modify original record\r
-                var resource = jQuery.extend(true, {}, record);\r
-                resource.leases = []; // a list of occupied timeslots\r
-\r
-                // How many timeslots ? SchedulerTotalVisibleCells\r
-                // SchedulerDateSelected\r
-                // from : to ??\r
-                // slot duration ?\r
-                for (i=0; i < SchedulerTotalVisibleCells; i++) {\r
-                    resource.leases.push({\r
-                        'id': 'coucou',\r
-                        'status': 'free', // 'selected', 'reserved', 'maintenance'\r
-                    });\r
-                }\r
-\r
-                // For each lease we need to mark slots appropriately\r
-                if (lease_by_resource[resource['urn']]) {\r
-                    $.each(lease_by_resource[resource['urn']], function(i, lease) {\r
-                        // $scope.from * GRANULARITY minutes since start\r
-                        $scope.from * GRANULARITY\r
-                        from_date = new Date(date.getTime() + ($scope.from * GRANULARITY) * 60000);\r
-                        to_date   = new Date(date.getTime() + (($scope.from + SchedulerTotalVisibleCells) * GRANULARITY) * 60000);\r
-                        // start_time, end_time\r
-                    });\r
-                }\r
-                \r
-                $scope.resources.push(resource);\r
-                $scope.$apply();\r
-            });\r
-        }\r
-*/\r
-        $scope.clearStuff = function() {\r
-            $scope.resources = new Array();\r
-            $scope.$apply();\r
-        }\r
-\r
-        // Called at initialization, after filtering, and after changing the date.\r
-        // this is like setpage(1) ???\r
-/*\r
-        $scope.initSchedulerResources = function (items_per_page) {\r
-            $scope.resources = new Array();\r
-\r
-            for (var k = 0; k < items_per_page; k++) {\r
-                $scope.resources.push(jQuery.extend(true, {}, SchedulerDataViewData[k]));\r
-                $scope.resources[k].leases = [];\r
-            }\r
-            $scope.items_per_page = items_per_page;\r
-            $scope.current_page = 0;\r
-            $scope.totalPages = parseInt(Math.ceil(SchedulerDataViewData.length / $scope.items_per_page));\r
-            $scope.initSlots(0, SchedulerTotalVisibleCells);\r
-        };\r
-*/\r
-\r
-        // Pagination\r
-\r
-        $scope.range = function() {\r
-            var range_size = $scope.page_count() > DEFAULT_PAGE_RANGE ? DEFAULT_PAGE_RANGE : $scope.page_count();\r
-            var ret = [];\r
-            var start;\r
-\r
-            start = $scope.current_page;\r
-            if ( start > $scope.page_count()-range_size ) {\r
-              start = $scope.page_count()-range_size+1;\r
-            }\r
-\r
-            for (var i=start; i<start+range_size; i++) {\r
-              ret.push(i);\r
-            }\r
-            return ret;\r
-        };\r
-\r
-        $scope.prevPage = function() {\r
-          if ($scope.current_page > 1) {\r
-            $scope.current_page--;\r
-          }\r
-        };\r
-\r
-        $scope.prevPageDisabled = function() {\r
-          return $scope.current_page === 1 ? "disabled" : "";\r
-        };\r
-  \r
-        $scope.page_count = function() {\r
-          return Math.ceil($scope.resources.length/$scope.items_per_page);\r
-        };\r
-  \r
-        $scope.nextPage = function() {\r
-          if ($scope.current_page < $scope.page_count()) {\r
-            $scope.current_page++;\r
-          }\r
-        };\r
-  \r
-        $scope.nextPageDisabled = function() {\r
-          return $scope.current_page === $scope.page_count() ? "disabled" : "";\r
-        }; \r
-\r
-        $scope.setPage = function(n) {\r
-            $scope.current_page = n;\r
-        };\r
-        // END pagination\r
-\r
-        // FILTER\r
-\r
-        $scope.filter_visible = function(resource)\r
-        {\r
-            return manifold.query_store.get_record_state($scope.options.query_uuid, resource['urn'], STATE_VISIBLE);\r
-        };\r
-\r
-        // SELECTION\r
-\r
-        $scope.select = function(index, model_lease, model_resource)\r
-        {\r
-            // XXX\r
-            // XXX Events won't work until we properly handle sets with composite keys\r
-            // XXX\r
-            console.log("Selected", index, model_lease, model_resource);\r
-\r
-            if (model_lease.status != 'free') {\r
-                console.log("Already selected slot");\r
-                return;\r
-            }\r
-            \r
-            var day_timestamp = SchedulerDateSelected.getTime() / 1000;\r
-            var start_time = day_timestamp + index       * model_resource.granularity;\r
-            var end_time   = day_timestamp + (index + 1) * model_resource.granularity;\r
-            var start_date = new Date(start_time * 1000);\r
-            var end_date   = new Date(end_time   * 1000);\r
-\r
-            var lease_key = manifold.metadata.get_key('lease');\r
-\r
-            // We search for leases in the cache we previously constructed\r
-            var resource_leases = $scope._leases_by_resource[model_resource.urn];\r
-            if (resource_leases) {\r
-                /* Search for leases before */\r
-                $.each(resource_leases, function(i, other) {\r
-                    if (other.end_time != start_time)\r
-                        return true; // ~ continue\r
-\r
-                    /* The lease 'other' is just before, and there should not exist\r
-                     * any other lease before it */\r
-                    start_time = other.start_time;\r
-\r
-                    other_key = {\r
-                        resource:   other.resource,\r
-                        start_time: other.start_time,\r
-                        end_time:   other.end_time\r
-                    }\r
-                    // This is needed to create a hashable object\r
-                    other_key.hashCode = manifold.record_hashcode(lease_key.sort());\r
-                    other_key.equals   = manifold.record_equals(lease_key);\r
-\r
-                    manifold.raise_event($scope.options.query_lease_uuid, SET_REMOVED, other_key);\r
-                    /* Remove from local cache also, unless we listen to events from outside */\r
-                    $.grep($scope._leases_by_resource[model_resource.urn], function(x) { return x != other; });\r
-                    return false; // ~ break\r
-                });\r
-\r
-                /* Search for leases after */\r
-                $.each(resource_leases, function(i, other) {\r
-                    if (other.start_time != end_time)\r
-                        return true; // ~ continue\r
-\r
-                    /* The lease 'other' is just after, and there should not exist\r
-                     * any other lease after it */\r
-                    end_time = other.end_time;\r
-                    // XXX SET_ADD and SET_REMOVE should accept full objects\r
-                    other_key = {\r
-                        resource:   other.resource,\r
-                        start_time: other.start_time,\r
-                        end_time:   other.end_time\r
-                    }\r
-                    // This is needed to create a hashable object\r
-                    other_key.hashCode = manifold.record_hashcode(lease_key.sort());\r
-                    other_key.equals   = manifold.record_equals(lease_key);\r
-\r
-                    manifold.raise_event($scope.options.query_lease_uuid, SET_REMOVED, other_key);\r
-                    /* Remove from local cache also, unless we listen to events from outside */\r
-                    $.grep($scope._leases_by_resource[model_resource.urn], function(x) { return x != other; });\r
-                    return false; // ~ break\r
-                });\r
-            }\r
-\r
-            /* Create a new lease */\r
-            new_lease = {\r
-                resource:   model_resource.urn,\r
-                start_time: start_time,\r
-                end_time:   end_time,\r
-            };\r
-\r
-            // This is needed to create a hashable object\r
-            new_lease.hashCode = manifold.record_hashcode(lease_key.sort());\r
-            new_lease.equals   = manifold.record_equals(lease_key);\r
-\r
-            manifold.raise_event($scope.options.query_lease_uuid, SET_ADD, new_lease);\r
-            /* Add to local cache also, unless we listen to events from outside */\r
-            if (!(model_resource.urn in $scope._leases_by_resource))\r
-                $scope._leases_by_resource[model_resource.urn] = [];\r
-            $scope._leases_by_resource[model_resource.urn].push(new_lease);\r
-\r
-            // XXX Shall we set it or wait for manifold event ?\r
-            model_lease.status = 'reserved'; // XXX pending\r
-\r
-            // DEBUG: display all leases and their status in the log\r
-            var leases = manifold.query_store.get_records($scope.options.query_lease_uuid);\r
-            console.log("--------------------");\r
-            $.each(leases, function(i, lease) {\r
-                var key = manifold.metadata.get_key('lease');\r
-                var lease_key = manifold.record_get_value(lease, key);\r
-                var state = manifold.query_store.get_record_state($scope.options.query_lease_uuid, lease_key, STATE_SET);\r
-                var state_str;\r
-                switch(state) {\r
-                    case STATE_SET_IN:\r
-                        state_str = 'STATE_SET_IN';\r
-                        break;\r
-                    case STATE_SET_OUT:\r
-                        state_str = 'STATE_SET_OUT';\r
-                        break;\r
-                    case STATE_SET_IN_PENDING:\r
-                        state_str = 'STATE_SET_IN_PENDING';\r
-                        break;\r
-                    case STATE_SET_OUT_PENDING:\r
-                        state_str = 'STATE_SET_OUT_PENDING';\r
-                        break;\r
-                    case STATE_SET_IN_SUCCESS:\r
-                        state_str = 'STATE_SET_IN_SUCCESS';\r
-                        break;\r
-                    case STATE_SET_OUT_SUCCESS:\r
-                        state_str = 'STATE_SET_OUT_SUCCESS';\r
-                        break;\r
-                    case STATE_SET_IN_FAILURE:\r
-                        state_str = 'STATE_SET_IN_FAILURE';\r
-                        break;\r
-                    case STATE_SET_OUT_FAILURE:\r
-                        state_str = 'STATE_SET_OUT_FAILURE';\r
-                        break;\r
-                }\r
-                console.log("LEASE", new Date(lease.start_time * 1000), new Date(lease.end_time * 1000), lease.resource, state_str);\r
-            });\r
-        };\r
-  \r
-\r
-/*\r
-        $scope.setPage = function(page) {\r
-            var tmpFrm = $scope.items_per_page * page;\r
-            var tmpTo = tmpFrm + $scope.items_per_page;\r
-            tmpTo = SchedulerDataViewData.length < tmpTo ? SchedulerDataViewData.length : tmpTo;\r
-            $scope.current_page = page;\r
-            $scope.resources = [];\r
-            var j = 0;\r
-            for (var k = tmpFrm; k < tmpTo; k++) {\r
-                $scope.resources.push(jQuery.extend(true, {}, SchedulerDataViewData[k]));\r
-                $scope.resources[j].leases = [];\r
-                j++;\r
-            }\r
-            //fix slider\r
-            $('#tblSlider').slider('value', 0);\r
-            //init Slots\r
-            $scope.initSlots(0, SchedulerTotalVisibleCells);\r
-        };*/\r
-\r
-        // Typically we will only init visible slots\r
-        $scope.initSlots = function (from, to) {\r
-            return; // JORDAN !!!\r
-\r
-            //init\r
-            $scope.slots = [];\r
-\r
-            var resourceIndex; //gia to paging\r
-            //set\r
-            for (var i = from; i < to; i++) {\r
-                $scope.slots.push(SchedulerSlots[i]);\r
-                resourceIndex = $scope.items_per_page * $scope.current_page;\r
-                for (var j = 0; j < $scope.resources.length; j++) {\r
-                    if (i == from) {\r
-                        $scope.resources[j].leases = [];\r
-                    }\r
-                    $scope.resources[j].leases.push(SchedulerDataViewData[resourceIndex].leases[i]);\r
-                    resourceIndex++;\r
-                }\r
-            }\r
-            //apply\r
-            $scope.$apply();\r
-        };\r
-\r
-/*\r
-        $scope.getPageNumbers = function () {\r
-            var totalNumbersShowned = ($scope.totalPages > 10 ? 10 : $scope.totalPages + 1 );\r
-            var tmtNumDiv = totalNumbersShowned / 2;\r
-            //local\r
-            var numFrom = 1;\r
-            var numTo = totalNumbersShowned;\r
-            var rtrnArr = new Array();\r
-\r
-            if (totalNumbersShowned > 1) {\r
-                //set from - to\r
-                if ($scope.totalPages > totalNumbersShowned) {\r
-                    if ($scope.current_page <= tmtNumDiv) {\r
-                        //nothing\r
-                    } else if ($scope.current_page >= $scope.totalPages - tmtNumDiv) {\r
-                        numTo = $scope.totalPages;\r
-                        numFrom = numTo - totalNumbersShowned;\r
-                    } else {\r
-                        numFrom = $scope.current_page - tmtNumDiv;\r
-                        numTo = numFrom + totalNumbersShowned;\r
-                    }\r
-                }\r
-\r
-                for (var i = numFrom; i < numTo; i++)\r
-                    rtrnArr.push(i);\r
-            } else {\r
-                rtrnArr.push(1);\r
-            }\r
-            return rtrnArr;\r
-        };\r
-*/\r
-        // Return this object reference.\r
-        return (this);\r
-\r
-    }\r
-\r
-\r
-    // Define the Controller as the constructor function.\r
-    app.controller("SchedulerCtrl", Controller);\r
-\r
-\r
-})(angular, myApp);\r
index fe8e3e8..5f35a0f 100755 (executable)
@@ -35,7 +35,7 @@ var scheduler2Instance;
 var schedulerCtrlPressed = false;\r
 //table Id\r
 var schedulerTblId = "scheduler-reservation-table";\r
-var SCHEDULER_FIRST_COLWIDTH = 150;\r
+var SCHEDULER_FIRST_COLWIDTH = 200;\r
 \r
 \r
 /* Number of scheduler slots per hour. Used to define granularity. Should be inferred from resources XXX */\r
@@ -69,6 +69,331 @@ var tmpSchedulerLeases = [];
 \r
 var SCHEDULER_COLWIDTH = 50;\r
 \r
+\r
+/******************************************************************************\r
+ *                             ANGULAR CONTROLLER                             *\r
+ ******************************************************************************/\r
+\r
+// Create a private execution space for our controller. When\r
+// executing this function expression, we're going to pass in\r
+// the Angular reference and our application module.\r
+(function (ng, app) {\r
+\r
+    // Define our Controller constructor.\r
+    function Controller($scope) {\r
+\r
+        // Store the scope so we can reference it in our\r
+        // class methods\r
+        this.scope = $scope;\r
+\r
+        // Set up the default scope value.\r
+        this.scope.errorMessage = null;\r
+        this.scope.name = "";\r
+\r
+        //Pagin\r
+        $scope.current_page = 1;\r
+        this.scope.items_per_page = 10;\r
+        $scope.from = 0; // JORDAN\r
+\r
+        $scope.instance = null;\r
+        $scope.resources = new Array();\r
+        $scope.slots = SchedulerSlotsViewData;\r
+        $scope.granularity = DEFAULT_GRANULARITY; /* Will be setup */\r
+        //$scope.msg = "hello";\r
+\r
+        angular.element(document).ready(function() {\r
+            //console.log('Hello World');\r
+            //alert('Hello World');\r
+            //afterAngularRendered();\r
+        });\r
+\r
+        // Pagination\r
+\r
+        $scope.range = function() {\r
+            var range_size = $scope.page_count() > DEFAULT_PAGE_RANGE ? DEFAULT_PAGE_RANGE : $scope.page_count();\r
+            var ret = [];\r
+            var start;\r
+\r
+            start = $scope.current_page;\r
+            if ( start > $scope.page_count()-range_size ) {\r
+              start = $scope.page_count()-range_size+1;\r
+            }\r
+\r
+            for (var i=start; i<start+range_size; i++) {\r
+              ret.push(i);\r
+            }\r
+            return ret;\r
+        };\r
+\r
+        $scope.prevPage = function() {\r
+          if ($scope.current_page > 1) {\r
+            $scope.current_page--;\r
+          }\r
+        };\r
+\r
+        $scope.prevPageDisabled = function() {\r
+          return $scope.current_page === 1 ? "disabled" : "";\r
+        };\r
+  \r
+        $scope.page_count = function()\r
+        {\r
+            // XXX need visible resources only\r
+            var query_ext, visible_resources_length;\r
+            if (!$scope.instance)\r
+                return 0;\r
+            query_ext = manifold.query_store.find_analyzed_query_ext($scope.instance.options.query_uuid);\r
+            var visible_resources_length = 0;\r
+            query_ext.state.each(function(i, state) {\r
+                if (state[STATE_VISIBLE])\r
+                    visible_resources_length++;\r
+            });\r
+            return Math.ceil(visible_resources_length/$scope.items_per_page);\r
+        };\r
+  \r
+        $scope.nextPage = function() {\r
+          if ($scope.current_page < $scope.page_count()) {\r
+            $scope.current_page++;\r
+          }\r
+        };\r
+  \r
+        $scope.nextPageDisabled = function() {\r
+          return $scope.current_page === $scope.page_count() ? "disabled" : "";\r
+        }; \r
+\r
+        $scope.setPage = function(n) {\r
+            $scope.current_page = n;\r
+        };\r
+        // END pagination\r
+\r
+        // FILTER\r
+\r
+        $scope.filter_visible = function(resource)\r
+        {\r
+            return manifold.query_store.get_record_state($scope.instance.options.query_uuid, resource['urn'], STATE_VISIBLE);\r
+        };\r
+\r
+        // SELECTION\r
+\r
+        $scope._create_new_lease = function(resource_urn, start_time, end_time)\r
+        {\r
+            var lease_key, new_lease;\r
+\r
+            lease_key = manifold.metadata.get_key('lease');\r
+\r
+            new_lease = {\r
+                resource:   resource_urn,\r
+                start_time: start_time,\r
+                end_time:   end_time,\r
+            };\r
+\r
+            // This is needed to create a hashable object\r
+            new_lease.hashCode = manifold.record_hashcode(lease_key.sort());\r
+            new_lease.equals   = manifold.record_equals(lease_key);\r
+\r
+            manifold.raise_event($scope.instance.options.query_lease_uuid, SET_ADD, new_lease);\r
+            /* Add to local cache also, unless we listen to events from outside */\r
+            if (!(resource_urn in $scope._leases_by_resource))\r
+                $scope._leases_by_resource[resource_urn] = [];\r
+            $scope._leases_by_resource[resource_urn].push(new_lease);\r
+        }\r
+\r
+        $scope._remove_lease = function(other)\r
+        {\r
+            var lease_key, other_key;\r
+\r
+            lease_key = manifold.metadata.get_key('lease');\r
+\r
+            // XXX This could be a manifold.record_get_value\r
+            other_key = {\r
+                resource:   other.resource,\r
+                start_time: other.start_time,\r
+                end_time:   other.end_time\r
+            }\r
+            other_key.hashCode = manifold.record_hashcode(lease_key.sort());\r
+            other_key.equals   = manifold.record_equals(lease_key);\r
+\r
+            manifold.raise_event($scope.instance.options.query_lease_uuid, SET_REMOVED, other_key);\r
+            /* Remove from local cache also, unless we listen to events from outside */\r
+            $.grep($scope._leases_by_resource[other.resource], function(x) { return x != other; });\r
+\r
+        }\r
+\r
+        $scope.select = function(index, model_lease, model_resource)\r
+        {\r
+            console.log("Selected", index, model_lease, model_resource);\r
+\r
+            var day_timestamp = SchedulerDateSelected.getTime() / 1000;\r
+            var start_time = day_timestamp + index       * model_resource.granularity;\r
+            var end_time   = day_timestamp + (index + 1) * model_resource.granularity;\r
+            var start_date = new Date(start_time * 1000);\r
+            var end_date   = new Date(end_time   * 1000);\r
+\r
+            var lease_key = manifold.metadata.get_key('lease');\r
+\r
+            // We search for leases in the cache we previously constructed\r
+            var resource_leases = $scope._leases_by_resource[model_resource.urn];\r
+\r
+            switch (model_lease.status)\r
+            {\r
+                case 'free': // out\r
+                case 'pendingout':\r
+                    if (resource_leases) {\r
+                        /* Search for leases before */\r
+                        $.each(resource_leases, function(i, other) {\r
+                            if (other.end_time != start_time)\r
+                                return true; // ~ continue\r
+        \r
+                            /* The lease 'other' is just before, and there should not exist\r
+                             * any other lease before it */\r
+                            start_time = other.start_time;\r
+        \r
+                            other_key = {\r
+                                resource:   other.resource,\r
+                                start_time: other.start_time,\r
+                                end_time:   other.end_time\r
+                            }\r
+                            // This is needed to create a hashable object\r
+                            other_key.hashCode = manifold.record_hashcode(lease_key.sort());\r
+                            other_key.equals   = manifold.record_equals(lease_key);\r
+        \r
+                            manifold.raise_event($scope.instance.options.query_lease_uuid, SET_REMOVED, other_key);\r
+                            /* Remove from local cache also, unless we listen to events from outside */\r
+                            $.grep($scope._leases_by_resource[model_resource.urn], function(x) { return x != other; });\r
+                            return false; // ~ break\r
+                        });\r
+        \r
+                        /* Search for leases after */\r
+                        $.each(resource_leases, function(i, other) {\r
+                            if (other.start_time != end_time)\r
+                                return true; // ~ continue\r
+        \r
+                            /* The lease 'other' is just after, and there should not exist\r
+                             * any other lease after it */\r
+                            end_time = other.end_time;\r
+                            // XXX SET_ADD and SET_REMOVE should accept full objects\r
+                            other_key = {\r
+                                resource:   other.resource,\r
+                                start_time: other.start_time,\r
+                                end_time:   other.end_time\r
+                            }\r
+                            // This is needed to create a hashable object\r
+                            other_key.hashCode = manifold.record_hashcode(lease_key.sort());\r
+                            other_key.equals   = manifold.record_equals(lease_key);\r
+        \r
+                            manifold.raise_event($scope.instance.options.query_lease_uuid, SET_REMOVED, other_key);\r
+                            /* Remove from local cache also, unless we listen to events from outside */\r
+                            $.grep($scope._leases_by_resource[model_resource.urn], function(x) { return x != other; });\r
+                            return false; // ~ break\r
+                        });\r
+                    }\r
+        \r
+                    $scope._create_new_lease(model_resource.urn, start_time, end_time);\r
+                    model_lease.status = 'pendingin'; \r
+                    // unless the exact same lease already existed (pending_out status for the lease, not the cell !!)\r
+\r
+                    break;\r
+\r
+                case 'selected':\r
+                case 'pendingin':\r
+                    // We remove the cell\r
+\r
+                    /* We search for leases including this cell. Either 0, 1 or 2.\r
+                     * 0 : NOT POSSIBLE, should be checked.\r
+                     * 1 : either IN or OUT, we have make no change in the session\r
+                     * 2 : both will be pending, since we have made a change in the session\r
+                    * /!\ need to properly remove pending_in leases when removed again\r
+                     */\r
+                    if (resource_leases) {\r
+                        $.each(resource_leases, function(i, other) {\r
+                            if ((other.start_time <= start_time) && (other.end_time >= end_time)) {\r
+                                // The cell is part of this lease.\r
+\r
+                                // If the cell is not at the beginning of the lease, we recreate a lease with cells before\r
+                                if (start_time > other.start_time) {\r
+                                    $scope._create_new_lease(model_resource.urn, other.start_time, start_time);\r
+                                }\r
+\r
+                                // If the cell is not at the end of the lease, we recreate a lease with cells after\r
+                                if (end_time < other.end_time) {\r
+                                    $scope._create_new_lease(model_resource.urn, end_time, other.end_time);\r
+                                }\r
+                                \r
+                                // The other lease will be removed\r
+                                $scope._remove_lease(other);\r
+                            }\r
+                            // NOTE: We can interrupt the search if we know that there is a single lease (depending on the status).\r
+                        });\r
+                    }\r
+                \r
+                    // cf comment in previous switch case\r
+                    model_lease.status = 'pendingout'; \r
+\r
+                    break;\r
+\r
+                case 'reserved':\r
+                case 'maintainance':\r
+                    // Do nothing\r
+                    break;\r
+            }\r
+            \r
+\r
+            //$scope._dump_leases();\r
+        };\r
+  \r
+        $scope._dump_leases = function()\r
+        {\r
+            // DEBUG: display all leases and their status in the log\r
+            var leases = manifold.query_store.get_records($scope.instance.options.query_lease_uuid);\r
+            console.log("--------------------");\r
+            $.each(leases, function(i, lease) {\r
+                var key = manifold.metadata.get_key('lease');\r
+                var lease_key = manifold.record_get_value(lease, key);\r
+                var state = manifold.query_store.get_record_state($scope.instance.options.query_lease_uuid, lease_key, STATE_SET);\r
+                var state_str;\r
+                switch(state) {\r
+                    case STATE_SET_IN:\r
+                        state_str = 'STATE_SET_IN';\r
+                        break;\r
+                    case STATE_SET_OUT:\r
+                        state_str = 'STATE_SET_OUT';\r
+                        break;\r
+                    case STATE_SET_IN_PENDING:\r
+                        state_str = 'STATE_SET_IN_PENDING';\r
+                        break;\r
+                    case STATE_SET_OUT_PENDING:\r
+                        state_str = 'STATE_SET_OUT_PENDING';\r
+                        break;\r
+                    case STATE_SET_IN_SUCCESS:\r
+                        state_str = 'STATE_SET_IN_SUCCESS';\r
+                        break;\r
+                    case STATE_SET_OUT_SUCCESS:\r
+                        state_str = 'STATE_SET_OUT_SUCCESS';\r
+                        break;\r
+                    case STATE_SET_IN_FAILURE:\r
+                        state_str = 'STATE_SET_IN_FAILURE';\r
+                        break;\r
+                    case STATE_SET_OUT_FAILURE:\r
+                        state_str = 'STATE_SET_OUT_FAILURE';\r
+                        break;\r
+                }\r
+                console.log("LEASE", new Date(lease.start_time * 1000), new Date(lease.end_time * 1000), lease.resource, state_str);\r
+            });\r
+        };\r
+\r
+        // Return this object reference.\r
+        return (this);\r
+\r
+    }\r
+\r
+    // Define the Controller as the constructor function.\r
+    app.controller("SchedulerCtrl", Controller);\r
+\r
+})(angular, ManifoldApp);\r
+\r
+/******************************************************************************\r
+ *                              MANIFOLD PLUGIN                               *\r
+ ******************************************************************************/\r
+\r
 (function($) {\r
         scheduler2 = Plugin.extend({\r
 \r
@@ -80,11 +405,11 @@ var SCHEDULER_COLWIDTH = 50;
          *     applied, which allows to maintain chainability of calls\r
          */\r
             init: function(options, element) {\r
-                this.classname = "scheduler2";\r
                 // Call the parent constructor, see FAQ when forgotten\r
                 this._super(options, element);\r
 \r
                 var scope = this._get_scope()\r
+                scope.instance = this;\r
 \r
                 // XXX not needed\r
                 scheduler2Instance = this;\r
@@ -120,15 +445,35 @@ var SCHEDULER_COLWIDTH = 50;
                 this.listen_query(options.query_uuid, 'resources');\r
                 this.listen_query(options.query_lease_uuid, 'leases');\r
 \r
+                this.elmt().on('show', this, this.on_show);\r
+                this.elmt().on('shown.bs.tab', this, this.on_show);\r
+                this.elmt().on('resize', this, this.on_resize);\r
+\r
                 /* Generate slots according to the default granularity. Should\r
                  * be updated when resources arrive.  Should be the pgcd in fact XXX */\r
                 this._granularity = DEFAULT_GRANULARITY;\r
                 scope.granularity = this._granularity;\r
                 this._all_slots = this._generate_all_slots();\r
 \r
+                // A list of {id, time} dictionaries representing the slots for the given day\r
+                scope.slots = this._all_slots;\r
+                this.scope_resources_by_key = {};\r
+\r
+                this.do_resize();\r
+    \r
+                scope.from = 0;\r
+\r
+                this._initUI();\r
+\r
+            },\r
+\r
+            do_resize: function()\r
+            {\r
+                var scope = this._get_scope();\r
+\r
                 $('#' + schedulerTblId + ' thead tr th:eq(0)').css("width", SCHEDULER_FIRST_COLWIDTH);\r
-                //this get width might need fix depending on the template \r
-                var tblwidth = $('#scheduler-tab').parent().outerWidth();\r
+                //self get width might need fix depending on the template \r
+                var tblwidth = $('#scheduler-reservation-table').parent().outerWidth();\r
 \r
                 /* Number of visible cells...*/\r
                 this._num_visible_cells = parseInt((tblwidth - SCHEDULER_FIRST_COLWIDTH) / SCHEDULER_COLWIDTH);\r
@@ -140,15 +485,27 @@ var SCHEDULER_COLWIDTH = 50;
                 scope.num_visible_cells = this._num_visible_cells;\r
                 scope.lcm_colspan = this._lcm_colspan;\r
 \r
-                scope.options = this.options;\r
-                scope.from = 0;\r
+                // Slider max value\r
 \r
-                // A list of {id, time} dictionaries representing the slots for the given day\r
-                scope.slots = this._all_slots;\r
-                this.scope_resources_by_key = {};\r
+                if ($('#tblSlider').data('slider') != undefined) {\r
+                    var new_max = (this._all_slots.length - this._num_visible_cells) / this._lcm_colspan;\r
+                    $('#tblSlider').slider('setAttribute', 'max', new_max);\r
+                }\r
 \r
-                this._initUI();\r
+            },\r
 \r
+            on_show: function(e)\r
+            {\r
+                var self = e.data;\r
+                self.do_resize();\r
+                self._get_scope().$apply();\r
+            },\r
+\r
+            on_resize: function(e)\r
+            {\r
+                var self = e.data;\r
+                self.do_resize();\r
+                self._get_scope().$apply();\r
             },\r
 \r
             /* Handlers */\r
@@ -299,39 +656,32 @@ var SCHEDULER_COLWIDTH = 50;
                 var self = this;\r
 \r
                 $("#DateToRes").datepicker({\r
-                    dateFormat: "yy-mm-dd",\r
-                    minDate: 0,\r
-                    numberOfMonths: 3\r
-                }).change(function() {\r
-                    // the selected date\r
-                    SchedulerDateSelected = $("#DateToRes").datepicker("getDate");\r
-                    if (SchedulerDateSelected == null || SchedulerDateSelected == '') {\r
-                        alert("Please select a date, so the scheduler can reserve leases.");\r
-                        return;\r
+                    onRender: function(date) {\r
+                        return date.valueOf() < now.valueOf() ? 'disabled' : '';\r
                     }\r
+                }).on('changeDate', function(ev) {\r
+                    SchedulerDateSelected = new Date(ev.date);\r
+                    SchedulerDateSelected.setHours(0,0,0,0);\r
                     // Set slider to origin\r
-                    $('#tblSlider').slider('value', 0);\r
+                    $('#tblSlider').slider('setValue', 0); // XXX\r
                     // Refresh leases\r
                     self._scope_clear_leases();\r
                     self._scope_set_leases();\r
                     // Refresh display\r
                     self._get_scope().$apply();\r
-                }).datepicker('setDate', SchedulerDateSelected);\r
+                }).datepicker('setValue', SchedulerDateSelected); //.data('datepicker');\r
 \r
                 //init Slider\r
                 $('#tblSlider').slider({\r
                     min: 0,\r
-                    max: (this._all_slots.length - self._num_visible_cells) / self._lcm_colspan,\r
+                    max: (self._all_slots.length - self._num_visible_cells) / self._lcm_colspan,\r
                     value: 0,\r
-                    slide: function(event, ui) {\r
-                        var scope = self._get_scope();\r
-                        scope.from = ui.value * self._lcm_colspan;\r
-                        scope.$apply();\r
-                   }\r
+                }).on('slide', function(ev) {\r
+                    var scope = self._get_scope();\r
+                    scope.from = ev.value * self._lcm_colspan;\r
+                    scope.$apply();\r
                 });\r
 \r
-                $('#btnSchedulerSubmit').click(this._on_submit);\r
-\r
                 $("#plugin-scheduler-loader").hide();\r
                 $("#plugin-scheduler").show();\r
             },\r
diff --git a/plugins/scheduler2/static/js/selectRangeWorker.js b/plugins/scheduler2/static/js/selectRangeWorker.js
deleted file mode 100755 (executable)
index 5f28270..0000000
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
index 95c6ba7..81efd74 100755 (executable)
@@ -6,55 +6,27 @@
     <img src="../../static/img/no-data.png" alt="no data found" style="width:100px;" />\r
     <h3>no data found...</h3>\r
 </div>\r
-<div id="plugin-{{ domid }}" class="" ng-app="myApp" style="display:none;">\r
+<div id="plugin-{{ domid }}" class="">\r
     <div class="row m-b">\r
         <div class="col-md-1">\r
             <label for="inputEmail3" class="col-sm-2 control-label">Date</label>\r
         </div>\r
         <div class="col-md-9">\r
-            <input id="DateToRes" type="text" class="form-control" placeholder="Reservation Date">\r
+            <input id="DateToRes" type="text" placeholder="Reservation Date">\r
+            <!-- <input id="DateToRes" type="text" class="form-control" placeholder="Reservation Date"> -->\r
             <span class="glyphicon glyphicon-calendar"></span>\r
-        </div>\r
-        <div class="col-md-2 text-center">\r
-            {% comment %}\r
-            <div id="TopologyModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">\r
-                <div class="modal-dialog">\r
-                    <div class="modal-header">\r
-                        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>\r
-                        <h4 class="modal-title" id="myModalLabel">Topology</h4>\r
-                    </div>\r
-                    <div class="modal-body">\r
-                        <img src="../../static/img/nitos_topo.png" alt="nitos topology" style="width:100%;" />\r
-                    </div>\r
-                </div><!-- /.modal-dialog -->\r
-            </div><!-- /.modal TopologyModal -->\r
-            <button type="button" class="btn btn-primary btn-md" data-toggle="modal" data-target="#TopologyModal">Topology</button>\r
-            {% endcomment  %}\r
-            <button id="btnSchedulerSubmit" type="button" class="btn btn-primary btn-md">Submit Leases</button>\r
-\r
+                       <div class="sliderContainer">\r
+                               <div id="tblSlider"></div>\r
+                       </div>\r
         </div>\r
     </div>\r
-    <!--<div class="row m-b">\r
-        <div class="col-md-1">\r
-            <label for="inputEmail3" class="col-sm-1 control-label">Time</label>\r
-        </div>\r
-        <div class="col-md-9">\r
-            <div id="time-range"></div>\r
-        </div>\r
-        <div class="col-md-2">\r
-            <span id="lbltime" class="label label-primary"></span>\r
-        </div>\r
-    </div>-->\r
     <div id="SchedulerCtrl" ng-controller="SchedulerCtrl" class='query-editor-spacer'>\r
-        <div class="sliderContainer">\r
-            <div id="tblSlider"></div>\r
-        </div>\r
         <div class="table-responsive">\r
 \r
             <table id="scheduler-reservation-table" class="table table-bordered table-condensed">\r
                 <thead>\r
                     <tr>\r
-                        <th>#</th>\r
+                        <th>Resource name</th>\r
                         <th ng-repeat="slot in slots | offset: from | limitTo: num_visible_cells ">\r
                             {[{ slot.time }]}\r
                         </th>\r
index a7dd15e..c72f667 100644 (file)
@@ -2,13 +2,13 @@ from unfold.plugin import Plugin
 
 class TestbedsPlugin(Plugin):
     
-    def __init__ (self, query=None, query_all=None, query_network=None, **settings):
+    def __init__ (self, query=None, query_networks=None, **settings):
         Plugin.__init__ (self, **settings)
 
         # Until we have a proper way to access queries in Python
         self.query              = query
-        self.query_all          = query_all
-        self.query_all_uuid     = query_all.query_uuid if query_all else None
+        self.query_networks          = query_networks
+        self.query_networks_uuid     = query_networks.query_uuid if query_networks else None
 
     def template_file (self):
         return "testbeds.html"
@@ -28,7 +28,7 @@ class TestbedsPlugin(Plugin):
         # query_uuid will pass self.query results to the javascript
         # and will be available as "record" in :
         # on_new_record: function(record)
-        return ['plugin_uuid', 'domid', 'query_uuid', 'query_all_uuid']
+        return ['plugin_uuid', 'domid', 'query_uuid', 'query_networks_uuid']
 
     def export_json_settings (self):
         return True
index 5f990b9..623bc91 100644 (file)
  * License:     GPLv3
  */
 
-(function($){
+// XXX Inherit from an AngularPlugin class ?
+(function (ng, app) {
+
+    // Define our Controller constructor.
+    function Controller($scope) {
+        /* Contructor */
+
+        /* Plugin instance */
+        $scope.instance = null;
+
+        /* Models */
+        $scope.testbeds = Array();
 
+        /* Click event */
+        $scope.select = function(testbed)
+        {
+            var selected, prev_selected, num, num_selected, num_prev_selected, filter;
+
+            prev_selected = $.map($scope.testbeds, function(x, i) {
+                return x.active ? x.network_hrn : null;
+            });
+
+            testbed.active = !testbed.active;
+
+            selected = $.map($scope.testbeds, function(x, i) {
+                return x.active ? x.network_hrn : null;
+            });
+
+            num = $scope.testbeds.length;
+            prev_num_selected = prev_selected.length;
+            num_selected = selected.length;
+
+            
+            if ((prev_num_selected != 0) && (prev_num_selected != num)) {
+                // Remove previous filter
+                filter = ['network_hrn', 'included', prev_selected];
+                manifold.raise_event($scope.instance.options.query_uuid, FILTER_REMOVED, filter);
+            }
+
+            if ((num_selected != 0) && (num_selected != num)) {
+                filter = ['network_hrn', 'included', selected];
+                manifold.raise_event($scope.instance.options.query_uuid, FILTER_ADDED, filter);
+            }
+        };
+
+        /* Return object reference */
+        return (this);
+    }
+
+    // Define the Controller as the constructor function.
+    app.controller("TestbedsCtrl", Controller);
+
+})(angular, ManifoldApp);
+
+(function($){
     var TestbedsPlugin = Plugin.extend({
 
         /** XXX to check
          * @return : a jQuery collection of objects on which the plugin is
          *     applied, which allows to maintain chainability of calls
          */
-        init: function(options, element) {
-               // for debugging tools
-               this.classname="testbedsplugin";
+        init: function(options, element) 
+        {
             // Call the parent constructor, see FAQ when forgotten
             this._super(options, element);
 
             /* Member variables */
             this.filters = Array();
             this.testbeds = Array();
-            /* Plugin events */
 
-            /* Setup query and record handlers */
+            this._get_scope().instance = this;
 
-            // Explain this will allow query events to be handled
-            // What happens when we don't define some events ?
-            // Some can be less efficient
+            /* Handlers */
             this.listen_query(options.query_uuid);
-            this.listen_query(options.query_all_uuid, 'all');
-
-            /* GUI setup and event binding */
-            // call function
-
+            this.listen_query(options.query_networks_uuid, 'networks');
         },
 
-        /* PLUGIN EVENTS */
-        // on_show like in querytable
-
 
-        /* GUI EVENTS */
-
-        // a function to bind events here: click change
-        // how to raise manifold events
-
-
-        /* GUI MANIPULATION */
-
-        // We advise you to write function to change behaviour of the GUI
-        // Will use naming helpers to access content _inside_ the plugin
-        // always refer to these functions in the remaining of the code
-
-        show_hide_button: function() 
-        {
-            // this.id, this.el, this.cl, this.elts
-            // same output as a jquery selector with some guarantees
-        },
-
-        /* TEMPLATES */
-
-        // see in the html template
-        // How to load a template, use of mustache
-
-        /* QUERY HANDLERS */
-
-        // How to make sure the plugin is not desynchronized
-        // He should manifest its interest in filters, fields or records
-        // functions triggered only if the proper listen is done
-
-        // no prefix
+        /* HANDLERS */
 
         /* When a filter is added/removed, update the list of filters local to the plugin */
         on_filter_added: function(filter)
                 }else if(filter[1]=='=' || filter[1]=='=='){
                     $("#testbeds-filter_"+filter[2]).addClass("active");
                 }
+                // XXX NAMING
+                // XXX How to display unsupported filters
+                // XXX Constants for operators
             }
         },
         on_filter_removed: function(filter)
 
         // ... be sure to list all events here
 
-        /* RECORD HANDLERS */
-        on_all_new_record: function(record)
+        on_networks_query_done: function()
         {
-            var self = this;
-            // If the resource has a network_hrn
-            if(record["network_hrn"]!="None" && record["network_hrn"]!="" && record["network_hrn"]!=null){
-                // If this network_hrn is not listed yet
-                if(jQuery.inArray(record["network_hrn"],self.testbeds)==-1){
-                    row  = '<a href="#" class="list-group-item sl-platform" id="testbeds-filter_'+record["network_hrn"]+'" data-platform="'+record["network_hrn"]+'">';
-                    //row += '<span class="list-group-item-heading">'+record["platform"]+'</span>';
-                    //row += '<span class="list-group-item-heading">'+record["network_hrn"]+'</span></a>';
-                    row += '<p class="list-group-item-heading">'+record["network_hrn"]+'</p></a>';
-                    $('#testbeds-filter').append(row);
-                    self.testbeds.push(record["network_hrn"]);
-                }
-            }
+             this.set_networks();
         },
 
-        /* When the query is done, add the click event to the elements  */
-        on_all_query_done: function() {
+/*
             var self = this;
             console.log('query network DONE');
             $("[id^='testbeds-filter_']").on('click',function(e) {
                 return $(this).hasClass('active') ? self._addFilter(key, op, value) : self._removeFilter(key, op, value);
             });
            
-        },
+        },*/
 
         /* INTERNAL FUNCTIONS */
-        _dummy: function() {
-            // only convention, not strictly enforced at the moment
+
+        set_networks: function()
+        {
+            var scope = this._get_scope();
+            var query_ext = manifold.query_store.find_analyzed_query_ext(this.options.query_networks_uuid);
+            scope.testbeds = query_ext.records.values();
+            $.each(scope.testbeds, function(i, testbed) { testbed.active = true });
+            scope.$apply();
         },
+
+        _get_scope : function()
+        {
+            return angular.element('[ng-controller=TestbedsCtrl]').scope()
+        },
+
         _addFilter: function(key, op, value)
         {
-            console.log("add "+value);
-            var self = this;
             values = Array();
             // get the previous list of values for this key, ex: [ple,nitos]
             // remove the previous filter
             if(network_filter.length > 0){
                 $.each(network_filter, function(i,f){
                     values = f[2];
-                    manifold.raise_event(self.options.query_uuid, FILTER_REMOVED, [key, op, values]);
                 });
             }
             // Add the new value to list of values, ex: wilab
index fe8c2ea..289a556 100644 (file)
@@ -1,3 +1,16 @@
-<div id={{ domid }}>
+<div id={{ domid }} ng-controller="TestbedsCtrl">
+
 <div class="list-group-item sl-platform"><span class="list-group-item-heading">Testbeds</span></div>
+
+<div ng-repeat="testbed in testbeds"
+     ng-click="select(testbed)">
+       <a href="#" 
+          class="list-group-item sl-platform"
+       ng-class="{active: testbed.active}"
+          id="testbeds-filter_{[{ testbed.network_hrn }]}"
+          data-platform="{[{ testbed.network_hrn }]}">
+       <span class="list-group-item-heading">{[{ testbed.platform }]}</span>
+       <p class="list-group-item-heading">{[{ testbed.network_hrn }]}</p></a>
+</div>
+
 </div>
index 498bdd2..40c49f1 100644 (file)
@@ -53,7 +53,7 @@ class SliceResourceView (LoginRequiredView, ThemeView):
         main_query.select(
                 'slice_urn', # XXX We need the key otherwise the storage of records bugs !
                 'slice_hrn',
-                'resource.urn', 
+                'resource.urn',
                 'resource.hostname', 'resource.type',
                 'resource.network_hrn',
                 'lease.resource',
@@ -180,8 +180,6 @@ class SliceResourceView (LoginRequiredView, ThemeView):
             # this is the query at the core of the slice list
             query = sq_resource,
             query_lease = sq_lease,
-            query_all_resources = query_resource_all,
-            query_all_leases = query_lease_all,
         )
 
         # --------------------------------------------------------------------------
@@ -206,15 +204,15 @@ class SliceResourceView (LoginRequiredView, ThemeView):
         network_md = metadata.details_by_object('network')
         network_fields = [column['name'] for column in network_md['column']]
 
-        #query_network = Query.get('network').select(network_fields)
-        #page.enqueue_query(query_network)
+        query_networks = Query.get('network').select(network_fields)
+        page.enqueue_query(query_networks)
 
         filter_testbeds = TestbedsPlugin(
             page            = page,
             domid           = 'testbeds-filter',
             title           = 'Filter by testbeds',
             query           = sq_resource,
-            #query_network  = query_network,
+            query_networks  = query_networks,
             init_key        = "network_hrn",
             checkboxes      = True,
             datatables_options = {
@@ -265,7 +263,7 @@ class SliceResourceView (LoginRequiredView, ThemeView):
         template_env['map_resources'] = map_resources.render(self.request)
         template_env['scheduler'] = resources_as_scheduler2.render(self.request)
 #        template_env['pending_resources'] = pending_resources.render(self.request)
-        template_env['sla_dialog'] = sla_dialog.render(self.request)
+        template_env['sla_dialog'] = '' # sla_dialog.render(self.request)
         template_env["theme"] = self.theme
         template_env["username"] = request.user
         template_env["slice"] = slicename
index 564e34a..de2ac74 100644 (file)
@@ -43,10 +43,12 @@ $(document).ready(function() {
                                        var el = $('*[data-key="'+myslice.pending[i]+'"]');
                                        el.addClass("active");
                                        el.find('input[type=checkbox]').prop('checked', true);
+                    /*
                                        if (myslice.count() > 0) {
                                                $('#badge-pending').text(myslice.count());
                                                $('#badge-pending').show();
                                        }
+                    */
                                }
                    }
                } );
@@ -60,18 +62,20 @@ $(document).ready(function() {
                                row.removeClass("active");
                                myslice.del(id);
                                cnt = myslice.count();
+                /*
                                $('#badge-pending').text(cnt);
                                if (cnt <= 0) {
                                        $('#badge-pending').hide();
-                               }
+                               }*/
                        } else {
                                row.addClass("active");
                                myslice.add(id);
+                /*
                                cnt = myslice.count();
                                $('#badge-pending').text(cnt);
                                if (cnt > 0) {
                                        $('#badge-pending').show();
-                               }
+                               }*/
                        }
                });
        });
index d5ab2da..66eb509 100644 (file)
@@ -17,6 +17,7 @@
 {% block head %} {% endblock head %}
 {# let's add these ones no matter what #}
 {% insert_str prelude "js/jquery.min.js" %}
+{% insert_str prelude "js/angular/angular.min.js" %}
 {% insert_str prelude "js/jquery.html5storage.min.js" %}
 {% insert_str prelude "js/messages-runtime.js" %}
 {% insert_str prelude "js/class.js" %}
 {% insert_str prelude "css/plugin.css" %}
 {% insert_str prelude "js/bootstrap.js" %}
 {% insert_str prelude "css/bootstrap.css" %}
+{% insert_str prelude "js/bootstrap-datepicker.js" %}
+{% insert_str prelude "css/datepicker.css" %}
+{% insert_str prelude "js/bootstrap-slider.js" %}
+{% insert_str prelude "css/slider.css" %}
 {% insert_str prelude "css/topmenu.css" %}
 {% insert_str prelude "js/logout.js" %}
 <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/{{ theme }}.css">
@@ -70,7 +75,7 @@ $(document).ready(function() {
 });
 </script>
 </head>
-<body>
+<body ng-app="ManifoldApp">
 {% block container %}
        {% block topmenu %}
        {% widget "_widget-topmenu.html" %}
index 158f53b..7eaaf05 100644 (file)
@@ -15,6 +15,7 @@
 {% block head %} {% endblock head %}
 {# let's add these ones no matter what #}
 {% insert_str prelude "js/jquery.min.js" %}
+{% insert_str prelude "js/angular/angular.min.js" %}
 {% insert_str prelude "js/jquery.html5storage.min.js" %}
 {% insert_str prelude "js/messages-runtime.js" %}
 {% insert_str prelude "js/class.js" %}
 {% insert_str prelude "css/plugin.css" %}
 {% insert_str prelude "js/bootstrap.js" %}
 {% insert_str prelude "css/bootstrap.css" %}
+{% insert_str prelude "js/bootstrap-datepicker.js" %}
+{% insert_str prelude "css/datepicker.css" %}
+{% insert_str prelude "js/bootstrap-slider.js" %}
+{% insert_str prelude "css/slider.css" %}
 {% insert_str prelude "css/topmenu.css" %}
 {% insert_str prelude "js/logout.js" %}
 <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/{{ theme }}.css">
 </head>
-<body>
+<body ng-app="ManifoldApp">
 {% block container %}
        {% block topmenu %}
        {% include theme|add:"__widget-topmenu.html" %}
index dcf6ee6..fdab352 100644 (file)
@@ -15,6 +15,7 @@
 {% block head %} {% endblock head %}
 {# let's add these ones no matter what #}
 {% insert_str prelude "js/jquery.min.js" %}
+{% insert_str prelude "js/angular/angular.min.js" %}
 {% insert_str prelude "js/jquery.html5storage.min.js" %}
 {% insert_str prelude "js/messages-runtime.js" %}
 {% insert_str prelude "js/class.js" %}
 {% insert_str prelude "css/plugin.css" %}
 {% insert_str prelude "js/bootstrap.js" %}
 {% insert_str prelude "css/bootstrap.css" %}
+{% insert_str prelude "js/bootstrap-datepicker.js" %}
+{% insert_str prelude "css/datepicker.css" %}
+{% insert_str prelude "js/bootstrap-slider.js" %}
+{% insert_str prelude "css/slider.css" %}
 {% insert_str prelude "css/topmenu.css" %}
 {% insert_str prelude "js/logout.js" %}
 <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/{{ theme }}.css">
 </head>
-<body>
+<body ng-app="ManifoldApp">
 {% block container %}
        {% block topmenu %}
        {% include theme|add:"__widget-topmenu.html" %}
index 12af9bf..4ecf483 100644 (file)
@@ -6,6 +6,14 @@
 <script src="{{ STATIC_URL }}js/onelab_slice-resource-view.js"></script>
 <script>
        //myslice.slice = "{{ slice }}";
+
+$(document).ready(function() {
+            $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+        // find the plugin object inside the tab content referenced by the current tabs
+        $('.plugin', $($(e.target).attr('href'))).trigger('shown.bs.tab');
+        $('.plugin', $($(e.target).attr('href'))).trigger('show');
+            });
+});
 </script>
 {% endblock %}
 
                -->
 
                <div class="row">
-                       <ul class="nav nav-pills nav-resources">
-                         <li class="active"><a data-panel="resources" href="#">Table</a></li>
-                         <li id="GoogleMap"><a data-panel="map" href="#">Map</a></li>
-                         <li id="Scheduler"><a data-panel="scheduler-tab" href="#">Scheduler</a></li>
+                       <ul class="nav nav-tabs">
+                         <li class="active"><a href="#resourcelist" role="tab" data-toggle="tab">Table</a></li>
+                         <li> <a href="#resourcemap" role="tab" data-toggle="tab">Map</a></li>
+                         <li> <a href="#resourcescheduler" role="tab" data-toggle="tab">Scheduler</a></li>
                        </ul>
                </div>
 
                        </div>
                </div>
                
-               <div class="row" style="height:100%;">
-                       <div id="resources" class="panel">
+               <div class="tab-content" style="height:100%;">
+                       <div class="tab-pane active" id="resourcelist">
                                 <!-- Button trigger modal - columns selector -->
                                <button class="btn btn-primary btn-sm" style="float:right;" data-toggle="modal" data-target="#myModal">...</button>
                 {{list_resources}}
                                <!-- <table cellpadding="0" cellspacing="0" border="0" class="table" id="objectList"></table> -->
                        </div>
-                       <div id="reserved" class="panel" style="height:370px;display:none;">
+                       <div class="tab-pane" id="resourcemap">
+                {{map_resources}}
+                       </div>
+                       <div class="tab-pane" id="resourcescheduler">
+                {{scheduler}}
+                       </div>
+
+                       <!--
+                       <div id="reserved" class="tab-pane" style="height:370px;display:none;">
                 <table width="80%">
                     <tr><th width="50%" style="text-align:center;">resources</th><th width="50%" style="text-align:center;">leases</th></tr>
                     <tr>
                     </tr>
                 </table>
                        </div>
-                       <div id="pending" class="panel" style="height:370px;display:none;">
+                       <div id="pending" class="tab-pane" style="height:370px;display:none;">
                 {{pending_resources}}
                        </div>
-                       <div id="sla_dialog" class="panel" style="height:370px;display:none;">
+                       <div id="sla_dialog" class="tab-pane" style="height:370px;display:none;">
                 {{sla_dialog}}
                        </div>
-                       <div id="map" class="panel" style="height:370px;display:none;">
-                {{map_resources}}
-                       </div>
-                       <div id="scheduler-tab" class="panel" style="height:370px;display:none;">
-                {{scheduler}}
-                       </div>
+-->
+
                </div>
        </div>
 {% endblock %}
diff --git a/third-party/bootstrap-datepicker b/third-party/bootstrap-datepicker
new file mode 120000 (symlink)
index 0000000..86a0496
--- /dev/null
@@ -0,0 +1 @@
+bootstrap-datepicker-1/
\ No newline at end of file
diff --git a/third-party/bootstrap-datepicker-1/bootstrap-datepicker.js b/third-party/bootstrap-datepicker-1/bootstrap-datepicker.js
new file mode 100644 (file)
index 0000000..4766bba
--- /dev/null
@@ -0,0 +1,962 @@
+/* =========================================================
+ * bootstrap-datepicker.js
+ * http://www.eyecon.ro/bootstrap-datepicker
+ * =========================================================
+ * Copyright 2012 Stefan Petre
+ * Improvements by Andrew Rowls
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+!function( $ ) {
+
+       function UTCDate(){
+               return new Date(Date.UTC.apply(Date, arguments));
+       }
+       function UTCToday(){
+               var today = new Date();
+               return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
+       }
+
+       // Picker object
+
+       var Datepicker = function(element, options) {
+               var that = this;
+
+               this.element = $(element);
+               this.language = options.language||this.element.data('date-language')||"en";
+               this.language = this.language in dates ? this.language : "en";
+               this.isRTL = dates[this.language].rtl||false;
+               this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
+               this.isInline = false;
+               this.isInput = this.element.is('input');
+               this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
+               this.hasInput = this.component && this.element.find('input').length;
+               if(this.component && this.component.length === 0)
+                       this.component = false;
+
+               this._attachEvents();
+
+               this.forceParse = true;
+               if ('forceParse' in options) {
+                       this.forceParse = options.forceParse;
+               } else if ('dateForceParse' in this.element.data()) {
+                       this.forceParse = this.element.data('date-force-parse');
+               }
+                
+
+               this.picker = $(DPGlobal.template)
+                                                       .appendTo(this.isInline ? this.element : 'body')
+                                                       .on({
+                                                               click: $.proxy(this.click, this),
+                                                               mousedown: $.proxy(this.mousedown, this)
+                                                       });
+
+               if(this.isInline) {
+                       this.picker.addClass('datepicker-inline');
+               } else {
+                       this.picker.addClass('datepicker-dropdown dropdown-menu');
+               }
+               if (this.isRTL){
+                       this.picker.addClass('datepicker-rtl');
+                       this.picker.find('.prev i, .next i')
+                                               .toggleClass('icon-arrow-left icon-arrow-right');
+               }
+               $(document).on('mousedown', function (e) {
+                       // Clicked outside the datepicker, hide it
+                       if ($(e.target).closest('.datepicker').length === 0) {
+                               that.hide();
+                       }
+               });
+
+               this.autoclose = false;
+               if ('autoclose' in options) {
+                       this.autoclose = options.autoclose;
+               } else if ('dateAutoclose' in this.element.data()) {
+                       this.autoclose = this.element.data('date-autoclose');
+               }
+
+               this.keyboardNavigation = true;
+               if ('keyboardNavigation' in options) {
+                       this.keyboardNavigation = options.keyboardNavigation;
+               } else if ('dateKeyboardNavigation' in this.element.data()) {
+                       this.keyboardNavigation = this.element.data('date-keyboard-navigation');
+               }
+
+               this.viewMode = this.startViewMode = 0;
+               switch(options.startView || this.element.data('date-start-view')){
+                       case 2:
+                       case 'decade':
+                               this.viewMode = this.startViewMode = 2;
+                               break;
+                       case 1:
+                       case 'year':
+                               this.viewMode = this.startViewMode = 1;
+                               break;
+               }
+
+               this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
+               this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false);
+
+               this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7);
+               this.weekEnd = ((this.weekStart + 6) % 7);
+               this.startDate = -Infinity;
+               this.endDate = Infinity;
+               this.daysOfWeekDisabled = [];
+               this.setStartDate(options.startDate||this.element.data('date-startdate'));
+               this.setEndDate(options.endDate||this.element.data('date-enddate'));
+               this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled'));
+               this.fillDow();
+               this.fillMonths();
+               this.update();
+               this.showMode();
+
+               if(this.isInline) {
+                       this.show();
+               }
+       };
+
+       Datepicker.prototype = {
+               constructor: Datepicker,
+
+               _events: [],
+               _attachEvents: function(){
+                       this._detachEvents();
+                       if (this.isInput) { // single input
+                               this._events = [
+                                       [this.element, {
+                                               focus: $.proxy(this.show, this),
+                                               keyup: $.proxy(this.update, this),
+                                               keydown: $.proxy(this.keydown, this)
+                                       }]
+                               ];
+                       }
+                       else if (this.component && this.hasInput){ // component: input + button
+                               this._events = [
+                                       // For components that are not readonly, allow keyboard nav
+                                       [this.element.find('input'), {
+                                               focus: $.proxy(this.show, this),
+                                               keyup: $.proxy(this.update, this),
+                                               keydown: $.proxy(this.keydown, this)
+                                       }],
+                                       [this.component, {
+                                               click: $.proxy(this.show, this)
+                                       }]
+                               ];
+                       }
+                                               else if (this.element.is('div')) {  // inline datepicker
+                                                       this.isInline = true;
+                                               }
+                       else {
+                               this._events = [
+                                       [this.element, {
+                                               click: $.proxy(this.show, this)
+                                       }]
+                               ];
+                       }
+                       for (var i=0, el, ev; i<this._events.length; i++){
+                               el = this._events[i][0];
+                               ev = this._events[i][1];
+                               el.on(ev);
+                       }
+               },
+               _detachEvents: function(){
+                       for (var i=0, el, ev; i<this._events.length; i++){
+                               el = this._events[i][0];
+                               ev = this._events[i][1];
+                               el.off(ev);
+                       }
+                       this._events = [];
+               },
+
+               show: function(e) {
+                       this.picker.show();
+                       this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
+                       this.update();
+                       this.place();
+                       $(window).on('resize', $.proxy(this.place, this));
+                       if (e ) {
+                               e.stopPropagation();
+                               e.preventDefault();
+                       }
+                       this.element.trigger({
+                               type: 'show',
+                               date: this.date
+                       });
+               },
+
+               hide: function(e){
+                       if(this.isInline) return;
+                       this.picker.hide();
+                       $(window).off('resize', this.place);
+                       this.viewMode = this.startViewMode;
+                       this.showMode();
+                       if (!this.isInput) {
+                               $(document).off('mousedown', this.hide);
+                       }
+
+                       if (
+                               this.forceParse &&
+                               (
+                                       this.isInput && this.element.val() ||
+                                       this.hasInput && this.element.find('input').val()
+                               )
+                       )
+                               this.setValue();
+                       this.element.trigger({
+                               type: 'hide',
+                               date: this.date
+                       });
+               },
+
+               remove: function() {
+                       this._detachEvents();
+                       this.picker.remove();
+                       delete this.element.data().datepicker;
+               },
+
+               getDate: function() {
+                       var d = this.getUTCDate();
+                       return new Date(d.getTime() + (d.getTimezoneOffset()*60000));
+               },
+
+               getUTCDate: function() {
+                       return this.date;
+               },
+
+               setDate: function(d) {
+                       this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000)));
+               },
+
+               setUTCDate: function(d) {
+                       this.date = d;
+                       this.setValue();
+               },
+
+               setValue: function() {
+                       var formatted = this.getFormattedDate();
+                       if (!this.isInput) {
+                               if (this.component){
+                                       this.element.find('input').val(formatted);
+                               }
+                               this.element.data('date', formatted);
+                       } else {
+                               this.element.val(formatted);
+                       }
+               },
+
+               getFormattedDate: function(format) {
+                       if (format === undefined)
+                               format = this.format;
+                       return DPGlobal.formatDate(this.date, format, this.language);
+               },
+
+               setStartDate: function(startDate){
+                       this.startDate = startDate||-Infinity;
+                       if (this.startDate !== -Infinity) {
+                               this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language);
+                       }
+                       this.update();
+                       this.updateNavArrows();
+               },
+
+               setEndDate: function(endDate){
+                       this.endDate = endDate||Infinity;
+                       if (this.endDate !== Infinity) {
+                               this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language);
+                       }
+                       this.update();
+                       this.updateNavArrows();
+               },
+
+               setDaysOfWeekDisabled: function(daysOfWeekDisabled){
+                       this.daysOfWeekDisabled = daysOfWeekDisabled||[];
+                       if (!$.isArray(this.daysOfWeekDisabled)) {
+                               this.daysOfWeekDisabled = this.daysOfWeekDisabled.split(/,\s*/);
+                       }
+                       this.daysOfWeekDisabled = $.map(this.daysOfWeekDisabled, function (d) {
+                               return parseInt(d, 10);
+                       });
+                       this.update();
+                       this.updateNavArrows();
+               },
+
+               place: function(){
+                                               if(this.isInline) return;
+                       var zIndex = parseInt(this.element.parents().filter(function() {
+                                                       return $(this).css('z-index') != 'auto';
+                                               }).first().css('z-index'))+10;
+                       var offset = this.component ? this.component.offset() : this.element.offset();
+                       var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true);
+                       this.picker.css({
+                               top: offset.top + height,
+                               left: offset.left,
+                               zIndex: zIndex
+                       });
+               },
+
+               update: function(){
+                       var date, fromArgs = false;
+                       if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
+                               date = arguments[0];
+                               fromArgs = true;
+                       } else {
+                               date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val();
+                       }
+
+                       this.date = DPGlobal.parseDate(date, this.format, this.language);
+
+                       if(fromArgs) this.setValue();
+
+                       var oldViewDate = this.viewDate;
+                       if (this.date < this.startDate) {
+                               this.viewDate = new Date(this.startDate);
+                       } else if (this.date > this.endDate) {
+                               this.viewDate = new Date(this.endDate);
+                       } else {
+                               this.viewDate = new Date(this.date);
+                       }
+
+                       if (oldViewDate && oldViewDate.getTime() != this.viewDate.getTime()){
+                               this.element.trigger({
+                                       type: 'changeDate',
+                                       date: this.viewDate
+                               });
+                       }
+                       this.fill();
+               },
+
+               fillDow: function(){
+                       var dowCnt = this.weekStart,
+                       html = '<tr>';
+                       while (dowCnt < this.weekStart + 7) {
+                               html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>';
+                       }
+                       html += '</tr>';
+                       this.picker.find('.datepicker-days thead').append(html);
+               },
+
+               fillMonths: function(){
+                       var html = '',
+                       i = 0;
+                       while (i < 12) {
+                               html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>';
+                       }
+                       this.picker.find('.datepicker-months td').html(html);
+               },
+
+               fill: function() {
+                       var d = new Date(this.viewDate),
+                               year = d.getUTCFullYear(),
+                               month = d.getUTCMonth(),
+                               startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity,
+                               startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity,
+                               endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity,
+                               endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity,
+                               currentDate = this.date && this.date.valueOf(),
+                               today = new Date();
+                       this.picker.find('.datepicker-days thead th:eq(1)')
+                                               .text(dates[this.language].months[month]+' '+year);
+                       this.picker.find('tfoot th.today')
+                                               .text(dates[this.language].today)
+                                               .toggle(this.todayBtn !== false);
+                       this.updateNavArrows();
+                       this.fillMonths();
+                       var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
+                               day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
+                       prevMonth.setUTCDate(day);
+                       prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7);
+                       var nextMonth = new Date(prevMonth);
+                       nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
+                       nextMonth = nextMonth.valueOf();
+                       var html = [];
+                       var clsName;
+                       while(prevMonth.valueOf() < nextMonth) {
+                               if (prevMonth.getUTCDay() == this.weekStart) {
+                                       html.push('<tr>');
+                               }
+                               clsName = '';
+                               if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) {
+                                       clsName += ' old';
+                               } else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) {
+                                       clsName += ' new';
+                               }
+                               // Compare internal UTC date with local today, not UTC today
+                               if (this.todayHighlight &&
+                                       prevMonth.getUTCFullYear() == today.getFullYear() &&
+                                       prevMonth.getUTCMonth() == today.getMonth() &&
+                                       prevMonth.getUTCDate() == today.getDate()) {
+                                       clsName += ' today';
+                               }
+                               if (currentDate && prevMonth.valueOf() == currentDate) {
+                                       clsName += ' active';
+                               }
+                               if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate ||
+                                       $.inArray(prevMonth.getUTCDay(), this.daysOfWeekDisabled) !== -1) {
+                                       clsName += ' disabled';
+                               }
+                               html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>');
+                               if (prevMonth.getUTCDay() == this.weekEnd) {
+                                       html.push('</tr>');
+                               }
+                               prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
+                       }
+                       this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
+                       var currentYear = this.date && this.date.getUTCFullYear();
+
+                       var months = this.picker.find('.datepicker-months')
+                                               .find('th:eq(1)')
+                                                       .text(year)
+                                                       .end()
+                                               .find('span').removeClass('active');
+                       if (currentYear && currentYear == year) {
+                               months.eq(this.date.getUTCMonth()).addClass('active');
+                       }
+                       if (year < startYear || year > endYear) {
+                               months.addClass('disabled');
+                       }
+                       if (year == startYear) {
+                               months.slice(0, startMonth).addClass('disabled');
+                       }
+                       if (year == endYear) {
+                               months.slice(endMonth+1).addClass('disabled');
+                       }
+
+                       html = '';
+                       year = parseInt(year/10, 10) * 10;
+                       var yearCont = this.picker.find('.datepicker-years')
+                                                               .find('th:eq(1)')
+                                                                       .text(year + '-' + (year + 9))
+                                                                       .end()
+                                                               .find('td');
+                       year -= 1;
+                       for (var i = -1; i < 11; i++) {
+                               html += '<span class="year'+(i == -1 || i == 10 ? ' old' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>';
+                               year += 1;
+                       }
+                       yearCont.html(html);
+               },
+
+               updateNavArrows: function() {
+                       var d = new Date(this.viewDate),
+                               year = d.getUTCFullYear(),
+                               month = d.getUTCMonth();
+                       switch (this.viewMode) {
+                               case 0:
+                                       if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) {
+                                               this.picker.find('.prev').css({visibility: 'hidden'});
+                                       } else {
+                                               this.picker.find('.prev').css({visibility: 'visible'});
+                                       }
+                                       if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) {
+                                               this.picker.find('.next').css({visibility: 'hidden'});
+                                       } else {
+                                               this.picker.find('.next').css({visibility: 'visible'});
+                                       }
+                                       break;
+                               case 1:
+                               case 2:
+                                       if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) {
+                                               this.picker.find('.prev').css({visibility: 'hidden'});
+                                       } else {
+                                               this.picker.find('.prev').css({visibility: 'visible'});
+                                       }
+                                       if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) {
+                                               this.picker.find('.next').css({visibility: 'hidden'});
+                                       } else {
+                                               this.picker.find('.next').css({visibility: 'visible'});
+                                       }
+                                       break;
+                       }
+               },
+
+               click: function(e) {
+                       e.stopPropagation();
+                       e.preventDefault();
+                       var target = $(e.target).closest('span, td, th');
+                       if (target.length == 1) {
+                               switch(target[0].nodeName.toLowerCase()) {
+                                       case 'th':
+                                               switch(target[0].className) {
+                                                       case 'switch':
+                                                               this.showMode(1);
+                                                               break;
+                                                       case 'prev':
+                                                       case 'next':
+                                                               var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
+                                                               switch(this.viewMode){
+                                                                       case 0:
+                                                                               this.viewDate = this.moveMonth(this.viewDate, dir);
+                                                                               break;
+                                                                       case 1:
+                                                                       case 2:
+                                                                               this.viewDate = this.moveYear(this.viewDate, dir);
+                                                                               break;
+                                                               }
+                                                               this.fill();
+                                                               break;
+                                                       case 'today':
+                                                               var date = new Date();
+                                                               date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+
+                                                               this.showMode(-2);
+                                                               var which = this.todayBtn == 'linked' ? null : 'view';
+                                                               this._setDate(date, which);
+                                                               break;
+                                               }
+                                               break;
+                                       case 'span':
+                                               if (!target.is('.disabled')) {
+                                                       this.viewDate.setUTCDate(1);
+                                                       if (target.is('.month')) {
+                                                               var month = target.parent().find('span').index(target);
+                                                               this.viewDate.setUTCMonth(month);
+                                                               this.element.trigger({
+                                                                       type: 'changeMonth',
+                                                                       date: this.viewDate
+                                                               });
+                                                       } else {
+                                                               var year = parseInt(target.text(), 10)||0;
+                                                               this.viewDate.setUTCFullYear(year);
+                                                               this.element.trigger({
+                                                                       type: 'changeYear',
+                                                                       date: this.viewDate
+                                                               });
+                                                       }
+                                                       this.showMode(-1);
+                                                       this.fill();
+                                               }
+                                               break;
+                                       case 'td':
+                                               if (target.is('.day') && !target.is('.disabled')){
+                                                       var day = parseInt(target.text(), 10)||1;
+                                                       var year = this.viewDate.getUTCFullYear(),
+                                                               month = this.viewDate.getUTCMonth();
+                                                       if (target.is('.old')) {
+                                                               if (month === 0) {
+                                                                       month = 11;
+                                                                       year -= 1;
+                                                               } else {
+                                                                       month -= 1;
+                                                               }
+                                                       } else if (target.is('.new')) {
+                                                               if (month == 11) {
+                                                                       month = 0;
+                                                                       year += 1;
+                                                               } else {
+                                                                       month += 1;
+                                                               }
+                                                       }
+                                                       this._setDate(UTCDate(year, month, day,0,0,0,0));
+                                               }
+                                               break;
+                               }
+                       }
+               },
+
+               _setDate: function(date, which){
+                       if (!which || which == 'date')
+                               this.date = date;
+                       if (!which || which  == 'view')
+                               this.viewDate = date;
+                       this.fill();
+                       this.setValue();
+                       this.element.trigger({
+                               type: 'changeDate',
+                               date: this.date
+                       });
+                       var element;
+                       if (this.isInput) {
+                               element = this.element;
+                       } else if (this.component){
+                               element = this.element.find('input');
+                       }
+                       if (element) {
+                               element.change();
+                               if (this.autoclose && (!which || which == 'date')) {
+                                       this.hide();
+                               }
+                       }
+               },
+
+               moveMonth: function(date, dir){
+                       if (!dir) return date;
+                       var new_date = new Date(date.valueOf()),
+                               day = new_date.getUTCDate(),
+                               month = new_date.getUTCMonth(),
+                               mag = Math.abs(dir),
+                               new_month, test;
+                       dir = dir > 0 ? 1 : -1;
+                       if (mag == 1){
+                               test = dir == -1
+                                       // If going back one month, make sure month is not current month
+                                       // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
+                                       ? function(){ return new_date.getUTCMonth() == month; }
+                                       // If going forward one month, make sure month is as expected
+                                       // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
+                                       : function(){ return new_date.getUTCMonth() != new_month; };
+                               new_month = month + dir;
+                               new_date.setUTCMonth(new_month);
+                               // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
+                               if (new_month < 0 || new_month > 11)
+                                       new_month = (new_month + 12) % 12;
+                       } else {
+                               // For magnitudes >1, move one month at a time...
+                               for (var i=0; i<mag; i++)
+                                       // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
+                                       new_date = this.moveMonth(new_date, dir);
+                               // ...then reset the day, keeping it in the new month
+                               new_month = new_date.getUTCMonth();
+                               new_date.setUTCDate(day);
+                               test = function(){ return new_month != new_date.getUTCMonth(); };
+                       }
+                       // Common date-resetting loop -- if date is beyond end of month, make it
+                       // end of month
+                       while (test()){
+                               new_date.setUTCDate(--day);
+                               new_date.setUTCMonth(new_month);
+                       }
+                       return new_date;
+               },
+
+               moveYear: function(date, dir){
+                       return this.moveMonth(date, dir*12);
+               },
+
+               dateWithinRange: function(date){
+                       return date >= this.startDate && date <= this.endDate;
+               },
+
+               keydown: function(e){
+                       if (this.picker.is(':not(:visible)')){
+                               if (e.keyCode == 27) // allow escape to hide and re-show picker
+                                       this.show();
+                               return;
+                       }
+                       var dateChanged = false,
+                               dir, day, month,
+                               newDate, newViewDate;
+                       switch(e.keyCode){
+                               case 27: // escape
+                                       this.hide();
+                                       e.preventDefault();
+                                       break;
+                               case 37: // left
+                               case 39: // right
+                                       if (!this.keyboardNavigation) break;
+                                       dir = e.keyCode == 37 ? -1 : 1;
+                                       if (e.ctrlKey){
+                                               newDate = this.moveYear(this.date, dir);
+                                               newViewDate = this.moveYear(this.viewDate, dir);
+                                       } else if (e.shiftKey){
+                                               newDate = this.moveMonth(this.date, dir);
+                                               newViewDate = this.moveMonth(this.viewDate, dir);
+                                       } else {
+                                               newDate = new Date(this.date);
+                                               newDate.setUTCDate(this.date.getUTCDate() + dir);
+                                               newViewDate = new Date(this.viewDate);
+                                               newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
+                                       }
+                                       if (this.dateWithinRange(newDate)){
+                                               this.date = newDate;
+                                               this.viewDate = newViewDate;
+                                               this.setValue();
+                                               this.update();
+                                               e.preventDefault();
+                                               dateChanged = true;
+                                       }
+                                       break;
+                               case 38: // up
+                               case 40: // down
+                                       if (!this.keyboardNavigation) break;
+                                       dir = e.keyCode == 38 ? -1 : 1;
+                                       if (e.ctrlKey){
+                                               newDate = this.moveYear(this.date, dir);
+                                               newViewDate = this.moveYear(this.viewDate, dir);
+                                       } else if (e.shiftKey){
+                                               newDate = this.moveMonth(this.date, dir);
+                                               newViewDate = this.moveMonth(this.viewDate, dir);
+                                       } else {
+                                               newDate = new Date(this.date);
+                                               newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
+                                               newViewDate = new Date(this.viewDate);
+                                               newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
+                                       }
+                                       if (this.dateWithinRange(newDate)){
+                                               this.date = newDate;
+                                               this.viewDate = newViewDate;
+                                               this.setValue();
+                                               this.update();
+                                               e.preventDefault();
+                                               dateChanged = true;
+                                       }
+                                       break;
+                               case 13: // enter
+                                       this.hide();
+                                       e.preventDefault();
+                                       break;
+                               case 9: // tab
+                                       this.hide();
+                                       break;
+                       }
+                       if (dateChanged){
+                               this.element.trigger({
+                                       type: 'changeDate',
+                                       date: this.date
+                               });
+                               var element;
+                               if (this.isInput) {
+                                       element = this.element;
+                               } else if (this.component){
+                                       element = this.element.find('input');
+                               }
+                               if (element) {
+                                       element.change();
+                               }
+                       }
+               },
+
+               showMode: function(dir) {
+                       if (dir) {
+                               this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir));
+                       }
+                       /*
+                               vitalets: fixing bug of very special conditions:
+                               jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
+                               Method show() does not set display css correctly and datepicker is not shown.
+                               Changed to .css('display', 'block') solve the problem.
+                               See https://github.com/vitalets/x-editable/issues/37
+
+                               In jquery 1.7.2+ everything works fine.
+                       */
+                       //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
+                       this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
+                       this.updateNavArrows();
+               }
+       };
+
+       $.fn.datepicker = function ( option ) {
+               var args = Array.apply(null, arguments);
+               args.shift();
+               return this.each(function () {
+                       var $this = $(this),
+                               data = $this.data('datepicker'),
+                               options = typeof option == 'object' && option;
+                       if (!data) {
+                               $this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options))));
+                       }
+                       if (typeof option == 'string' && typeof data[option] == 'function') {
+                               data[option].apply(data, args);
+                       }
+               });
+       };
+
+       $.fn.datepicker.defaults = {
+       };
+       $.fn.datepicker.Constructor = Datepicker;
+       var dates = $.fn.datepicker.dates = {
+               en: {
+                       days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
+                       daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
+                       daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
+                       months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+                       monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+                       today: "Today"
+               }
+       };
+
+       var DPGlobal = {
+               modes: [
+                       {
+                               clsName: 'days',
+                               navFnc: 'Month',
+                               navStep: 1
+                       },
+                       {
+                               clsName: 'months',
+                               navFnc: 'FullYear',
+                               navStep: 1
+                       },
+                       {
+                               clsName: 'years',
+                               navFnc: 'FullYear',
+                               navStep: 10
+               }],
+               isLeapYear: function (year) {
+                       return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
+               },
+               getDaysInMonth: function (year, month) {
+                       return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
+               },
+               validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
+               nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
+               parseFormat: function(format){
+                       // IE treats \0 as a string end in inputs (truncating the value),
+                       // so it's a bad format delimiter, anyway
+                       var separators = format.replace(this.validParts, '\0').split('\0'),
+                               parts = format.match(this.validParts);
+                       if (!separators || !separators.length || !parts || parts.length === 0){
+                               throw new Error("Invalid date format.");
+                       }
+                       return {separators: separators, parts: parts};
+               },
+               parseDate: function(date, format, language) {
+                       if (date instanceof Date) return date;
+                       if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) {
+                               var part_re = /([\-+]\d+)([dmwy])/,
+                                       parts = date.match(/([\-+]\d+)([dmwy])/g),
+                                       part, dir;
+                               date = new Date();
+                               for (var i=0; i<parts.length; i++) {
+                                       part = part_re.exec(parts[i]);
+                                       dir = parseInt(part[1]);
+                                       switch(part[2]){
+                                               case 'd':
+                                                       date.setUTCDate(date.getUTCDate() + dir);
+                                                       break;
+                                               case 'm':
+                                                       date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
+                                                       break;
+                                               case 'w':
+                                                       date.setUTCDate(date.getUTCDate() + dir * 7);
+                                                       break;
+                                               case 'y':
+                                                       date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
+                                                       break;
+                                       }
+                               }
+                               return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
+                       }
+                       var parts = date && date.match(this.nonpunctuation) || [],
+                               date = new Date(),
+                               parsed = {},
+                               setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
+                               setters_map = {
+                                       yyyy: function(d,v){ return d.setUTCFullYear(v); },
+                                       yy: function(d,v){ return d.setUTCFullYear(2000+v); },
+                                       m: function(d,v){
+                                               v -= 1;
+                                               while (v<0) v += 12;
+                                               v %= 12;
+                                               d.setUTCMonth(v);
+                                               while (d.getUTCMonth() != v)
+                                                       d.setUTCDate(d.getUTCDate()-1);
+                                               return d;
+                                       },
+                                       d: function(d,v){ return d.setUTCDate(v); }
+                               },
+                               val, filtered, part;
+                       setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
+                       setters_map['dd'] = setters_map['d'];
+                       date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+                       var fparts = format.parts.slice();
+                       // Remove noop parts
+                       if (parts.length != fparts.length) {
+                               fparts = $(fparts).filter(function(i,p){
+                                       return $.inArray(p, setters_order) !== -1;
+                               }).toArray();
+                       }
+                       // Process remainder
+                       if (parts.length == fparts.length) {
+                               for (var i=0, cnt = fparts.length; i < cnt; i++) {
+                                       val = parseInt(parts[i], 10);
+                                       part = fparts[i];
+                                       if (isNaN(val)) {
+                                               switch(part) {
+                                                       case 'MM':
+                                                               filtered = $(dates[language].months).filter(function(){
+                                                                       var m = this.slice(0, parts[i].length),
+                                                                               p = parts[i].slice(0, m.length);
+                                                                       return m == p;
+                                                               });
+                                                               val = $.inArray(filtered[0], dates[language].months) + 1;
+                                                               break;
+                                                       case 'M':
+                                                               filtered = $(dates[language].monthsShort).filter(function(){
+                                                                       var m = this.slice(0, parts[i].length),
+                                                                               p = parts[i].slice(0, m.length);
+                                                                       return m == p;
+                                                               });
+                                                               val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
+                                                               break;
+                                               }
+                                       }
+                                       parsed[part] = val;
+                               }
+                               for (var i=0, s; i<setters_order.length; i++){
+                                       s = setters_order[i];
+                                       if (s in parsed && !isNaN(parsed[s]))
+                                               setters_map[s](date, parsed[s]);
+                               }
+                       }
+                       return date;
+               },
+               formatDate: function(date, format, language){
+                       var val = {
+                               d: date.getUTCDate(),
+                               D: dates[language].daysShort[date.getUTCDay()],
+                               DD: dates[language].days[date.getUTCDay()],
+                               m: date.getUTCMonth() + 1,
+                               M: dates[language].monthsShort[date.getUTCMonth()],
+                               MM: dates[language].months[date.getUTCMonth()],
+                               yy: date.getUTCFullYear().toString().substring(2),
+                               yyyy: date.getUTCFullYear()
+                       };
+                       val.dd = (val.d < 10 ? '0' : '') + val.d;
+                       val.mm = (val.m < 10 ? '0' : '') + val.m;
+                       var date = [],
+                               seps = $.extend([], format.separators);
+                       for (var i=0, cnt = format.parts.length; i < cnt; i++) {
+                               if (seps.length)
+                                       date.push(seps.shift());
+                               date.push(val[format.parts[i]]);
+                       }
+                       return date.join('');
+               },
+               headTemplate: '<thead>'+
+                                                       '<tr>'+
+                                                               '<th class="prev"><i class="icon-arrow-left"/></th>'+
+                                                               '<th colspan="5" class="switch"></th>'+
+                                                               '<th class="next"><i class="icon-arrow-right"/></th>'+
+                                                       '</tr>'+
+                                               '</thead>',
+               contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
+               footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr></tfoot>'
+       };
+       DPGlobal.template = '<div class="datepicker">'+
+                                                       '<div class="datepicker-days">'+
+                                                               '<table class=" table-condensed">'+
+                                                                       DPGlobal.headTemplate+
+                                                                       '<tbody></tbody>'+
+                                                                       DPGlobal.footTemplate+
+                                                               '</table>'+
+                                                       '</div>'+
+                                                       '<div class="datepicker-months">'+
+                                                               '<table class="table-condensed">'+
+                                                                       DPGlobal.headTemplate+
+                                                                       DPGlobal.contTemplate+
+                                                                       DPGlobal.footTemplate+
+                                                               '</table>'+
+                                                       '</div>'+
+                                                       '<div class="datepicker-years">'+
+                                                               '<table class="table-condensed">'+
+                                                                       DPGlobal.headTemplate+
+                                                                       DPGlobal.contTemplate+
+                                                                       DPGlobal.footTemplate+
+                                                               '</table>'+
+                                                       '</div>'+
+                                               '</div>';
+
+       $.fn.datepicker.DPGlobal = DPGlobal;
+
+}( window.jQuery );
diff --git a/third-party/bootstrap-datepicker-1/datepicker.css b/third-party/bootstrap-datepicker-1/datepicker.css
new file mode 100644 (file)
index 0000000..6f061df
--- /dev/null
@@ -0,0 +1,514 @@
+/*!
+ * Datepicker for Bootstrap
+ *
+ * Copyright 2012 Stefan Petre
+ * Improvements by Andrew Rowls
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+.datepicker {
+  padding: 4px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  direction: ltr;
+  /*.dow {
+               border-top: 1px solid #ddd !important;
+       }*/
+}
+.datepicker-inline {
+  width: 220px;
+}
+.datepicker.datepicker-rtl {
+  direction: rtl;
+}
+.datepicker.datepicker-rtl table tr td span {
+  float: right;
+}
+.datepicker-dropdown {
+  top: 0;
+  left: 0;
+}
+.datepicker-dropdown:before {
+  content: '';
+  display: inline-block;
+  border-left: 7px solid transparent;
+  border-right: 7px solid transparent;
+  border-bottom: 7px solid #ccc;
+  border-top: 0;
+  border-bottom-color: rgba(0, 0, 0, 0.2);
+  position: absolute;
+}
+.datepicker-dropdown:after {
+  content: '';
+  display: inline-block;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid #ffffff;
+  border-top: 0;
+  position: absolute;
+}
+.datepicker-dropdown.datepicker-orient-left:before {
+  left: 6px;
+}
+.datepicker-dropdown.datepicker-orient-left:after {
+  left: 7px;
+}
+.datepicker-dropdown.datepicker-orient-right:before {
+  right: 6px;
+}
+.datepicker-dropdown.datepicker-orient-right:after {
+  right: 7px;
+}
+.datepicker-dropdown.datepicker-orient-top:before {
+  top: -7px;
+}
+.datepicker-dropdown.datepicker-orient-top:after {
+  top: -6px;
+}
+.datepicker-dropdown.datepicker-orient-bottom:before {
+  bottom: -7px;
+  border-bottom: 0;
+  border-top: 7px solid #999;
+}
+.datepicker-dropdown.datepicker-orient-bottom:after {
+  bottom: -6px;
+  border-bottom: 0;
+  border-top: 6px solid #ffffff;
+}
+.datepicker > div {
+  display: none;
+}
+.datepicker.days div.datepicker-days {
+  display: block;
+}
+.datepicker.months div.datepicker-months {
+  display: block;
+}
+.datepicker.years div.datepicker-years {
+  display: block;
+}
+.datepicker table {
+  margin: 0;
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.datepicker td,
+.datepicker th {
+  text-align: center;
+  width: 20px;
+  height: 20px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  border: none;
+}
+.table-striped .datepicker table tr td,
+.table-striped .datepicker table tr th {
+  background-color: transparent;
+}
+.datepicker table tr td.day:hover,
+.datepicker table tr td.day.focused {
+  background: #eeeeee;
+  cursor: pointer;
+}
+.datepicker table tr td.old,
+.datepicker table tr td.new {
+  color: #999999;
+}
+.datepicker table tr td.disabled,
+.datepicker table tr td.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker table tr td.today,
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today.disabled:hover {
+  background-color: #fde19a;
+  background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
+  background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -o-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: linear-gradient(top, #fdd49a, #fdf59a);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
+  border-color: #fdf59a #fdf59a #fbed50;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #000;
+}
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today:hover:hover,
+.datepicker table tr td.today.disabled:hover,
+.datepicker table tr td.today.disabled:hover:hover,
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today:hover.disabled,
+.datepicker table tr td.today.disabled.disabled,
+.datepicker table tr td.today.disabled:hover.disabled,
+.datepicker table tr td.today[disabled],
+.datepicker table tr td.today:hover[disabled],
+.datepicker table tr td.today.disabled[disabled],
+.datepicker table tr td.today.disabled:hover[disabled] {
+  background-color: #fdf59a;
+}
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active {
+  background-color: #fbf069 \9;
+}
+.datepicker table tr td.today:hover:hover {
+  color: #000;
+}
+.datepicker table tr td.today.active:hover {
+  color: #fff;
+}
+.datepicker table tr td.range,
+.datepicker table tr td.range:hover,
+.datepicker table tr td.range.disabled,
+.datepicker table tr td.range.disabled:hover {
+  background: #eeeeee;
+  -webkit-border-radius: 0;
+  -moz-border-radius: 0;
+  border-radius: 0;
+}
+.datepicker table tr td.range.today,
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today.disabled:hover {
+  background-color: #f3d17a;
+  background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
+  background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: -o-linear-gradient(top, #f3c17a, #f3e97a);
+  background-image: linear-gradient(top, #f3c17a, #f3e97a);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
+  border-color: #f3e97a #f3e97a #edde34;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  -webkit-border-radius: 0;
+  -moz-border-radius: 0;
+  border-radius: 0;
+}
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today:hover:hover,
+.datepicker table tr td.range.today.disabled:hover,
+.datepicker table tr td.range.today.disabled:hover:hover,
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today:hover.disabled,
+.datepicker table tr td.range.today.disabled.disabled,
+.datepicker table tr td.range.today.disabled:hover.disabled,
+.datepicker table tr td.range.today[disabled],
+.datepicker table tr td.range.today:hover[disabled],
+.datepicker table tr td.range.today.disabled[disabled],
+.datepicker table tr td.range.today.disabled:hover[disabled] {
+  background-color: #f3e97a;
+}
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active {
+  background-color: #efe24b \9;
+}
+.datepicker table tr td.selected,
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected.disabled:hover {
+  background-color: #9e9e9e;
+  background-image: -moz-linear-gradient(top, #b3b3b3, #808080);
+  background-image: -ms-linear-gradient(top, #b3b3b3, #808080);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
+  background-image: -webkit-linear-gradient(top, #b3b3b3, #808080);
+  background-image: -o-linear-gradient(top, #b3b3b3, #808080);
+  background-image: linear-gradient(top, #b3b3b3, #808080);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
+  border-color: #808080 #808080 #595959;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected:hover:hover,
+.datepicker table tr td.selected.disabled:hover,
+.datepicker table tr td.selected.disabled:hover:hover,
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected:hover.disabled,
+.datepicker table tr td.selected.disabled.disabled,
+.datepicker table tr td.selected.disabled:hover.disabled,
+.datepicker table tr td.selected[disabled],
+.datepicker table tr td.selected:hover[disabled],
+.datepicker table tr td.selected.disabled[disabled],
+.datepicker table tr td.selected.disabled:hover[disabled] {
+  background-color: #808080;
+}
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active {
+  background-color: #666666 \9;
+}
+.datepicker table tr td.active,
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active:hover:hover,
+.datepicker table tr td.active.disabled:hover,
+.datepicker table tr td.active.disabled:hover:hover,
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active:hover.disabled,
+.datepicker table tr td.active.disabled.disabled,
+.datepicker table tr td.active.disabled:hover.disabled,
+.datepicker table tr td.active[disabled],
+.datepicker table tr td.active:hover[disabled],
+.datepicker table tr td.active.disabled[disabled],
+.datepicker table tr td.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker table tr td span {
+  display: block;
+  width: 23%;
+  height: 54px;
+  line-height: 54px;
+  float: left;
+  margin: 1%;
+  cursor: pointer;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.datepicker table tr td span:hover {
+  background: #eeeeee;
+}
+.datepicker table tr td span.disabled,
+.datepicker table tr td span.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker table tr td span.active,
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active:hover:hover,
+.datepicker table tr td span.active.disabled:hover,
+.datepicker table tr td span.active.disabled:hover:hover,
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active:hover.disabled,
+.datepicker table tr td span.active.disabled.disabled,
+.datepicker table tr td span.active.disabled:hover.disabled,
+.datepicker table tr td span.active[disabled],
+.datepicker table tr td span.active:hover[disabled],
+.datepicker table tr td span.active.disabled[disabled],
+.datepicker table tr td span.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker table tr td span.old,
+.datepicker table tr td span.new {
+  color: #999999;
+}
+.datepicker th.datepicker-switch {
+  width: 145px;
+}
+.datepicker thead tr:first-child th,
+.datepicker tfoot tr th {
+  cursor: pointer;
+}
+.datepicker thead tr:first-child th:hover,
+.datepicker tfoot tr th:hover {
+  background: #eeeeee;
+}
+.datepicker .cw {
+  font-size: 10px;
+  width: 12px;
+  padding: 0 2px 0 5px;
+  vertical-align: middle;
+}
+.datepicker thead tr:first-child th.cw {
+  cursor: default;
+  background-color: transparent;
+}
+.input-append.date .add-on i,
+.input-prepend.date .add-on i {
+  cursor: pointer;
+  width: 16px;
+  height: 16px;
+}
+.input-daterange input {
+  text-align: center;
+}
+.input-daterange input:first-child {
+  -webkit-border-radius: 3px 0 0 3px;
+  -moz-border-radius: 3px 0 0 3px;
+  border-radius: 3px 0 0 3px;
+}
+.input-daterange input:last-child {
+  -webkit-border-radius: 0 3px 3px 0;
+  -moz-border-radius: 0 3px 3px 0;
+  border-radius: 0 3px 3px 0;
+}
+.input-daterange .add-on {
+  display: inline-block;
+  width: auto;
+  min-width: 16px;
+  height: 20px;
+  padding: 4px 5px;
+  font-weight: normal;
+  line-height: 20px;
+  text-align: center;
+  text-shadow: 0 1px 0 #ffffff;
+  vertical-align: middle;
+  background-color: #eeeeee;
+  border: 1px solid #ccc;
+  margin-left: -5px;
+  margin-right: -5px;
+}
+.datepicker.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  float: left;
+  display: none;
+  min-width: 160px;
+  list-style: none;
+  background-color: #ffffff;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  -webkit-background-clip: padding-box;
+  -moz-background-clip: padding;
+  background-clip: padding-box;
+  *border-right-width: 2px;
+  *border-bottom-width: 2px;
+  color: #333333;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  line-height: 20px;
+}
+.datepicker.dropdown-menu th,
+.datepicker.datepicker-inline th,
+.datepicker.dropdown-menu td,
+.datepicker.datepicker-inline td {
+  padding: 4px 5px;
+}
diff --git a/third-party/bootstrap-slider b/third-party/bootstrap-slider
new file mode 120000 (symlink)
index 0000000..cb49b67
--- /dev/null
@@ -0,0 +1 @@
+bootstrap-slider-1
\ No newline at end of file
diff --git a/third-party/bootstrap-slider-1/bootstrap-slider.js b/third-party/bootstrap-slider-1/bootstrap-slider.js
new file mode 100644 (file)
index 0000000..a101dc3
--- /dev/null
@@ -0,0 +1,776 @@
+/* =========================================================
+ * bootstrap-slider.js v3.0.0
+ * =========================================================
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+(function( $ ) {
+
+       var ErrorMsgs = {
+               formatInvalidInputErrorMsg : function(input) {
+                       return "Invalid input value '" + input + "' passed in";
+               },
+               callingContextNotSliderInstance : "Calling context element does not have instance of Slider bound to it. Check your code to make sure the JQuery object returned from the call to the slider() initializer is calling the method"
+       };
+
+       var Slider = function(element, options) {
+               var el = this.element = $(element).hide();
+               var origWidth =  $(element)[0].style.width;
+
+               var updateSlider = false;
+               var parent = this.element.parent();
+
+
+               if (parent.hasClass('slider') === true) {
+                       updateSlider = true;
+                       this.picker = parent;
+               } else {
+                       this.picker = $('<div class="slider">'+
+                                                               '<div class="slider-track">'+
+                                                                       '<div class="slider-selection"></div>'+
+                                                                       '<div class="slider-handle min-slider-handle"></div>'+
+                                                                       '<div class="slider-handle max-slider-handle"></div>'+
+                                                               '</div>'+
+                                                               '<div id="tooltip" class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
+                                                               '<div id="tooltip_min" class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
+                                                               '<div id="tooltip_max" class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
+                                                       '</div>')
+                                                               .insertBefore(this.element)
+                                                               .append(this.element);
+               }
+
+               this.id = this.element.data('slider-id')||options.id;
+               if (this.id) {
+                       this.picker[0].id = this.id;
+               }
+
+               if (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch) {
+                       this.touchCapable = true;
+               }
+
+               var tooltip = this.element.data('slider-tooltip')||options.tooltip;
+
+               this.tooltip = this.picker.find('#tooltip');
+               this.tooltipInner = this.tooltip.find('div.tooltip-inner');
+
+               this.tooltip_min = this.picker.find('#tooltip_min');
+               this.tooltipInner_min = this.tooltip_min.find('div.tooltip-inner');
+
+               this.tooltip_max = this.picker.find('#tooltip_max');
+               this.tooltipInner_max= this.tooltip_max.find('div.tooltip-inner');
+
+               if (updateSlider === true) {
+                       // Reset classes
+                       this.picker.removeClass('slider-horizontal');
+                       this.picker.removeClass('slider-vertical');
+                       this.tooltip.removeClass('hide');
+                       this.tooltip_min.removeClass('hide');
+                       this.tooltip_max.removeClass('hide');
+
+               }
+
+               this.orientation = this.element.data('slider-orientation')||options.orientation;
+               switch(this.orientation) {
+                       case 'vertical':
+                               this.picker.addClass('slider-vertical');
+                               this.stylePos = 'top';
+                               this.mousePos = 'pageY';
+                               this.sizePos = 'offsetHeight';
+                               this.tooltip.addClass('right')[0].style.left = '100%';
+                               this.tooltip_min.addClass('right')[0].style.left = '100%';
+                               this.tooltip_max.addClass('right')[0].style.left = '100%';
+                               break;
+                       default:
+                               this.picker
+                                       .addClass('slider-horizontal')
+                                       .css('width', origWidth);
+                               this.orientation = 'horizontal';
+                               this.stylePos = 'left';
+                               this.mousePos = 'pageX';
+                               this.sizePos = 'offsetWidth';
+                               this.tooltip.addClass('top')[0].style.top = -this.tooltip.outerHeight() - 14 + 'px';
+                               this.tooltip_min.addClass('top')[0].style.top = -this.tooltip_min.outerHeight() - 14 + 'px';
+                               this.tooltip_max.addClass('top')[0].style.top = -this.tooltip_max.outerHeight() - 14 + 'px';
+                               break;
+               }
+
+               var self = this;
+               $.each(['min',
+                               'max',
+                               'step',
+                               'precision',
+                               'value',
+                               'reversed',
+                               'handle'
+                       ], function(i, attr) {
+                               if (typeof el.data('slider-' + attr) !== 'undefined') {
+                                       self[attr] = el.data('slider-' + attr);
+                               } else if (typeof options[attr] !== 'undefined') {
+                                       self[attr] = options[attr];
+                               } else if (typeof el.prop(attr) !== 'undefined') {
+                                       self[attr] = el.prop(attr);
+                               } else {
+                                       self[attr] = 0; // to prevent empty string issues in calculations in IE
+                               }
+               });
+
+               if (this.value instanceof Array) {
+                       if (updateSlider && !this.range) {
+                               this.value = this.value[0];
+                       } else {
+                               this.range = true;
+                       }
+               } else if (this.range) {
+                       // User wants a range, but value is not an array
+                       this.value = [this.value, this.max];
+               }
+
+               this.selection = this.element.data('slider-selection')||options.selection;
+               this.selectionEl = this.picker.find('.slider-selection');
+               if (this.selection === 'none') {
+                       this.selectionEl.addClass('hide');
+               }
+
+               this.selectionElStyle = this.selectionEl[0].style;
+
+               this.handle1 = this.picker.find('.slider-handle:first');
+               this.handle1Stype = this.handle1[0].style;
+
+               this.handle2 = this.picker.find('.slider-handle:last');
+               this.handle2Stype = this.handle2[0].style;
+
+               if (updateSlider === true) {
+                       // Reset classes
+                       this.handle1.removeClass('round triangle');
+                       this.handle2.removeClass('round triangle hide');
+               }
+
+               var availableHandleModifiers = ['round', 'triangle', 'custom'];
+               if (availableHandleModifiers.indexOf(this.handle) !== -1){
+                       this.handle1.addClass(this.handle);
+                       this.handle2.addClass(this.handle);
+               }
+
+               this.offset = this.picker.offset();
+               this.size = this.picker[0][this.sizePos];
+               this.formater = options.formater;
+               
+               this.tooltip_separator = options.tooltip_separator;
+               this.tooltip_split = options.tooltip_split;
+
+               this.setValue(this.value);
+
+               this.handle1.on({
+                       keydown: $.proxy(this.keydown, this, 0)
+               });
+               this.handle2.on({
+                       keydown: $.proxy(this.keydown, this, 1)
+               });
+
+               if (this.touchCapable) {
+                       // Touch: Bind touch events:
+                       this.picker.on({
+                               touchstart: $.proxy(this.mousedown, this)
+                       });
+               }
+               // Bind mouse events:
+               this.picker.on({
+                       mousedown: $.proxy(this.mousedown, this)
+               });
+
+               if(tooltip === 'hide') {
+                       this.tooltip.addClass('hide');
+                       this.tooltip_min.addClass('hide');
+                       this.tooltip_max.addClass('hide');
+               } else if(tooltip === 'always') {
+                       this.showTooltip();
+                       this.alwaysShowTooltip = true;
+               } else {
+                       this.picker.on({
+                               mouseenter: $.proxy(this.showTooltip, this),
+                               mouseleave: $.proxy(this.hideTooltip, this)
+                       });
+                       this.handle1.on({
+                               focus: $.proxy(this.showTooltip, this),
+                               blur: $.proxy(this.hideTooltip, this)
+                       });
+                       this.handle2.on({
+                               focus: $.proxy(this.showTooltip, this),
+                               blur: $.proxy(this.hideTooltip, this)
+                       });
+               }
+
+               this.enabled = options.enabled &&
+                                               (this.element.data('slider-enabled') === undefined || this.element.data('slider-enabled') === true);
+               if(this.enabled) {
+                       this.enable();
+               } else {
+                       this.disable();
+               }
+               this.natural_arrow_keys = this.element.data('slider-natural_arrow_keys') || options.natural_arrow_keys;
+       };
+
+       Slider.prototype = {
+               constructor: Slider,
+
+               over: false,
+               inDrag: false,
+
+               showTooltip: function(){
+            if (this.tooltip_split === false ){
+                this.tooltip.addClass('in');
+            } else {
+                this.tooltip_min.addClass('in');
+                this.tooltip_max.addClass('in');
+            }
+
+                       this.over = true;
+               },
+
+               hideTooltip: function(){
+                       if (this.inDrag === false && this.alwaysShowTooltip !== true) {
+                               this.tooltip.removeClass('in');
+                               this.tooltip_min.removeClass('in');
+                               this.tooltip_max.removeClass('in');
+                       }
+                       this.over = false;
+               },
+
+               layout: function(){
+                       var positionPercentages;
+
+                       if(this.reversed) {
+                               positionPercentages = [ 100 - this.percentage[0], this.percentage[1] ];
+                       } else {
+                               positionPercentages = [ this.percentage[0], this.percentage[1] ];
+                       }
+
+                       this.handle1Stype[this.stylePos] = positionPercentages[0]+'%';
+                       this.handle2Stype[this.stylePos] = positionPercentages[1]+'%';
+
+                       if (this.orientation === 'vertical') {
+                               this.selectionElStyle.top = Math.min(positionPercentages[0], positionPercentages[1]) +'%';
+                               this.selectionElStyle.height = Math.abs(positionPercentages[0] - positionPercentages[1]) +'%';
+                       } else {
+                               this.selectionElStyle.left = Math.min(positionPercentages[0], positionPercentages[1]) +'%';
+                               this.selectionElStyle.width = Math.abs(positionPercentages[0] - positionPercentages[1]) +'%';
+
+                var offset_min = this.tooltip_min[0].getBoundingClientRect();
+                var offset_max = this.tooltip_max[0].getBoundingClientRect();
+
+                if (offset_min.right > offset_max.left) {
+                    this.tooltip_max.removeClass('top');
+                    this.tooltip_max.addClass('bottom')[0].style.top = 18 + 'px';
+                } else {
+                    this.tooltip_max.removeClass('bottom');
+                    this.tooltip_max.addClass('top')[0].style.top = -30 + 'px';
+                }
+                       }
+
+                       if (this.range) {
+                               this.tooltipInner.text(
+                                       this.formater(this.value[0]) + this.tooltip_separator + this.formater(this.value[1])
+                               );
+                               this.tooltip[0].style[this.stylePos] = (positionPercentages[1] + positionPercentages[0])/2 + '%';
+                               if (this.orientation === 'vertical') {
+                                       this.tooltip.css('margin-top', -this.tooltip.outerHeight() / 2 + 'px');
+                               } else {
+                                       this.tooltip.css('margin-left', -this.tooltip.outerWidth() / 2 + 'px');
+                               }
+                               
+                               if (this.orientation === 'vertical') {
+                                       this.tooltip.css('margin-top', -this.tooltip.outerHeight() / 2 + 'px');
+                               } else {
+                                       this.tooltip.css('margin-left', -this.tooltip.outerWidth() / 2 + 'px');
+                               }
+                               this.tooltipInner_min.text(
+                                       this.formater(this.value[0])
+                               );
+                               this.tooltipInner_max.text(
+                                       this.formater(this.value[1])
+                               );
+
+                               this.tooltip_min[0].style[this.stylePos] = positionPercentages[0] + '%';
+                               if (this.orientation === 'vertical') {
+                                       this.tooltip_min.css('margin-top', -this.tooltip_min.outerHeight() / 2 + 'px');
+                               } else {
+                                       this.tooltip_min.css('margin-left', -this.tooltip_min.outerWidth() / 2 + 'px');
+                               }
+                               this.tooltip_max[0].style[this.stylePos] = positionPercentages[1] + '%';
+                               if (this.orientation === 'vertical') {
+                                       this.tooltip_max.css('margin-top', -this.tooltip_max.outerHeight() / 2 + 'px');
+                               } else {
+                                       this.tooltip_max.css('margin-left', -this.tooltip_max.outerWidth() / 2 + 'px');
+                               }
+                       } else {
+                               this.tooltipInner.text(
+                                       this.formater(this.value[0])
+                               );
+                               this.tooltip[0].style[this.stylePos] = positionPercentages[0] + '%';
+                               if (this.orientation === 'vertical') {
+                                       this.tooltip.css('margin-top', -this.tooltip.outerHeight() / 2 + 'px');
+                               } else {
+                                       this.tooltip.css('margin-left', -this.tooltip.outerWidth() / 2 + 'px');
+                               }
+                       }
+               },
+
+               mousedown: function(ev) {
+                       if(!this.isEnabled()) {
+                               return false;
+                       }
+                       // Touch: Get the original event:
+                       if (this.touchCapable && ev.type === 'touchstart') {
+                               ev = ev.originalEvent;
+                       }
+
+                       this.triggerFocusOnHandle();
+
+                       this.offset = this.picker.offset();
+                       this.size = this.picker[0][this.sizePos];
+
+                       var percentage = this.getPercentage(ev);
+
+                       if (this.range) {
+                               var diff1 = Math.abs(this.percentage[0] - percentage);
+                               var diff2 = Math.abs(this.percentage[1] - percentage);
+                               this.dragged = (diff1 < diff2) ? 0 : 1;
+                       } else {
+                               this.dragged = 0;
+                       }
+
+                       this.percentage[this.dragged] = this.reversed ? 100 - percentage : percentage;
+                       this.layout();
+
+                       if (this.touchCapable) {
+                               // Touch: Bind touch events:
+                               $(document).on({
+                                       touchmove: $.proxy(this.mousemove, this),
+                                       touchend: $.proxy(this.mouseup, this)
+                               });
+                       }
+                       // Bind mouse events:
+                       $(document).on({
+                               mousemove: $.proxy(this.mousemove, this),
+                               mouseup: $.proxy(this.mouseup, this)
+                       });
+
+                       this.inDrag = true;
+                       var val = this.calculateValue();
+                       this.element.trigger({
+                                       type: 'slideStart',
+                                       value: val
+                               })
+                               .data('value', val)
+                               .prop('value', val);
+                       this.setValue(val);
+                       return true;
+               },
+
+               triggerFocusOnHandle: function(handleIdx) {
+                       if(handleIdx === 0) {
+                               this.handle1.focus();
+                       }
+                       if(handleIdx === 1) {
+                               this.handle2.focus();
+                       }
+               },
+
+               keydown: function(handleIdx, ev) {
+                       if(!this.isEnabled()) {
+                               return false;
+                       }
+
+                       var dir;
+                       switch (ev.which) {
+                               case 37: // left
+                               case 40: // down
+                                       dir = -1;
+                                       break;
+                               case 39: // right
+                               case 38: // up
+                                       dir = 1;
+                                       break;
+                       }
+                       if (!dir) {
+                               return;
+                       }
+
+                       // use natural arrow keys instead of from min to max
+                       if (this.natural_arrow_keys) {
+                               if ((this.orientation === 'vertical' && !this.reversed) || (this.orientation === 'horizontal' && this.reversed)) {
+                                       dir = dir * -1;
+                               }
+                       }
+
+                       var oneStepValuePercentageChange = dir * this.percentage[2];
+                       var percentage = this.percentage[handleIdx] + oneStepValuePercentageChange;
+
+                       if (percentage > 100) {
+                               percentage = 100;
+                       } else if (percentage < 0) {
+                               percentage = 0;
+                       }
+
+                       this.dragged = handleIdx;
+                       this.adjustPercentageForRangeSliders(percentage);
+                       this.percentage[this.dragged] = percentage;
+                       this.layout();
+
+                       var val = this.calculateValue();
+                       
+                       this.element.trigger({
+                                       type: 'slideStart',
+                                       value: val
+                               })
+                               .data('value', val)
+                               .prop('value', val);
+
+                       this.setValue(val, true);
+
+                       this.element
+                               .trigger({
+                                       type: 'slideStop',
+                                       value: val
+                               })
+                               .data('value', val)
+                               .prop('value', val);
+                       return false;
+               },
+
+               mousemove: function(ev) {
+                       if(!this.isEnabled()) {
+                               return false;
+                       }
+                       // Touch: Get the original event:
+                       if (this.touchCapable && ev.type === 'touchmove') {
+                               ev = ev.originalEvent;
+                       }
+
+                       var percentage = this.getPercentage(ev);
+                       this.adjustPercentageForRangeSliders(percentage);
+                       this.percentage[this.dragged] = this.reversed ? 100 - percentage : percentage;
+                       this.layout();
+
+                       var val = this.calculateValue();
+                       this.setValue(val, true);
+
+                       return false;
+               },
+               adjustPercentageForRangeSliders: function(percentage) {
+                       if (this.range) {
+                               if (this.dragged === 0 && this.percentage[1] < percentage) {
+                                       this.percentage[0] = this.percentage[1];
+                                       this.dragged = 1;
+                               } else if (this.dragged === 1 && this.percentage[0] > percentage) {
+                                       this.percentage[1] = this.percentage[0];
+                                       this.dragged = 0;
+                               }
+                       }
+               },
+
+               mouseup: function() {
+                       if(!this.isEnabled()) {
+                               return false;
+                       }
+                       if (this.touchCapable) {
+                               // Touch: Unbind touch event handlers:
+                               $(document).off({
+                                       touchmove: this.mousemove,
+                                       touchend: this.mouseup
+                               });
+                       }
+                       // Unbind mouse event handlers:
+                       $(document).off({
+                               mousemove: this.mousemove,
+                               mouseup: this.mouseup
+                       });
+
+                       this.inDrag = false;
+                       if (this.over === false) {
+                               this.hideTooltip();
+                       }
+                       var val = this.calculateValue();
+                       this.layout();
+                       this.element
+                               .data('value', val)
+                               .prop('value', val)
+                               .trigger({
+                                       type: 'slideStop',
+                                       value: val
+                               });
+                       return false;
+               },
+
+               calculateValue: function() {
+                       var val;
+                       if (this.range) {
+                               val = [this.min,this.max];
+                if (this.percentage[0] !== 0){
+                    val[0] = (Math.max(this.min, this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step));
+                    val[0] = this.applyPrecision(val[0]);
+                }
+                if (this.percentage[1] !== 100){
+                    val[1] = (Math.min(this.max, this.min + Math.round((this.diff * this.percentage[1]/100)/this.step)*this.step));
+                    val[1] = this.applyPrecision(val[1]);
+                }
+                               this.value = val;
+                       } else {
+                               val = (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step);
+                               if (val < this.min) {
+                                       val = this.min;
+                               }
+                               else if (val > this.max) {
+                                       val = this.max;
+                               }
+                               val = parseFloat(val);
+                               val = this.applyPrecision(val);
+                               this.value = [val, this.value[1]];
+                       }
+                       return val;
+               },
+               applyPrecision: function(val) {
+                       var precision = this.precision || this.getNumDigitsAfterDecimalPlace(this.step);
+                       return this.applyToFixedAndParseFloat(val, precision);
+               },
+               /*
+                       Credits to Mike Samuel for the following method!
+                       Source: http://stackoverflow.com/questions/10454518/javascript-how-to-retrieve-the-number-of-decimals-of-a-string-number
+               */
+               getNumDigitsAfterDecimalPlace: function(num) {
+                       var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
+                       if (!match) { return 0; }
+                       return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0));
+               },
+
+               applyToFixedAndParseFloat: function(num, toFixedInput) {
+                       var truncatedNum = num.toFixed(toFixedInput);
+                       return parseFloat(truncatedNum);
+               },
+
+               getPercentage: function(ev) {
+                       if (this.touchCapable && (ev.type === 'touchstart' || ev.type === 'touchmove')) {
+                               ev = ev.touches[0];
+                       }
+                       var percentage = (ev[this.mousePos] - this.offset[this.stylePos])*100/this.size;
+                       percentage = Math.round(percentage/this.percentage[2])*this.percentage[2];
+                       return Math.max(0, Math.min(100, percentage));
+               },
+
+               getValue: function() {
+                       if (this.range) {
+                               return this.value;
+                       }
+                       return this.value[0];
+               },
+
+               setValue: function(val, triggerSlideEvent) {
+                       if (!val) {
+                               val = 0;
+                       }
+                       this.value = this.validateInputValue(val);
+
+                       if (this.range) {
+                               this.value[0] = this.applyPrecision(this.value[0]);
+                               this.value[1] = this.applyPrecision(this.value[1]); 
+
+                               this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0]));
+                               this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1]));
+                       } else {
+                               this.value = this.applyPrecision(this.value);
+                               this.value = [ Math.max(this.min, Math.min(this.max, this.value))];
+                               this.handle2.addClass('hide');
+                               if (this.selection === 'after') {
+                                       this.value[1] = this.max;
+                               } else {
+                                       this.value[1] = this.min;
+                               }
+                       }
+
+                       this.diff = this.max - this.min;
+                       if (this.diff > 0) {
+                               this.percentage = [
+                                       (this.value[0] - this.min) * 100 / this.diff,
+                                       (this.value[1] - this.min) * 100 / this.diff,
+                                       this.step * 100 / this.diff
+                               ];
+                       } else {
+                               this.percentage = [0, 0, 100];
+                       }
+
+                       this.layout();
+
+
+                       if(triggerSlideEvent === true) {
+                               var slideEventValue = this.range ? this.value : this.value[0];
+                               this.element
+                                       .trigger({
+                                               'type': 'slide',
+                                               'value': slideEventValue
+                                       })
+                                       .data('value', slideEventValue)
+                                       .prop('value', slideEventValue);
+                       }
+               },
+
+               validateInputValue : function(val) {
+                       if(typeof val === 'number') {
+                               return val;
+                       } else if(val instanceof Array) {
+                               $.each(val, function(i, input) { if (typeof input !== 'number') { throw new Error( ErrorMsgs.formatInvalidInputErrorMsg(input) ); }});
+                               return val;
+                       } else {
+                               throw new Error( ErrorMsgs.formatInvalidInputErrorMsg(val) );
+                       }
+               },
+
+               destroy: function(){
+                       this.handle1.off();
+                       this.handle2.off();
+                       this.element.off().show().insertBefore(this.picker);
+                       this.picker.off().remove();
+                       $(this.element).removeData('slider');
+               },
+
+               disable: function() {
+                       this.enabled = false;
+                       this.handle1.removeAttr("tabindex");
+                       this.handle2.removeAttr("tabindex");
+                       this.picker.addClass('slider-disabled');
+                       this.element.trigger('slideDisabled');
+               },
+
+               enable: function() {
+                       this.enabled = true;
+                       this.handle1.attr("tabindex", 0);
+                       this.handle2.attr("tabindex", 0);
+                       this.picker.removeClass('slider-disabled');
+                       this.element.trigger('slideEnabled');
+               },
+
+               toggle: function() {
+                       if(this.enabled) {
+                               this.disable();
+                       } else {
+                               this.enable();
+                       }
+               },
+
+               isEnabled: function() {
+                       return this.enabled;
+               },
+
+               setAttribute: function(attribute, value) {
+                       this[attribute] = value;
+               },
+
+               getAttribute: function(attribute) {
+                       return this[attribute];
+               }
+
+       };
+
+       var publicMethods = {
+               getValue : Slider.prototype.getValue,
+               setValue : Slider.prototype.setValue,
+               setAttribute : Slider.prototype.setAttribute,
+               getAttribute : Slider.prototype.getAttribute,
+               destroy : Slider.prototype.destroy,
+               disable : Slider.prototype.disable,
+               enable : Slider.prototype.enable,
+               toggle : Slider.prototype.toggle,
+               isEnabled: Slider.prototype.isEnabled
+       };
+
+       $.fn.slider = function (option) {
+               if (typeof option === 'string' && option !== 'refresh') {
+                       var args = Array.prototype.slice.call(arguments, 1);
+                       return invokePublicMethod.call(this, option, args);
+               } else {
+                       return createNewSliderInstance.call(this, option);
+               }
+       };
+
+       function invokePublicMethod(methodName, args) {
+               if(publicMethods[methodName]) {
+                       var sliderObject = retrieveSliderObjectFromElement(this);
+                       var result = publicMethods[methodName].apply(sliderObject, args);
+
+                       if (typeof result === "undefined") {
+                               return $(this);
+                       } else {
+                               return result;
+                       }
+               } else {
+                       throw new Error("method '" + methodName + "()' does not exist for slider.");
+               }
+       }
+
+       function retrieveSliderObjectFromElement(element) {
+               var sliderObject = $(element).data('slider');
+               if(sliderObject && sliderObject instanceof Slider) {
+                       return sliderObject;
+               } else {
+                       throw new Error(ErrorMsgs.callingContextNotSliderInstance);
+               }
+       }
+
+       function createNewSliderInstance(opts) {
+               var $this = $(this);
+               $this.each(function() {
+                       var $this = $(this),
+                               slider = $this.data('slider'),
+                               options = typeof opts === 'object' && opts;
+
+                       // If slider already exists, use its attributes
+                       // as options so slider refreshes properly
+                       if (slider && !options) {
+                               options = {};
+
+                               $.each($.fn.slider.defaults, function(key) {
+                                       options[key] = slider[key];
+                               });
+                       }
+
+                       $this.data('slider', (new Slider(this, $.extend({}, $.fn.slider.defaults, options))));
+               });
+               return $this;
+       }
+
+       $.fn.slider.defaults = {
+               min: 0,
+               max: 10,
+               step: 1,
+               precision: 0,
+               orientation: 'horizontal',
+               value: 5,
+               range: false,
+               selection: 'before',
+               tooltip: 'show',
+               tooltip_separator: ':',
+               tooltip_split: false,
+               natural_arrow_keys: false,
+               handle: 'round',
+               reversed : false,
+               enabled: true,
+               formater: function(value) {
+                       return value;
+               }
+       };
+
+       $.fn.slider.Constructor = Slider;
+
+})( window.jQuery );
+
+/* vim: set noexpandtab tabstop=4 shiftwidth=4 autoindent: */
diff --git a/third-party/bootstrap-slider-1/slider.css b/third-party/bootstrap-slider-1/slider.css
new file mode 100644 (file)
index 0000000..b527aa8
--- /dev/null
@@ -0,0 +1,138 @@
+/*!
+ * Slider for Bootstrap
+ *
+ * Copyright 2012 Stefan Petre
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+.slider {
+  display: inline-block;
+  vertical-align: middle;
+  position: relative;
+}
+.slider.slider-horizontal {
+  width: 210px;
+  height: 20px;
+}
+.slider.slider-horizontal .slider-track {
+  height: 10px;
+  width: 100%;
+  margin-top: -5px;
+  top: 50%;
+  left: 0;
+}
+.slider.slider-horizontal .slider-selection {
+  height: 100%;
+  top: 0;
+  bottom: 0;
+}
+.slider.slider-horizontal .slider-handle {
+  margin-left: -10px;
+  margin-top: -5px;
+}
+.slider.slider-horizontal .slider-handle.triangle {
+  border-width: 0 10px 10px 10px;
+  width: 0;
+  height: 0;
+  border-bottom-color: #0480be;
+  margin-top: 0;
+}
+.slider.slider-vertical {
+  height: 210px;
+  width: 20px;
+}
+.slider.slider-vertical .slider-track {
+  width: 10px;
+  height: 100%;
+  margin-left: -5px;
+  left: 50%;
+  top: 0;
+}
+.slider.slider-vertical .slider-selection {
+  width: 100%;
+  left: 0;
+  top: 0;
+  bottom: 0;
+}
+.slider.slider-vertical .slider-handle {
+  margin-left: -5px;
+  margin-top: -10px;
+}
+.slider.slider-vertical .slider-handle.triangle {
+  border-width: 10px 0 10px 10px;
+  width: 1px;
+  height: 1px;
+  border-left-color: #0480be;
+  margin-left: 0;
+}
+.slider input {
+  display: none;
+}
+.slider .tooltip-inner {
+  white-space: nowrap;
+}
+.slider-track {
+  position: absolute;
+  cursor: pointer;
+  background-color: #f7f7f7;
+  background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));
+  background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9);
+  background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9);
+  background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+  -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.slider-selection {
+  position: absolute;
+  background-color: #f7f7f7;
+  background-image: -moz-linear-gradient(top, #f9f9f9, #f5f5f5);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#f5f5f5));
+  background-image: -webkit-linear-gradient(top, #f9f9f9, #f5f5f5);
+  background-image: -o-linear-gradient(top, #f9f9f9, #f5f5f5);
+  background-image: linear-gradient(to bottom, #f9f9f9, #f5f5f5);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.slider-handle {
+  position: absolute;
+  width: 20px;
+  height: 20px;
+  background-color: #0e90d2;
+  background-image: -moz-linear-gradient(top, #149bdf, #0480be);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));
+  background-image: -webkit-linear-gradient(top, #149bdf, #0480be);
+  background-image: -o-linear-gradient(top, #149bdf, #0480be);
+  background-image: linear-gradient(to bottom, #149bdf, #0480be);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  opacity: 0.8;
+  border: 0px solid transparent;
+}
+.slider-handle.round {
+  -webkit-border-radius: 20px;
+  -moz-border-radius: 20px;
+  border-radius: 20px;
+}
+.slider-handle.triangle {
+  background: transparent none;
+}
\ No newline at end of file
index 151ab1c..2c2495c 100644 (file)
@@ -1,7 +1,7 @@
-<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.js" type="text/javascript"></script>
+<!--<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.js" type="text/javascript"></script>-->
 <!-- <script type="text/javascript">{{ STATIC_URL }}js/ui.widget.js</script> -->
-<script src="{{ STATIC_URL }}js/jquery.notify.js" type="text/javascript"></script>
-<link rel='stylesheet' href='{{ STATIC_URL }}css/ui.notify.css' type='text/css' />
+<!--<script src="{{ STATIC_URL }}js/jquery.notify.js" type="text/javascript"></script>-->
+<!--<link rel='stylesheet' href='{{ STATIC_URL }}css/ui.notify.css' type='text/css' />-->
 
 <script type="text/javascript">
 function create( template, vars, opts ){
@@ -13,7 +13,9 @@ $(function(){
        // the defaults will apply to any notification created within this
        // container, but can be overwritten on notification-by-notification
        // basis.
-       $container = $("#notifications").notify();
+
+       // XXX disabled since jquery ui conflicts with bootstrap!
+       //$container = $("#notifications").notify();
        
        // create two when the pg loads
        //create("default", { title:'Default Notification', text:'Example of a default notification.  I will fade out after 5 seconds'});